Skip to content

Commit 39353b7

Browse files
authored
Add Heartbeat (#4)
1 parent 01a663a commit 39353b7

File tree

4 files changed

+238
-86
lines changed

4 files changed

+238
-86
lines changed

src/extension.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const EXTENSION_ID = 'gitpod.gitpod-desktop';
1717
const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';
1818

1919
let telemetry: TelemetryReporter;
20+
let remoteConnector: RemoteConnector;
2021

2122
export async function activate(context: vscode.ExtensionContext) {
2223
const packageJSON = vscode.extensions.getExtension(EXTENSION_ID)!.packageJSON;
@@ -71,9 +72,8 @@ export async function activate(context: vscode.ExtensionContext) {
7172
}));
7273

7374
const authProvider = new GitpodAuthenticationProvider(context, logger, telemetry);
74-
const remoteConnector = new RemoteConnector(context, logger, telemetry);
75+
remoteConnector = new RemoteConnector(context, logger, telemetry);
7576
context.subscriptions.push(authProvider);
76-
context.subscriptions.push(remoteConnector);
7777
context.subscriptions.push(vscode.window.registerUriHandler({
7878
handleUri(uri: vscode.Uri) {
7979
// logger.trace('Handling Uri...', uri.toString());
@@ -85,18 +85,13 @@ export async function activate(context: vscode.ExtensionContext) {
8585
}
8686
}));
8787

88-
if (await remoteConnector.checkRemoteConnectionSuccessful()) {
89-
context.subscriptions.push(vscode.commands.registerCommand('gitpod.api.autoTunnel', remoteConnector.autoTunnelCommand, remoteConnector));
90-
}
91-
9288
if (!context.globalState.get<boolean>(FIRST_INSTALL_KEY, false)) {
9389
await context.globalState.update(FIRST_INSTALL_KEY, true);
9490
telemetry.sendTelemetryEvent('gitpod_desktop_installation', { kind: 'install' });
9591
}
9692
}
9793

9894
export async function deactivate() {
99-
if (telemetry) {
100-
await telemetry.dispose();
101-
}
95+
await remoteConnector?.dispose();
96+
await telemetry?.dispose();
10297
}

