Skip to content

Add Heartbeat #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 15, 2022
13 changes: 4 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const EXTENSION_ID = 'gitpod.gitpod-desktop';
const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';

let telemetry: TelemetryReporter;
let remoteConnector: RemoteConnector;

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

const authProvider = new GitpodAuthenticationProvider(context, logger, telemetry);
const remoteConnector = new RemoteConnector(context, logger, telemetry);
remoteConnector = new RemoteConnector(context, logger, telemetry);
context.subscriptions.push(authProvider);
context.subscriptions.push(remoteConnector);
context.subscriptions.push(vscode.window.registerUriHandler({
handleUri(uri: vscode.Uri) {
// logger.trace('Handling Uri...', uri.toString());
Expand All @@ -85,18 +85,13 @@ export async function activate(context: vscode.ExtensionContext) {
}
}));

if (await remoteConnector.checkRemoteConnectionSuccessful()) {
context.subscriptions.push(vscode.commands.registerCommand('gitpod.api.autoTunnel', remoteConnector.autoTunnelCommand, remoteConnector));
}

if (!context.globalState.get<boolean>(FIRST_INSTALL_KEY, false)) {
await context.globalState.update(FIRST_INSTALL_KEY, true);
telemetry.sendTelemetryEvent('gitpod_desktop_installation', { kind: 'install' });
}
}

export async function deactivate() {
if (telemetry) {
await telemetry.dispose();
}
await remoteConnector?.dispose();
await telemetry?.dispose();
}
132 changes: 132 additions & 0 deletions src/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Gitpod. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Disposable } from './common/dispose';
import Log from './common/logger';
import { withServerApi } from './internalApi';
import TelemetryReporter from './telemetryReporter';

export class HeartbeatManager extends Disposable {

static HEARTBEAT_INTERVAL = 30000;

private lastActivity = new Date().getTime();
private isWorkspaceRunning = true;
private heartBeatHandle: NodeJS.Timer | undefined;

constructor(
readonly gitpodHost: string,
readonly workspaceId: string,
readonly instanceId: string,
private readonly accessToken: string,
private readonly logger: Log,
private readonly telemetry: TelemetryReporter
) {
super();

this._register(vscode.window.onDidChangeActiveTextEditor(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeVisibleTextEditors(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeTextEditorSelection(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeTextEditorVisibleRanges(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeTextEditorOptions(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeTextEditorViewColumn(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeActiveTerminal(this.updateLastActivitiy, this));
this._register(vscode.window.onDidOpenTerminal(this.updateLastActivitiy, this));
this._register(vscode.window.onDidCloseTerminal(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeTerminalState(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeWindowState(this.updateLastActivitiy, this));
this._register(vscode.window.onDidChangeActiveColorTheme(this.updateLastActivitiy, this));
this._register(vscode.authentication.onDidChangeSessions(this.updateLastActivitiy, this));
this._register(vscode.debug.onDidChangeActiveDebugSession(this.updateLastActivitiy, this));
this._register(vscode.debug.onDidStartDebugSession(this.updateLastActivitiy, this));
this._register(vscode.debug.onDidReceiveDebugSessionCustomEvent(this.updateLastActivitiy, this));
this._register(vscode.debug.onDidTerminateDebugSession(this.updateLastActivitiy, this));
this._register(vscode.debug.onDidChangeBreakpoints(this.updateLastActivitiy, this));
this._register(vscode.extensions.onDidChange(this.updateLastActivitiy, this));
this._register(vscode.languages.onDidChangeDiagnostics(this.updateLastActivitiy, this));
this._register(vscode.tasks.onDidStartTask(this.updateLastActivitiy, this));
this._register(vscode.tasks.onDidStartTaskProcess(this.updateLastActivitiy, this));
this._register(vscode.tasks.onDidEndTask(this.updateLastActivitiy, this));
this._register(vscode.tasks.onDidEndTaskProcess(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidChangeWorkspaceFolders(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidOpenTextDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidCloseTextDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidChangeTextDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidSaveTextDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidChangeNotebookDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidSaveNotebookDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidOpenNotebookDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidCloseNotebookDocument(this.updateLastActivitiy, this));
this._register(vscode.workspace.onWillCreateFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidCreateFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onWillDeleteFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidDeleteFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onWillRenameFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidRenameFiles(this.updateLastActivitiy, this));
this._register(vscode.workspace.onDidChangeConfiguration(this.updateLastActivitiy, this));
this._register(vscode.languages.registerHoverProvider('*', {
provideHover: () => {
this.updateLastActivitiy();
return null;
}
}));

this.logger.trace(`Heartbeat manager for workspace ${workspaceId} (${instanceId}) - ${gitpodHost} started`);

// Start heatbeating interval
this.sendHeartBeat();
this.heartBeatHandle = setInterval(() => {
// Add an additional random value between 5 and 15 seconds. See https://github.com/gitpod-io/gitpod/pull/5613
const randomInterval = Math.floor(Math.random() * (15000 - 5000 + 1)) + 5000;
if (this.lastActivity + HeartbeatManager.HEARTBEAT_INTERVAL + randomInterval < new Date().getTime()) {
// no activity, no heartbeat
return;
}

this.sendHeartBeat();
}, HeartbeatManager.HEARTBEAT_INTERVAL);
}

private updateLastActivitiy() {
this.lastActivity = new Date().getTime();
}

private async sendHeartBeat(wasClosed?: true) {
const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat';
try {
await withServerApi(this.accessToken, this.gitpodHost, async service => {
const workspaceInfo = await service.server.getWorkspace(this.workspaceId);
this.isWorkspaceRunning = workspaceInfo.latestInstance?.status?.phase === 'running' && workspaceInfo.latestInstance?.id === this.instanceId;
if (this.isWorkspaceRunning) {
await service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed });
if (wasClosed) {
this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, gitpodHost: this.gitpodHost, clientKind: 'vscode' });
this.logger.trace('send ' + suffix);
}
} else {
this.stopHeartbeat();
}
}, this.logger);
} catch (err) {
this.logger.error(`failed to send ${suffix}:`, err);
}
}

private stopHeartbeat() {
if (this.heartBeatHandle) {
clearInterval(this.heartBeatHandle);
this.heartBeatHandle = undefined;
}
}

public override async dispose(): Promise<void> {
this.stopHeartbeat();
if (this.isWorkspaceRunning) {
await this.sendHeartBeat(true);
}
super.dispose();
}
}
2 changes: 1 addition & 1 deletion src/internalApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ReconnectingWebSocket from 'reconnecting-websocket';
import * as vscode from 'vscode';
import Log from './common/logger';

type UsedGitpodFunction = ['getLoggedInUser', 'getGitpodTokenScopes', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys'];
type UsedGitpodFunction = ['getLoggedInUser', 'getWorkspace', 'getOwnerToken', 'getSSHPublicKeys', 'sendHeartBeat'];
type Union<Tuple extends any[], Union = never> = Tuple[number] | Union;
export type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
server: Pick<GitpodServer, Union<UsedGitpodFunction>>;
Expand Down
Loading