Skip to content

Commit 8d2ad82

Browse files
committed
Use SSE to track CRC status change and logs
Signed-off-by: Yevhen Vydolob <[email protected]>
1 parent caccc05 commit 8d2ad82

File tree

10 files changed

+442
-58
lines changed

10 files changed

+442
-58
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import type { Preset } from './types';
2020

21+
export const PRE_SSE_VERSION = '2.30.0';
22+
2123
// copied from https://github.com/crc-org/crc/blob/632676d7c9ba0c030736c3d914984c4e140c1bf5/pkg/crc/constants/constants.go#L198
2224

2325
export function getDefaultCPUs(preset: Preset): number {

src/crc-delete.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function registerDeleteCommand(): void {
3333
}
3434

3535
export async function deleteCrc(suppressNotification = false): Promise<boolean> {
36-
if (crcStatus.status.CrcStatus === 'No Cluster') {
36+
if (crcStatus.status.CrcStatus === 'NoVM') {
3737
if (!suppressNotification) {
3838
await extensionApi.window.showNotification({
3939
silent: false,

src/crc-status.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@
1818

1919
import * as extensionApi from '@podman-desktop/api';
2020
import type { Status, CrcStatus as CrcStatusApi } from './types';
21-
import { commander } from './daemon-commander';
21+
import { commander, getCrcApiUrl } from './daemon-commander';
2222
import { isNeedSetup } from './crc-setup';
23+
import { EventSource } from './events/eventsource';
24+
import type { CrcVersion } from './crc-cli';
25+
import { compare } from 'compare-versions';
26+
import { PRE_SSE_VERSION } from './constants';
2327

2428
const defaultStatus: Status = { CrcStatus: 'Unknown', Preset: 'openshift' };
2529
const setupStatus: Status = { CrcStatus: 'Need Setup', Preset: 'Unknown' };
2630
const errorStatus: Status = { CrcStatus: 'Error', Preset: 'Unknown' };
2731

2832
export class CrcStatus {
29-
private updateTimer: NodeJS.Timer;
3033
private _status: Status;
3134
private isSetupGoing: boolean;
35+
private statusEventSource: EventSource;
36+
private crcVersion: CrcVersion;
37+
private updateTimer: NodeJS.Timer;
3238
private statusChangeEventEmitter = new extensionApi.EventEmitter<Status>();
3339
public readonly onStatusChange = this.statusChangeEventEmitter.event;
3440

@@ -37,30 +43,61 @@ export class CrcStatus {
3743
}
3844

3945
startStatusUpdate(): void {
40-
if (this.updateTimer) {
41-
return; // we already set timer
42-
}
43-
this.updateTimer = setInterval(async () => {
44-
try {
45-
// we don't need to update status while setup is going
46-
if (this.isSetupGoing) {
47-
this._status = createStatus('Starting', this._status.Preset);
48-
return;
46+
if (compare(this.crcVersion.version, PRE_SSE_VERSION, '<=')) {
47+
if (this.updateTimer) {
48+
return; // we already set timer
49+
}
50+
this.updateTimer = setInterval(async () => {
51+
try {
52+
// we don't need to update status while setup is going
53+
if (this.isSetupGoing) {
54+
this._status = createStatus('Starting', this._status.Preset);
55+
return;
56+
}
57+
const oldStatus = this._status;
58+
this._status = await commander.status();
59+
// notify listeners when status changed
60+
if (oldStatus.CrcStatus !== this._status.CrcStatus) {
61+
this.statusChangeEventEmitter.fire(this._status);
62+
}
63+
} catch (e) {
64+
console.error('CRC Status tick: ' + e);
65+
this._status = defaultStatus;
4966
}
50-
const oldStatus = this._status;
51-
this._status = await commander.status();
52-
// notify listeners when status changed
53-
if (oldStatus.CrcStatus !== this._status.CrcStatus) {
67+
}, 2000);
68+
} else {
69+
if (this.statusEventSource) {
70+
return;
71+
}
72+
73+
this.statusEventSource = new EventSource(getCrcApiUrl() + '/events?stream=status_change');
74+
this.statusEventSource.on('status_change', (e: MessageEvent) => {
75+
const data = e.data;
76+
try {
77+
if (this.isSetupGoing) {
78+
this._status = createStatus('Starting', this._status.Preset);
79+
return;
80+
}
81+
console.error(`On Status: ${data}`);
82+
this._status = JSON.parse(data).status;
5483
this.statusChangeEventEmitter.fire(this._status);
84+
} catch (err) {
85+
console.error(err);
86+
this._status = defaultStatus;
5587
}
56-
} catch (e) {
57-
console.error('CRC Status tick: ' + e);
88+
});
89+
this.statusEventSource.on('error', e => {
90+
console.error(e);
5891
this._status = defaultStatus;
59-
}
60-
}, 2000);
92+
});
93+
}
6194
}
6295

6396
stopStatusUpdate(): void {
97+
if (this.statusEventSource) {
98+
this.statusEventSource.close();
99+
this.statusEventSource = undefined;
100+
}
64101
if (this.updateTimer) {
65102
clearInterval(this.updateTimer);
66103
}
@@ -74,7 +111,8 @@ export class CrcStatus {
74111
this._status = errorStatus;
75112
}
76113

77-
async initialize(): Promise<void> {
114+
async initialize(crcVersion: CrcVersion): Promise<void> {
115+
this.crcVersion = crcVersion;
78116
if (isNeedSetup) {
79117
this._status = setupStatus;
80118
return;
@@ -107,7 +145,7 @@ export class CrcStatus {
107145
case 'Stopping':
108146
return 'stopping';
109147
case 'Stopped':
110-
case 'No Cluster':
148+
case 'NoVM':
111149
return 'stopped';
112150
default:
113151
return 'unknown';
@@ -124,7 +162,7 @@ export class CrcStatus {
124162
return 'stopping';
125163
case 'Stopped':
126164
return 'configured';
127-
case 'No Cluster':
165+
case 'NoVM':
128166
return 'installed';
129167
case 'Error':
130168
return 'error';

src/daemon-commander.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ import got from 'got';
2121
import { isWindows } from './util';
2222
import type { ConfigKeys, Configuration, StartInfo, Status } from './types';
2323

24+
export function getCrcApiUrl(): string {
25+
if (isWindows()) {
26+
return 'http://unix://?/pipe/crc-http:';
27+
}
28+
return `http://unix:${process.env.HOME}/.crc/crc-http.sock:`;
29+
}
30+
2431
export class DaemonCommander {
2532
private apiPath: string;
2633

2734
constructor() {
28-
this.apiPath = `http://unix:${process.env.HOME}/.crc/crc-http.sock:/api`;
29-
30-
if (isWindows()) {
31-
this.apiPath = 'http://unix://?/pipe/crc-http:/api';
32-
}
35+
this.apiPath = getCrcApiUrl() + '/api';
3336
}
3437

3538
async status(): Promise<Status> {
@@ -41,7 +44,7 @@ export class DaemonCommander {
4144
} catch (error) {
4245
// ignore status error, as it may happen when no cluster created
4346
return {
44-
CrcStatus: 'No Cluster',
47+
CrcStatus: 'NoVM',
4548
};
4649
}
4750
}

src/events/buffered-reader.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**********************************************************************
2+
* Copyright (C) 2023 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
export interface LineConsumer {
20+
(buf: Buffer, pos: number, fieldLength: number, lineLength: number): void;
21+
}
22+
23+
const maxBufferAheadAllocation = 1024 * 256;
24+
const bom = [239, 187, 191];
25+
const colon = 58;
26+
const lineFeed = 10;
27+
const carriageReturn = 13;
28+
29+
export class BufferedReader {
30+
private buffer: Buffer;
31+
private bytesUsed = 0;
32+
private discardTrailingNewline = false;
33+
private startingFieldLength = -1;
34+
private startingPos = 0;
35+
36+
constructor(private readonly lineConsumer: LineConsumer) {}
37+
38+
onData(chunk: Buffer): void {
39+
let newBuffer: Buffer;
40+
let newBufferSize = 0;
41+
42+
if (!this.buffer) {
43+
this.buffer = chunk;
44+
if (hasBom(this.buffer)) {
45+
this.buffer = this.buffer.subarray(bom.length);
46+
}
47+
this.bytesUsed = this.buffer.length;
48+
} else {
49+
if (chunk.length > this.buffer.length - this.bytesUsed) {
50+
newBufferSize = this.buffer.length * 2 + chunk.length;
51+
if (newBufferSize > maxBufferAheadAllocation) {
52+
newBufferSize = this.buffer.length + chunk.length + maxBufferAheadAllocation;
53+
}
54+
newBuffer = Buffer.alloc(newBufferSize);
55+
this.buffer.copy(newBuffer, 0, 0, this.bytesUsed);
56+
this.buffer = newBuffer;
57+
}
58+
chunk.copy(this.buffer, this.bytesUsed);
59+
this.bytesUsed += chunk.length;
60+
}
61+
62+
let pos = 0;
63+
const length = this.bytesUsed;
64+
65+
while (pos < length) {
66+
if (this.discardTrailingNewline) {
67+
if (this.buffer[pos] === lineFeed) {
68+
++pos;
69+
}
70+
this.discardTrailingNewline = false;
71+
}
72+
73+
let lineLength = -1;
74+
let fieldLength = this.startingFieldLength;
75+
let c: number;
76+
77+
for (let i = this.startingPos; lineLength < 0 && i < length; ++i) {
78+
c = this.buffer[i];
79+
if (c === colon) {
80+
if (fieldLength < 0) {
81+
fieldLength = i - pos;
82+
}
83+
} else if (c === carriageReturn) {
84+
this.discardTrailingNewline = true;
85+
lineLength = i - pos;
86+
} else if (c === lineFeed) {
87+
lineLength = i - pos;
88+
}
89+
}
90+
91+
if (lineLength < 0) {
92+
this.startingPos = length - pos;
93+
this.startingFieldLength = fieldLength;
94+
break;
95+
} else {
96+
this.startingPos = 0;
97+
this.startingFieldLength = -1;
98+
}
99+
100+
this.lineConsumer(this.buffer, pos, fieldLength, lineLength);
101+
102+
pos += lineLength + 1;
103+
}
104+
105+
if (pos === length) {
106+
this.buffer = void 0;
107+
this.bytesUsed = 0;
108+
} else if (pos > 0) {
109+
this.buffer = this.buffer.subarray(pos, this.bytesUsed);
110+
this.bytesUsed = this.buffer.length;
111+
}
112+
}
113+
}
114+
115+
function hasBom(buffer: Buffer): boolean {
116+
return bom.every((charCode, index) => {
117+
return buffer[index] === charCode;
118+
});
119+
}

0 commit comments

Comments
 (0)