src/heartbeat.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { Disposable } from './common/dispose';
8+
import Log from './common/logger';
9+
import { withServerApi } from './internalApi';
10+
import TelemetryReporter from './telemetryReporter';
11+
12+
export class HeartbeatManager extends Disposable {
13+
14+
static HEARTBEAT_INTERVAL = 30000;
15+
16+
private lastActivity = new Date().getTime();
17+
private isWorkspaceRunning = true;
18+
private heartBeatHandle: NodeJS.Timer | undefined;
19+
20+
constructor(
21+
readonly gitpodHost: string,
22+
readonly workspaceId: string,
23+
readonly instanceId: string,
24+
private readonly accessToken: string,
25+
private readonly logger: Log,
26+
private readonly telemetry: TelemetryReporter
27+
) {
28+
super();
29+
30+
this._register(vscode.window.onDidChangeActiveTextEditor(this.updateLastActivitiy, this));
31+
this._register(vscode.window.onDidChangeVisibleTextEditors(this.updateLastActivitiy, this));
32+
this._register(vscode.window.onDidChangeTextEditorSelection(this.updateLastActivitiy, this));
33+
this._register(vscode.window.onDidChangeTextEditorVisibleRanges(this.updateLastActivitiy, this));
34+
this._register(vscode.window.onDidChangeTextEditorOptions(this.updateLastActivitiy, this));
35+
this._register(vscode.window.onDidChangeTextEditorViewColumn(this.updateLastActivitiy, this));
36+
this._register(vscode.window.onDidChangeActiveTerminal(this.updateLastActivitiy, this));
37+
this._register(vscode.window.onDidOpenTerminal(this.updateLastActivitiy, this));
38+
this._register(vscode.window.onDidCloseTerminal(this.updateLastActivitiy, this));
39+
this._register(vscode.window.onDidChangeTerminalState(this.updateLastActivitiy, this));
40+
this._register(vscode.window.onDidChangeWindowState(this.updateLastActivitiy, this));
41+
this._register(vscode.window.onDidChangeActiveColorTheme(this.updateLastActivitiy, this));
42+
this._register(vscode.authentication.onDidChangeSessions(this.updateLastActivitiy, this));
43+
this._register(vscode.debug.onDidChangeActiveDebugSession(this.updateLastActivitiy, this));
44+
this._register(vscode.debug.onDidStartDebugSession(this.updateLastActivitiy, this));
45+
this._register(vscode.debug.onDidReceiveDebugSessionCustomEvent(this.updateLastActivitiy, this));
46+
this._register(vscode.debug.onDidTerminateDebugSession(this.updateLastActivitiy, this));
47+
this._register(vscode.debug.onDidChangeBreakpoints(this.updateLastActivitiy, this));
48+
this._register(vscode.extensions.onDidChange(this.updateLastActivitiy, this));
49+
this._register(vscode.languages.onDidChangeDiagnostics(this.updateLastActivitiy, this));
50+
this._register(vscode.tasks.onDidStartTask(this.updateLastActivitiy, this));
51+
this._register(vscode.tasks.onDidStartTaskProcess(this.updateLastActivitiy, this));
52+
this._register(vscode.tasks.onDidEndTask(this.updateLastActivitiy, this));
53+
this._register(vscode.tasks.onDidEndTaskProcess(this.updateLastActivitiy, this));
54+
this._register(vscode.workspace.onDidChangeWorkspaceFolders(this.updateLastActivitiy, this));
55+
this._register(vscode.workspace.onDidOpenTextDocument(this.updateLastActivitiy, this));
56+
this._register(vscode.workspace.onDidCloseTextDocument(this.updateLastActivitiy, this));
57+
this._register(vscode.workspace.onDidChangeTextDocument(this.updateLastActivitiy, this));
58+
this._register(vscode.workspace.onDidSaveTextDocument(this.updateLastActivitiy, this));
59+
this._register(vscode.workspace.onDidChangeNotebookDocument(this.updateLastActivitiy, this));
60+
this._register(vscode.workspace.onDidSaveNotebookDocument(this.updateLastActivitiy, this));
61+
this._register(vscode.workspace.onDidOpenNotebookDocument(this.updateLastActivitiy, this));
62+
this._register(vscode.workspace.onDidCloseNotebookDocument(this.updateLastActivitiy, this));
63+
this._register(vscode.workspace.onWillCreateFiles(this.updateLastActivitiy, this));
64+
this._register(vscode.workspace.onDidCreateFiles(this.updateLastActivitiy, this));
65+
this._register(vscode.workspace.onWillDeleteFiles(this.updateLastActivitiy, this));
66+
this._register(vscode.workspace.onDidDeleteFiles(this.updateLastActivitiy, this));
67+
this._register(vscode.workspace.onWillRenameFiles(this.updateLastActivitiy, this));
68+
this._register(vscode.workspace.onDidRenameFiles(this.updateLastActivitiy, this));
69+
this._register(vscode.workspace.onDidChangeConfiguration(this.updateLastActivitiy, this));
70+
this._register(vscode.languages.registerHoverProvider('*', {
71+
provideHover: () => {
72+
this.updateLastActivitiy();
73+
return null;
74+
}
75+
}));
76+
77+
this.logger.trace(`Heartbeat manager for workspace ${workspaceId} (${instanceId}) - ${gitpodHost} started`);
78+
79+
// Start heatbeating interval
80+
this.sendHeartBeat();
81+
this.heartBeatHandle = setInterval(() => {
82+
// Add an additional random value between 5 and 15 seconds. See https://github.com/gitpod-io/gitpod/pull/5613
83+
const randomInterval = Math.floor(Math.random() * (15000 - 5000 + 1)) + 5000;
84+
if (this.lastActivity + HeartbeatManager.HEARTBEAT_INTERVAL + randomInterval < new Date().getTime()) {
85+
// no activity, no heartbeat
86+
return;
87+
}
88+
89+
this.sendHeartBeat();
90+
}, HeartbeatManager.HEARTBEAT_INTERVAL);
91+
}
92+
93+
private updateLastActivitiy() {
94+
this.lastActivity = new Date().getTime();
95+
}
96+
97+
private async sendHeartBeat(wasClosed?: true) {
98+
const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat';
99+
try {
100+
await withServerApi(this.accessToken, this.gitpodHost, async service => {
101+
const workspaceInfo = await service.server.getWorkspace(this.workspaceId);
102+
this.isWorkspaceRunning = workspaceInfo.latestInstance?.status?.phase === 'running' && workspaceInfo.latestInstance?.id === this.instanceId;
103+
if (this.isWorkspaceRunning) {
104+
await service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed });
105+
if (wasClosed) {
106+
this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, gitpodHost: this.gitpodHost, clientKind: 'vscode' });
107+
this.logger.trace('send ' + suffix);
108+
}
109+
} else {
110+
this.stopHeartbeat();
111+
}
112+
}, this.logger);
113+
} catch (err) {
114+
this.logger.error(`failed to send ${suffix}:`, err);
115+
}
116+
}
117+
118+
private stopHeartbeat() {
119+
if (this.heartBeatHandle) {
120+
clearInterval(this.heartBeatHandle);
121+
this.heartBeatHandle = undefined;
122+
}
123+
}
124+
125+
public override async dispose(): Promise<void> {
126+
this.stopHeartbeat();
127+
if (this.isWorkspaceRunning) {
128+
await this.sendHeartBeat(true);
129+
}
130+
super.dispose();
131+
}
132+
}

src/internalApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ReconnectingWebSocket from 'reconnecting-websocket';
1111
import * as vscode from 'vscode';
1212
import Log from './common/logger';
1313

14-
type UsedGitpodFunction = ['getLoggedInUser', 'getGitpodTokenScopes', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys'];
14+
type UsedGitpodFunction = ['getLoggedInUser', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys', 'sendHeartBeat'];
1515
type Union<Tuple extends any[], Union = never> = Tuple[number] | Union;
1616
export type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
1717
server: Pick<GitpodServer, Union<UsedGitpodFunction>>;

0 commit comments

Comments
 (0)