Skip to content

Commit 9b4108d

Browse files
committed
💄
1 parent ae44561 commit 9b4108d

File tree

6 files changed

+104
-63
lines changed

6 files changed

+104
-63
lines changed

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export async function activate(context: vscode.ExtensionContext) {
109109
commandManager.register(new SignInCommand(sessionService));
110110
commandManager.register(new ExportLogsCommand(context.logUri, notificationService, telemetryService, logger, hostService));
111111

112-
ensureDaemonStarted(logger, telemetryService, 3).then(() => { }).catch(e => { logger?.error(e) });
112+
ensureDaemonStarted(logger, telemetryService, 3).then(() => { }).catch(e => { logger?.error(e); });
113113

114114
if (!context.globalState.get<boolean>(FIRST_INSTALL_KEY, false)) {
115115
context.globalState.update(FIRST_INSTALL_KEY, true);

src/heartbeat.ts

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { WorkspaceInfo } from '@gitpod/gitpod-protocol';
7-
import { Workspace, WorkspaceInstanceStatus_Phase } from '@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_pb';
87
import * as vscode from 'vscode';
98
import { Disposable } from './common/dispose';
109
import { withServerApi } from './internalApi';
@@ -13,6 +12,7 @@ import { SSHConnectionParams } from './remote';
1312
import { ISessionService } from './services/sessionService';
1413
import { ILogService } from './services/logService';
1514
import { RawTelemetryEventProperties } from './common/telemetry';
15+
import { WorkspaceState } from './workspaceState';
1616

1717
const IDEHeartbeatTelemetryEvent = 'ide_heartbeat';
1818
interface IDEHeartbeatTelemetryData extends RawTelemetryEventProperties {
@@ -23,7 +23,7 @@ interface IDEHeartbeatTelemetryData extends RawTelemetryEventProperties {
2323
instanceId: string;
2424
gitpodHost: string;
2525
debugWorkspace: 'true' | 'false';
26-
delta?: { [key: string]: number; };
26+
delta?: { [key: string]: number };
2727
}
2828

2929
export class HeartbeatManager extends Disposable {
@@ -39,17 +39,17 @@ export class HeartbeatManager extends Disposable {
3939
private eventCounterMap = new Map<string, number>();
4040

4141
private ideHeartbeatTelemetryHandle: NodeJS.Timer | undefined;
42-
private ideHeartbeatData: Pick<IDEHeartbeatTelemetryData, "successfulCount" | "totalCount"> = {
42+
private ideHeartbeatData: Pick<IDEHeartbeatTelemetryData, 'successfulCount' | 'totalCount'> = {
4343
successfulCount: 0,
4444
totalCount: 0,
45-
}
45+
};
4646

4747
constructor(
4848
private readonly connectionInfo: SSHConnectionParams,
49+
private readonly workspaceState: WorkspaceState | undefined,
4950
private readonly sessionService: ISessionService,
5051
private readonly logService: ILogService,
5152
private readonly telemetryService: ITelemetryService,
52-
private readonly usePublicApi: boolean,
5353
) {
5454
super();
5555
this._register(vscode.window.onDidChangeActiveTextEditor(e => this.updateLastActivity('onDidChangeActiveTextEditor', e?.document)));
@@ -123,6 +123,15 @@ export class HeartbeatManager extends Disposable {
123123
}, HeartbeatManager.HEARTBEAT_INTERVAL);
124124

125125
this.ideHeartbeatTelemetryHandle = setInterval(() => this.sendIDEHeartbeatTelemetry(), HeartbeatManager.IDE_HEARTBEAT_INTERVAL);
126+
127+
if (this.workspaceState) {
128+
this._register(this.workspaceState.onWorkspaceStopped(() => {
129+
this.logService.trace('Stopping heartbeat as workspace is not running');
130+
this.stopHeartbeat();
131+
this.stopIDEHeartbeatTelemetry();
132+
this.sendIDEHeartbeatTelemetry();
133+
}));
134+
}
126135
}
127136

128137
private updateLastActivity(event: string, document?: vscode.TextDocument) {
@@ -136,39 +145,48 @@ export class HeartbeatManager extends Disposable {
136145
}
137146

138147
private async sendHeartBeat(wasClosed?: true) {
139-
let heartbeatSucceed = false
148+
let heartbeatSucceed = false;
140149
try {
141-
await withServerApi(this.sessionService.getGitpodToken(), this.connectionInfo.gitpodHost, async service => {
142-
const workspaceInfo = this.usePublicApi
143-
? await this.sessionService.getAPI().getWorkspace(this.connectionInfo.workspaceId)
144-
: await service.server.getWorkspace(this.connectionInfo.workspaceId);
145-
this.isWorkspaceRunning = this.usePublicApi
146-
? (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.RUNNING && (workspaceInfo as Workspace)?.status?.instance?.instanceId === this.connectionInfo.instanceId
147-
: (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase === 'running' && (workspaceInfo as WorkspaceInfo).latestInstance?.id === this.connectionInfo.instanceId;
148-
if (this.isWorkspaceRunning) {
149-
this.usePublicApi
150-
? (!wasClosed ? await this.sessionService.getAPI().sendHeartbeat(this.connectionInfo.workspaceId) : await this.sessionService.getAPI().sendDidClose(this.connectionInfo.workspaceId))
151-
: await service.server.sendHeartBeat({ instanceId: this.connectionInfo.instanceId, wasClosed });
152-
if (wasClosed) {
150+
if (this.workspaceState) {
151+
if (this.workspaceState.isWorkspaceRunning()) {
152+
if (!wasClosed) {
153+
await this.sessionService.getAPI().sendHeartbeat(this.connectionInfo.workspaceId);
154+
this.logService.trace(`Send heartbeat, triggered by ${this.lastActivityEvent} event`);
155+
} else {
156+
await this.sessionService.getAPI().sendDidClose(this.connectionInfo.workspaceId);
153157
this.telemetryService.sendTelemetryEvent(this.connectionInfo.gitpodHost, 'ide_close_signal', { workspaceId: this.connectionInfo.workspaceId, instanceId: this.connectionInfo.instanceId, gitpodHost: this.connectionInfo.gitpodHost, clientKind: 'vscode', debugWorkspace: String(!!this.connectionInfo.debugWorkspace) });
154158
this.logService.trace(`Send closed heartbeat`);
155-
} else {
156-
this.logService.trace(`Send heartbeat, triggered by ${this.lastActivityEvent} event`);
157159
}
158160
heartbeatSucceed = true;
159-
} else {
160-
this.logService.trace('Stopping heartbeat as workspace is not running');
161-
this.stopHeartbeat();
162-
this.stopIDEHeartbeatTelemetry();
163161
}
164-
}, this.logService);
162+
} else {
163+
await withServerApi(this.sessionService.getGitpodToken(), this.connectionInfo.gitpodHost, async service => {
164+
const workspaceInfo = await service.server.getWorkspace(this.connectionInfo.workspaceId);
165+
this.isWorkspaceRunning = workspaceInfo.latestInstance?.status?.phase === 'running' && (workspaceInfo as WorkspaceInfo).latestInstance?.id === this.connectionInfo.instanceId;
166+
if (this.isWorkspaceRunning) {
167+
await service.server.sendHeartBeat({ instanceId: this.connectionInfo.instanceId, wasClosed });
168+
if (wasClosed) {
169+
this.telemetryService.sendTelemetryEvent(this.connectionInfo.gitpodHost, 'ide_close_signal', { workspaceId: this.connectionInfo.workspaceId, instanceId: this.connectionInfo.instanceId, gitpodHost: this.connectionInfo.gitpodHost, clientKind: 'vscode', debugWorkspace: String(!!this.connectionInfo.debugWorkspace) });
170+
this.logService.trace(`Send closed heartbeat`);
171+
} else {
172+
this.logService.trace(`Send heartbeat, triggered by ${this.lastActivityEvent} event`);
173+
}
174+
heartbeatSucceed = true;
175+
} else {
176+
this.logService.trace('Stopping heartbeat as workspace is not running');
177+
this.stopHeartbeat();
178+
this.stopIDEHeartbeatTelemetry();
179+
this.sendIDEHeartbeatTelemetry();
180+
}
181+
}, this.logService);
182+
}
165183
} catch (e) {
166184
const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat';
167185
const originMsg = e.message;
168186
e.message = `Failed to send ${suffix}, triggered by event: ${this.lastActivityEvent}: ${originMsg}`;
169187
this.logService.error(e);
170188
e.message = `Failed to send ${suffix}: ${originMsg}`;
171-
this.telemetryService.sendTelemetryException(this.connectionInfo.gitpodHost, e, { workspaceId: this.connectionInfo.workspaceId, instanceId: this.connectionInfo.instanceId});
189+
this.telemetryService.sendTelemetryException(this.connectionInfo.gitpodHost, e, { workspaceId: this.connectionInfo.workspaceId, instanceId: this.connectionInfo.instanceId });
172190
} finally {
173191
if (heartbeatSucceed) {
174192
this.ideHeartbeatData.successfulCount++;
@@ -184,6 +202,13 @@ export class HeartbeatManager extends Disposable {
184202
}
185203
}
186204

205+
private stopIDEHeartbeatTelemetry() {
206+
if (this.ideHeartbeatTelemetryHandle) {
207+
clearInterval(this.ideHeartbeatTelemetryHandle);
208+
this.ideHeartbeatTelemetryHandle = undefined;
209+
}
210+
}
211+
187212
private sendIDEHeartbeatTelemetry() {
188213
this.telemetryService.sendRawTelemetryEvent(this.connectionInfo.gitpodHost, IDEHeartbeatTelemetryEvent, {
189214
...this.ideHeartbeatData,
@@ -200,19 +225,12 @@ export class HeartbeatManager extends Disposable {
200225
this.ideHeartbeatData.totalCount = 0;
201226
}
202227

203-
private stopIDEHeartbeatTelemetry() {
204-
if (this.ideHeartbeatTelemetryHandle) {
205-
clearInterval(this.ideHeartbeatTelemetryHandle);
206-
this.ideHeartbeatTelemetryHandle = undefined;
207-
this.sendIDEHeartbeatTelemetry()
208-
}
209-
}
210-
211228
public override async dispose(): Promise<void> {
212229
super.dispose();
213230
this.stopIDEHeartbeatTelemetry();
214231
this.stopHeartbeat();
215-
if (this.isWorkspaceRunning) {
232+
if (this.workspaceState?.isWorkspaceRunning() ?? this.isWorkspaceRunning) {
233+
this.sendIDEHeartbeatTelemetry();
216234
await this.sendHeartBeat(true);
217235
}
218236
}

src/remote.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function checkForStoppedWorkspaces(context: vscode.ExtensionContext
7272
for (const k of stopped_ws_keys) {
7373
const ws = context.globalState.get<WorkspaceRestartInfo>(k)!;
7474
context.globalState.update(k, undefined);
75-
if (flow.gitpodHost === ws.gitpodHost) {
75+
if (new URL(flow.gitpodHost).host === new URL(ws.gitpodHost).host) {
7676
showWsNotRunningDialog(ws.workspaceId, ws.gitpodHost, { ...flow, workspaceId: ws.workspaceId, gitpodHost: ws.gitpodHost }, notificationService, logService);
7777
}
7878
}

src/remoteSession.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,17 @@ export class RemoteSession extends Disposable {
7878

7979
if (this.usePublicApi) {
8080
this.workspaceState = new WorkspaceState(this.connectionInfo.workspaceId, this.sessionService, this.logService);
81+
await this.workspaceState.initialize();
8182

82-
let handled = false;
83-
this._register(this.workspaceState.onWorkspaceStatusChanged(async () => {
84-
if (!this.workspaceState!.isWorkspaceRunning() && !handled) {
85-
handled = true;
86-
await this.context.globalState.update(`${WORKSPACE_STOPPED_PREFIX}${this.connectionInfo.workspaceId}`, { workspaceId: this.connectionInfo.workspaceId, gitpodHost: this.connectionInfo.gitpodHost } as WorkspaceRestartInfo);
87-
vscode.commands.executeCommand('workbench.action.remote.close');
88-
}
83+
this._register(this.workspaceState.onWorkspaceStopped(async () => {
84+
await this.context.globalState.update(`${WORKSPACE_STOPPED_PREFIX}${this.connectionInfo.workspaceId}`, { workspaceId: this.connectionInfo.workspaceId, gitpodHost: this.connectionInfo.gitpodHost } as WorkspaceRestartInfo);
85+
vscode.commands.executeCommand('workbench.action.remote.close');
8986
}));
9087
}
9188

9289
const heartbeatSupported = this.sessionService.getScopes().includes(ScopeFeature.LocalHeartbeat);
9390
if (heartbeatSupported) {
94-
this.heartbeatManager = new HeartbeatManager(this.connectionInfo, this.sessionService, this.logService, this.telemetryService, this.usePublicApi);
91+
this.heartbeatManager = new HeartbeatManager(this.connectionInfo, this.workspaceState, this.sessionService, this.logService, this.telemetryService);
9592
} else {
9693
this.logService.error(`Local heartbeat not supported in ${this.connectionInfo.gitpodHost}, using version ${gitpodVersion.raw}`);
9794
}

src/services/telemetryService.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ const analyticsClientFactory = async (gitpodHost: string, segmentKey: string, lo
1414
const settings: AnalyticsSettings = {
1515
writeKey: segmentKey,
1616
// in dev mode we report directly to IDE playground source
17-
host: "https://api.segment.io",
18-
path: "/v1/batch"
19-
}
20-
if (segmentKey === "untrusted-dummy-key") {
17+
host: 'https://api.segment.io',
18+
path: '/v1/batch'
19+
};
20+
if (segmentKey === 'untrusted-dummy-key') {
2121
settings.host = gitpodHost;
2222
settings.path = '/analytics' + settings.path;
2323
} else {
24-
if (serviceUrl.host !== "gitpod.io" && !serviceUrl.host.endsWith(".gitpod-dev.com")) {
24+
if (serviceUrl.host !== 'gitpod.io' && !serviceUrl.host.endsWith('.gitpod-dev.com')) {
2525
logger.warn(`No telemetry: dedicated installations should send data always to own endpoints, host: ${serviceUrl.host}`);
2626
return {
2727
logEvent: () => { },
2828
logException: () => { },
2929
flush: () => { },
30-
}
30+
};
3131
}
3232
}
33-
logger.debug("analytics: " + new URL(settings.path!, settings.host).href.replace(/\/$/, '')); // aligned with how segment does it internally
33+
logger.debug('analytics: ' + new URL(settings.path!, settings.host).href.replace(/\/$/, '')); // aligned with how segment does it internally
3434

3535
const errorMetricsEndpoint = `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
3636

src/workspaceState.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { WorkspaceStatus, WorkspaceInstanceStatus_Phase } from './lib/gitpod/experimental/v1/workspaces.pb';
6+
import { WorkspaceStatus as WorkspaceStatus2, workspaceInstanceStatus_PhaseToJSON, admissionLevelToJSON } from './lib/gitpod/experimental/v1/workspaces.pb';
7+
import { WorkspaceStatus, WorkspaceInstanceStatus_Phase } from '@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_pb';
78
import * as vscode from 'vscode';
89
import { Disposable } from './common/dispose';
910
import { ISessionService } from './services/sessionService';
1011
import { ILogService } from './services/logService';
12+
import { filterEvent } from './common/utils';
1113

1214
export class WorkspaceState extends Disposable {
1315
private workspaceState: WorkspaceStatus | undefined;
1416

15-
private _onWorkspaceStatusChanged = this._register(new vscode.EventEmitter<void>());
16-
readonly onWorkspaceStatusChanged = this._onWorkspaceStatusChanged.event;
17+
private _onWorkspaceStateChanged = this._register(new vscode.EventEmitter<void>());
18+
readonly onWorkspaceStateChanged = this._onWorkspaceStateChanged.event;
19+
20+
readonly onWorkspaceStopped = filterEvent(this.onWorkspaceStateChanged, () => this.workspaceState?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPING); // assuming stopping state is never skipped
1721

1822
constructor(
1923
readonly workspaceId: string,
@@ -25,34 +29,56 @@ export class WorkspaceState extends Disposable {
2529
this.logger.trace(`WorkspaceState manager for workspace ${workspaceId} started`);
2630

2731
const { onStatusChanged, dispose } = this.sessionService.getAPI().workspaceStatusStreaming(workspaceId);
28-
this._register(onStatusChanged(u => this.checkWorkspaceState(u)));
32+
this._register(onStatusChanged(u => this.checkWorkspaceState(this.toWorkspaceStatus(u))));
2933
this._register({ dispose });
3034
}
3135

36+
public async initialize() {
37+
const ws = await this.sessionService.getAPI().getWorkspace(this.workspaceId);
38+
if (!this.workspaceState) {
39+
this.workspaceState = ws?.status;
40+
}
41+
}
42+
3243
public isWorkspaceStopped() {
3344
const phase = this.workspaceState?.instance?.status?.phase;
34-
return phase === WorkspaceInstanceStatus_Phase.PHASE_STOPPED || phase === WorkspaceInstanceStatus_Phase.PHASE_STOPPING;
45+
return phase === WorkspaceInstanceStatus_Phase.STOPPED || phase === WorkspaceInstanceStatus_Phase.STOPPING;
3546
}
3647

3748
public isWorkspaceRunning() {
3849
const phase = this.workspaceState?.instance?.status?.phase;
39-
return phase === WorkspaceInstanceStatus_Phase.PHASE_RUNNING;
50+
return phase === WorkspaceInstanceStatus_Phase.RUNNING;
4051
}
4152

4253
public workspaceUrl() {
4354
return this.workspaceState?.instance?.status?.url;
4455
}
4556

46-
public getInstanceId() {
47-
return this.workspaceState?.instance?.instanceId;
48-
}
49-
5057
private async checkWorkspaceState(workspaceState: WorkspaceStatus | undefined) {
5158
const phase = workspaceState?.instance?.status?.phase;
5259
const oldPhase = this.workspaceState?.instance?.status?.phase;
5360
this.workspaceState = workspaceState;
5461
if (phase && oldPhase && phase !== oldPhase) {
55-
this._onWorkspaceStatusChanged.fire();
62+
this._onWorkspaceStateChanged.fire();
5663
}
5764
}
65+
66+
private toWorkspaceStatus(workspaceState: WorkspaceStatus2): WorkspaceStatus {
67+
return WorkspaceStatus.fromJson({
68+
instance: {
69+
instanceId: workspaceState.instance!.instanceId,
70+
workspaceId: workspaceState.instance!.workspaceId,
71+
createdAt: workspaceState.instance!.createdAt?.toISOString() ?? null,
72+
status: {
73+
statusVersion: workspaceState.instance!.status!.statusVersion.toString(),
74+
phase: workspaceInstanceStatus_PhaseToJSON(workspaceState.instance!.status!.phase),
75+
conditions: null,
76+
message: '',
77+
url: workspaceState.instance!.status!.url,
78+
admission: admissionLevelToJSON(workspaceState.instance!.status!.admission),
79+
ports: []
80+
}
81+
}
82+
});
83+
}
5884
}

0 commit comments

Comments
 (0)