From 8e790bc4584fba3df912741ca790772e21992b3a Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Wed, 29 Jun 2022 11:55:25 -0500 Subject: [PATCH 01/15] WIP: heartbeat --- src/heartbeat.ts | 95 ++++++++++++++++++++++++++++++++++++++++++ src/internalApi.ts | 2 +- src/remoteConnector.ts | 46 ++++++++++++++------ 3 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 src/heartbeat.ts diff --git a/src/heartbeat.ts b/src/heartbeat.ts new file mode 100644 index 00000000..b16d4244 --- /dev/null +++ b/src/heartbeat.ts @@ -0,0 +1,95 @@ +import * as vscode from 'vscode'; +import { Disposable } from './common/dispose'; +import Log from './common/logger'; +import { withServerApi } from './internalApi'; + +export class HeartbeatManager extends Disposable { + + private lastActivity = new Date().getTime(); + private gitpodHost = vscode.workspace.getConfiguration('gitpod').get('host')!; + + constructor(readonly instanceId: string, private readonly accessToken: string, private readonly logger: Log) { + super(); + + this.sendHeartBeat(); + + const activityInterval = 10000; + const heartBeatHandle = setInterval(() => { + if (this.lastActivity + activityInterval < new Date().getTime()) { + // no activity, no heartbeat + return; + } + this.sendHeartBeat(); + }, activityInterval); + + this._register({ dispose: () => clearInterval(heartBeatHandle) }); + 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; + } + })); + } + + private updateLastActivitiy() { + this.lastActivity = new Date().getTime(); + } + + private async sendHeartBeat(wasClosed?: true) { + const suffix = wasClosed ? 'was closed heartbeat' : 'heartbeat'; + if (wasClosed) { + this.logger.trace('sending ' + suffix); + } + + try { + await withServerApi(this.accessToken, this.gitpodHost, service => service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed }), this.logger); + } catch (err) { + this.logger.error(`failed to send ${suffix}:`, err); + } + } + + public override async dispose(): Promise { + await this.sendHeartBeat(true); + super.dispose(); + } +} diff --git a/src/internalApi.ts b/src/internalApi.ts index 9d0ab90e..cf10fe48 100644 --- a/src/internalApi.ts +++ b/src/internalApi.ts @@ -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']; +type UsedGitpodFunction = ['getLoggedInUser', 'getGitpodTokenScopes', 'getWorkspace', 'getOwnerToken', 'sendHeartBeat']; type Union = Tuple[number] | Union; export type GitpodConnection = Omit, 'server'> & { server: Pick>; diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 35c836f9..76ebc7c1 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -22,6 +22,7 @@ import { withServerApi } from './internalApi'; import TelemetryReporter from './telemetryReporter'; import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile'; import { checkDefaultIdentityFiles } from './ssh/identityFiles'; +import { HeartbeatManager } from './heartbeat'; interface SSHConnectionParams { workspaceId: string; @@ -109,6 +110,8 @@ export default class RemoteConnector extends Disposable { private static LOCK_COUNT = 0; private static SSH_DEST_KEY = 'ssh-dest:'; + private heartbeatManager: HeartbeatManager | undefined; + constructor(private readonly context: vscode.ExtensionContext, private readonly logger: Log, private readonly telemetry: TelemetryReporter) { super(); @@ -416,16 +419,10 @@ export default class RemoteConnector extends Disposable { } } - private async getWorkspaceSSHDestination(workspaceId: string, gitpodHost: string): Promise<{ destination: string; password?: string }> { - const session = await vscode.authentication.getSession( - 'gitpod', - ['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'], - { createIfNone: true } - ); - + private async getWorkspaceSSHDestination(accessToken: string, { workspaceId, gitpodHost }: SSHConnectionParams): Promise<{ destination: string; password?: string }> { const serviceUrl = new URL(gitpodHost); - const workspaceInfo = await withServerApi(session.accessToken, serviceUrl.toString(), service => service.server.getWorkspace(workspaceId), this.logger); + const workspaceInfo = await withServerApi(accessToken, serviceUrl.toString(), service => service.server.getWorkspace(workspaceId), this.logger); if (workspaceInfo.latestInstance?.status?.phase !== 'running') { throw new NoRunningInstanceError(workspaceId); } @@ -441,7 +438,7 @@ export default class RemoteConnector extends Disposable { const sshHostKeys: { type: string; host_key: string }[] = await sshHostKeyResponse.json(); - const ownerToken = await withServerApi(session.accessToken, serviceUrl.toString(), service => service.server.getOwnerToken(workspaceId), this.logger); + const ownerToken = await withServerApi(accessToken, serviceUrl.toString(), service => service.server.getOwnerToken(workspaceId), this.logger); let password: string | undefined = ownerToken; const sshDestInfo = { @@ -628,6 +625,14 @@ export default class RemoteConnector extends Disposable { throw new Error('SSH password modal dialog, Canceled'); } + private async getGitpodSession() { + return vscode.authentication.getSession( + 'gitpod', + ['function:sendHeartBeat', 'function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'], + { createIfNone: true } + ); + } + public async handleUri(uri: vscode.Uri) { if (uri.path === RemoteConnector.AUTH_COMPLETE_PATH) { this.logger.info('auth completed'); @@ -639,6 +644,8 @@ export default class RemoteConnector extends Disposable { return; } + const session = await this.getGitpodSession(); + const gitpodHost = vscode.workspace.getConfiguration('gitpod').get('host')!; const forceUseLocalApp = vscode.workspace.getConfiguration('gitpod').get('remote.useLocalApp')!; @@ -662,7 +669,7 @@ export default class RemoteConnector extends Disposable { try { this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { kind: 'gateway', status: 'connecting', ...params }); - const { destination, password } = await this.getWorkspaceSSHDestination(params.workspaceId, params.gitpodHost); + const { destination, password } = await this.getWorkspaceSSHDestination(session.accessToken, params); sshDestination = destination; if (password) { @@ -736,7 +743,7 @@ export default class RemoteConnector extends Disposable { await this.updateRemoteSSHConfig(usingSSHGateway, localAppSSHConfigPath); - await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestination!}`, params); + await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestination!}`, { ...params, isFirstConnection: true }); vscode.commands.executeCommand( 'vscode.openFolder', @@ -798,7 +805,7 @@ export default class RemoteConnector extends Disposable { const sshDest = parseSSHDest(sshDestStr); const connectionSuccessful = await isRemoteExtensionHostRunning(); - const connectionInfo = this.context.globalState.get(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`); + const connectionInfo = this.context.globalState.get(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`); if (connectionInfo) { sshDest; // const usingSSHGateway = typeof sshDest !== 'string'; @@ -821,7 +828,15 @@ export default class RemoteConnector extends Disposable { // gitpodHost: connectionInfo.gitpodHost // }); // } - await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, undefined); + + if (this.heartbeatManager?.instanceId !== connectionInfo.instanceId) { + await this.heartbeatManager?.dispose(); + const session = await this.getGitpodSession(); + this.heartbeatManager = new HeartbeatManager(connectionInfo.instanceId, session.accessToken, this.logger); + this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); + } + + await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); } return connectionSuccessful; @@ -829,4 +844,9 @@ export default class RemoteConnector extends Disposable { return false; } + + public override async dispose(): Promise { + await this.heartbeatManager?.dispose(); + super.dispose(); + } } From 3f4f41d431f1dac975940ef78203e45321497bc9 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Wed, 29 Jun 2022 15:06:09 -0500 Subject: [PATCH 02/15] Fixes --- src/extension.ts | 9 ++++----- src/remoteConnector.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 22c9b956..8f47a2b8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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; @@ -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()); @@ -96,7 +96,6 @@ export async function activate(context: vscode.ExtensionContext) { } export async function deactivate() { - if (telemetry) { - await telemetry.dispose(); - } + await telemetry?.dispose(); + await remoteConnector?.dispose(); } diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 76ebc7c1..3a67a0ca 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -644,8 +644,6 @@ export default class RemoteConnector extends Disposable { return; } - const session = await this.getGitpodSession(); - const gitpodHost = vscode.workspace.getConfiguration('gitpod').get('host')!; const forceUseLocalApp = vscode.workspace.getConfiguration('gitpod').get('remote.useLocalApp')!; @@ -662,6 +660,8 @@ export default class RemoteConnector extends Disposable { this.logger.info(`Updated 'gitpod.host' setting to '${params.gitpodHost}' while trying to connect to a Gitpod workspace`); } + const session = await this.getGitpodSession(); + this.logger.info('Opening Gitpod workspace', uri.toString()); let sshDestination: string | undefined; From de8729631e6e94f87df1b2744ee83ecab75bf72f Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Wed, 29 Jun 2022 16:35:38 -0500 Subject: [PATCH 03/15] Fix --- src/remoteConnector.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 3a67a0ca..c4742849 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -625,7 +625,7 @@ export default class RemoteConnector extends Disposable { throw new Error('SSH password modal dialog, Canceled'); } - private async getGitpodSession() { + private getGitpodSession() { return vscode.authentication.getSession( 'gitpod', ['function:sendHeartBeat', 'function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'], @@ -748,7 +748,7 @@ export default class RemoteConnector extends Disposable { vscode.commands.executeCommand( 'vscode.openFolder', vscode.Uri.parse(`vscode-remote://ssh-remote+${sshDestination}${uri.path || '/'}`), - { forceNewWindow: true } + { forceNewWindow: false } ); } @@ -778,6 +778,15 @@ export default class RemoteConnector extends Disposable { } } + private async startHeartBeat(connectionInfo: SSHConnectionParams) { + if (this.heartbeatManager?.instanceId !== connectionInfo.instanceId) { + await this.heartbeatManager?.dispose(); + const session = await this.getGitpodSession(); + this.heartbeatManager = new HeartbeatManager(connectionInfo.instanceId, session.accessToken, this.logger); + this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); + } + } + public async checkRemoteConnectionSuccessful() { const isRemoteExtensionHostRunning = async () => { try { @@ -829,12 +838,8 @@ export default class RemoteConnector extends Disposable { // }); // } - if (this.heartbeatManager?.instanceId !== connectionInfo.instanceId) { - await this.heartbeatManager?.dispose(); - const session = await this.getGitpodSession(); - this.heartbeatManager = new HeartbeatManager(connectionInfo.instanceId, session.accessToken, this.logger); - this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); - } + // Don't await this on purpose so it doesn't block extension activation + this.startHeartBeat(connectionInfo); await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); } From 02ddc51ba761218fde4df7fa6c4574658e48b1a6 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Wed, 29 Jun 2022 16:44:40 -0500 Subject: [PATCH 04/15] TODO: Clean up this --- src/heartbeat.ts | 6 +++--- src/remoteConnector.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index b16d4244..fa28a8e8 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -77,9 +77,9 @@ export class HeartbeatManager extends Disposable { private async sendHeartBeat(wasClosed?: true) { const suffix = wasClosed ? 'was closed heartbeat' : 'heartbeat'; - if (wasClosed) { - this.logger.trace('sending ' + suffix); - } + // if (wasClosed) { + this.logger.trace('sending ' + suffix); + // } try { await withServerApi(this.accessToken, this.gitpodHost, service => service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed }), this.logger); diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index c4742849..51ceb852 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -748,7 +748,7 @@ export default class RemoteConnector extends Disposable { vscode.commands.executeCommand( 'vscode.openFolder', vscode.Uri.parse(`vscode-remote://ssh-remote+${sshDestination}${uri.path || '/'}`), - { forceNewWindow: false } + { forceNewWindow: false } // REVERT THIS ); } From 84dd91993bafd0f839e4f6b7bcd922ccdd97a4cb Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Fri, 1 Jul 2022 00:34:21 -0500 Subject: [PATCH 05/15] Fix --- src/remoteConnector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 51ceb852..de82c17c 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -819,7 +819,7 @@ export default class RemoteConnector extends Disposable { sshDest; // const usingSSHGateway = typeof sshDest !== 'string'; // const kind = usingSSHGateway ? 'gateway' : 'local-app'; - // if (connectionSuccessful) { + // if (connectionInfo.isFirstConnection && connectionSuccessful) { // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { // kind, // status: 'connected', From 0dd2b15d2339af4d1ad52b20c55662fec2100243 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Fri, 1 Jul 2022 01:01:08 -0500 Subject: [PATCH 06/15] :lipstick: --- src/heartbeat.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index fa28a8e8..d12d65fb 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; From c67351b3a0aacf4a214b07e69060f82d3e21ca6c Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Sat, 2 Jul 2022 12:32:45 -0500 Subject: [PATCH 07/15] Feedback --- src/extension.ts | 6 +- src/heartbeat.ts | 21 +++-- src/remoteConnector.ts | 187 ++++++++++++++++++++++++----------------- 3 files changed, 123 insertions(+), 91 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 8f47a2b8..02f8055f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -85,10 +85,6 @@ 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(FIRST_INSTALL_KEY, false)) { await context.globalState.update(FIRST_INSTALL_KEY, true); telemetry.sendTelemetryEvent('gitpod_desktop_installation', { kind: 'install' }); @@ -96,6 +92,6 @@ export async function activate(context: vscode.ExtensionContext) { } export async function deactivate() { - await telemetry?.dispose(); await remoteConnector?.dispose(); + await telemetry?.dispose(); } diff --git a/src/heartbeat.ts b/src/heartbeat.ts index d12d65fb..6c454dc3 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -7,13 +7,20 @@ 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 { private lastActivity = new Date().getTime(); - private gitpodHost = vscode.workspace.getConfiguration('gitpod').get('host')!; - constructor(readonly instanceId: string, private readonly accessToken: string, private readonly logger: Log) { + constructor( + readonly gitpodHost: string, + readonly workspaceId: string, + readonly instanceId: string, + private readonly accessToken: string, + private readonly logger: Log, + private readonly telemetry: TelemetryReporter + ) { super(); this.sendHeartBeat(); @@ -81,13 +88,13 @@ export class HeartbeatManager extends Disposable { } private async sendHeartBeat(wasClosed?: true) { - const suffix = wasClosed ? 'was closed heartbeat' : 'heartbeat'; - // if (wasClosed) { - this.logger.trace('sending ' + suffix); - // } - + const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat'; try { await withServerApi(this.accessToken, this.gitpodHost, service => service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed }), this.logger); + this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, clientKind: 'vscode' }); + // if (wasClosed) { + this.logger.trace('send ' + suffix); + // } } catch (err) { this.logger.error(`failed to send ${suffix}:`, err); } diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index de82c17c..5f10061e 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -106,15 +106,23 @@ class NoSSHGatewayError extends Error { export default class RemoteConnector extends Disposable { + public static SSH_DEST_KEY = 'ssh-dest:'; public static AUTH_COMPLETE_PATH = '/auth-complete'; private static LOCK_COUNT = 0; - private static SSH_DEST_KEY = 'ssh-dest:'; private heartbeatManager: HeartbeatManager | undefined; constructor(private readonly context: vscode.ExtensionContext, private readonly logger: Log, private readonly telemetry: TelemetryReporter) { super(); + if (isGitpodRemoteWindow(context)) { + context.subscriptions.push(vscode.commands.registerCommand('gitpod.api.autoTunnel', this.autoTunnelCommand, this)); + + // Don't await this on purpose so it doesn't block extension activation. + // Internally requesting a Gitpod Session requires the extension to be already activated. + this.onGitpodRemoteConnection(); + } + this.releaseStaleLocks(); } @@ -625,7 +633,21 @@ export default class RemoteConnector extends Disposable { throw new Error('SSH password modal dialog, Canceled'); } - private getGitpodSession() { + private async getGitpodSession(gitpodHost: string) { + const config = vscode.workspace.getConfiguration('gitpod'); + const currentGitpodHost = config.get('host')!; + if (new URL(gitpodHost).host !== new URL(currentGitpodHost).host) { + const yes = 'Yes'; + const cancel = 'Cancel'; + const action = await vscode.window.showInformationMessage(`Connecting to a Gitpod workspace in '${gitpodHost}'. Would you like to switch from '${currentGitpodHost}' and continue?`, yes, cancel); + if (action === cancel) { + throw new vscode.CancellationError(); + } + + await config.update('host', gitpodHost, vscode.ConfigurationTarget.Global); + this.logger.info(`Updated 'gitpod.host' setting to '${gitpodHost}' while trying to connect to a Gitpod workspace`); + } + return vscode.authentication.getSession( 'gitpod', ['function:sendHeartBeat', 'function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'], @@ -644,26 +666,22 @@ export default class RemoteConnector extends Disposable { return; } - const gitpodHost = vscode.workspace.getConfiguration('gitpod').get('host')!; - const forceUseLocalApp = vscode.workspace.getConfiguration('gitpod').get('remote.useLocalApp')!; - const params: SSHConnectionParams = JSON.parse(uri.query); - if (new URL(params.gitpodHost).host !== new URL(gitpodHost).host) { - const yes = 'Yes'; - const cancel = 'Cancel'; - const action = await vscode.window.showInformationMessage(`Connecting to a Gitpod workspace in '${params.gitpodHost}'. Would you like to switch from '${gitpodHost}' and continue?`, yes, cancel); - if (action === cancel) { + + let session; + try { + session = await this.getGitpodSession(params.gitpodHost); + } catch (e) { + if (e instanceof vscode.CancellationError) { return; + } else { + throw e; } - - await vscode.workspace.getConfiguration('gitpod').update('host', params.gitpodHost, vscode.ConfigurationTarget.Global); - this.logger.info(`Updated 'gitpod.host' setting to '${params.gitpodHost}' while trying to connect to a Gitpod workspace`); } - const session = await this.getGitpodSession(); - this.logger.info('Opening Gitpod workspace', uri.toString()); + const forceUseLocalApp = vscode.workspace.getConfiguration('gitpod').get('remote.useLocalApp')!; let sshDestination: string | undefined; if (!forceUseLocalApp) { try { @@ -745,10 +763,11 @@ export default class RemoteConnector extends Disposable { await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestination!}`, { ...params, isFirstConnection: true }); + const forceNewWindow = this.context.extensionMode === vscode.ExtensionMode.Production; vscode.commands.executeCommand( 'vscode.openFolder', vscode.Uri.parse(`vscode-remote://ssh-remote+${sshDestination}${uri.path || '/'}`), - { forceNewWindow: false } // REVERT THIS + { forceNewWindow } ); } @@ -778,76 +797,54 @@ export default class RemoteConnector extends Disposable { } } - private async startHeartBeat(connectionInfo: SSHConnectionParams) { - if (this.heartbeatManager?.instanceId !== connectionInfo.instanceId) { - await this.heartbeatManager?.dispose(); - const session = await this.getGitpodSession(); - this.heartbeatManager = new HeartbeatManager(connectionInfo.instanceId, session.accessToken, this.logger); - this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); + private startHeartBeat(accessToken: string, connectionInfo: SSHConnectionParams) { + if (this.heartbeatManager) { + return; } - } - - public async checkRemoteConnectionSuccessful() { - const isRemoteExtensionHostRunning = async () => { - try { - // Invoke command from gitpot-remote extension to test if connection is successful - await vscode.commands.executeCommand('__gitpod.getGitpodRemoteLogsUri'); - return true; - } catch { - return false; - } - }; - const parseSSHDest = (sshDestStr: string): { user: string; hostName: string } | string => { - let decoded; - try { - decoded = JSON.parse(Buffer.from(sshDestStr, 'hex').toString('utf8')); - } catch { - // no-op - } - return decoded && typeof decoded.hostName === 'string' ? decoded : sshDestStr; - }; + this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, accessToken, this.logger, this.telemetry); + this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); + } + private async onGitpodRemoteConnection() { const remoteUri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0].uri; - if (vscode.env.remoteName === 'ssh-remote' && this.context.extension.extensionKind === vscode.ExtensionKind.UI && remoteUri) { - const [, sshDestStr] = remoteUri.authority.split('+'); - const sshDest = parseSSHDest(sshDestStr); - - const connectionSuccessful = await isRemoteExtensionHostRunning(); - const connectionInfo = this.context.globalState.get(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`); - if (connectionInfo) { - sshDest; - // const usingSSHGateway = typeof sshDest !== 'string'; - // const kind = usingSSHGateway ? 'gateway' : 'local-app'; - // if (connectionInfo.isFirstConnection && connectionSuccessful) { - // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { - // kind, - // status: 'connected', - // instanceId: connectionInfo.instanceId, - // workspaceId: connectionInfo.workspaceId, - // gitpodHost: connectionInfo.gitpodHost - // }); - // } else { - // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { - // kind, - // status: 'failed', - // reason: 'remote-ssh extension: connection failed', - // instanceId: connectionInfo.instanceId, - // workspaceId: connectionInfo.workspaceId, - // gitpodHost: connectionInfo.gitpodHost - // }); - // } - - // Don't await this on purpose so it doesn't block extension activation - this.startHeartBeat(connectionInfo); - - await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); - } + if (!remoteUri) { + return; + } - return connectionSuccessful; + const [, sshDestStr] = remoteUri.authority.split('+'); + const connectionInfo = this.context.globalState.get(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`); + if (!connectionInfo) { + return; } - return false; + await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); + + const session = await this.getGitpodSession(connectionInfo.gitpodHost); + this.startHeartBeat(session.accessToken, connectionInfo); + + // const sshDest = parseSSHDest(sshDestStr); + // const connectionSuccessful = await isRemoteExtensionHostRunning(); + // const usingSSHGateway = typeof sshDest !== 'string'; + // const kind = usingSSHGateway ? 'gateway' : 'local-app'; + // if (connectionInfo.isFirstConnection && connectionSuccessful) { + // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { + // kind, + // status: 'connected', + // instanceId: connectionInfo.instanceId, + // workspaceId: connectionInfo.workspaceId, + // gitpodHost: connectionInfo.gitpodHost + // }); + // } else { + // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { + // kind, + // status: 'failed', + // reason: 'remote-ssh extension: connection failed', + // instanceId: connectionInfo.instanceId, + // workspaceId: connectionInfo.workspaceId, + // gitpodHost: connectionInfo.gitpodHost + // }); + // } } public override async dispose(): Promise { @@ -855,3 +852,35 @@ export default class RemoteConnector extends Disposable { super.dispose(); } } + +function isGitpodRemoteWindow(context: vscode.ExtensionContext) { + const remoteUri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0].uri; + if (vscode.env.remoteName === 'ssh-remote' && context.extension.extensionKind === vscode.ExtensionKind.UI && remoteUri) { + const [, sshDestStr] = remoteUri.authority.split('+'); + const connectionInfo = context.globalState.get(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`); + + return !!connectionInfo; + } + + return false; +} + +// async function isRemoteExtensionHostRunning() { +// try { +// // Invoke command from gitpot-remote extension to test if connection is successful +// await vscode.commands.executeCommand('__gitpod.getGitpodRemoteLogsUri'); +// return true; +// } catch { +// return false; +// } +// } + +// function parseSSHDest(sshDestStr: string): { user: string; hostName: string } | string { +// let decoded; +// try { +// decoded = JSON.parse(Buffer.from(sshDestStr, 'hex').toString('utf8')); +// } catch { +// // no-op +// } +// return decoded && typeof decoded.hostName === 'string' ? decoded : sshDestStr; +// } From 4bd0a4657eaef53603d12361ae6287d4f783e37b Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Sun, 3 Jul 2022 14:57:11 -0500 Subject: [PATCH 08/15] :lipstick: --- src/remoteConnector.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 5f10061e..a0900550 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -641,7 +641,7 @@ export default class RemoteConnector extends Disposable { const cancel = 'Cancel'; const action = await vscode.window.showInformationMessage(`Connecting to a Gitpod workspace in '${gitpodHost}'. Would you like to switch from '${currentGitpodHost}' and continue?`, yes, cancel); if (action === cancel) { - throw new vscode.CancellationError(); + return; } await config.update('host', gitpodHost, vscode.ConfigurationTarget.Global); @@ -668,15 +668,9 @@ export default class RemoteConnector extends Disposable { const params: SSHConnectionParams = JSON.parse(uri.query); - let session; - try { - session = await this.getGitpodSession(params.gitpodHost); - } catch (e) { - if (e instanceof vscode.CancellationError) { - return; - } else { - throw e; - } + const session = await this.getGitpodSession(params.gitpodHost); + if (!session) { + return; } this.logger.info('Opening Gitpod workspace', uri.toString()); @@ -821,7 +815,9 @@ export default class RemoteConnector extends Disposable { await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); const session = await this.getGitpodSession(connectionInfo.gitpodHost); - this.startHeartBeat(session.accessToken, connectionInfo); + if (session) { + this.startHeartBeat(session.accessToken, connectionInfo); + } // const sshDest = parseSSHDest(sshDestStr); // const connectionSuccessful = await isRemoteExtensionHostRunning(); From 47406308b4b5e3ddba00f0e8dbaae9a8e1e8a13f Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 5 Jul 2022 12:31:30 -0500 Subject: [PATCH 09/15] Check if workspace is running before sending heartbeat --- src/heartbeat.ts | 59 +++++++++++++++++++++++++++++------------- src/remoteConnector.ts | 1 - 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index 6c454dc3..4fd47296 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -11,7 +11,11 @@ import TelemetryReporter from './telemetryReporter'; export class HeartbeatManager extends Disposable { + static HEARTBEAT_INTERVAL = 10000; + private lastActivity = new Date().getTime(); + private isWorkspaceRunning = true; + private heartBeatHandle: NodeJS.Timer | undefined; constructor( readonly gitpodHost: string, @@ -23,18 +27,6 @@ export class HeartbeatManager extends Disposable { ) { super(); - this.sendHeartBeat(); - - const activityInterval = 10000; - const heartBeatHandle = setInterval(() => { - if (this.lastActivity + activityInterval < new Date().getTime()) { - // no activity, no heartbeat - return; - } - this.sendHeartBeat(); - }, activityInterval); - - this._register({ dispose: () => clearInterval(heartBeatHandle) }); this._register(vscode.window.onDidChangeActiveTextEditor(this.updateLastActivitiy, this)); this._register(vscode.window.onDidChangeVisibleTextEditors(this.updateLastActivitiy, this)); this._register(vscode.window.onDidChangeTextEditorSelection(this.updateLastActivitiy, this)); @@ -81,6 +73,19 @@ export class HeartbeatManager extends Disposable { return null; } })); + + this.logger.trace(`Heartbeat manager for workspace ${workspaceId} (${instanceId}) - ${gitpodHost} started`); + + // Start heatbeating interval + this.sendHeartBeat(); + this.heartBeatHandle = setInterval(() => { + if (this.lastActivity + HeartbeatManager.HEARTBEAT_INTERVAL < new Date().getTime()) { + // no activity, no heartbeat + return; + } + + this.sendHeartBeat(); + }, HeartbeatManager.HEARTBEAT_INTERVAL); } private updateLastActivitiy() { @@ -90,18 +95,36 @@ export class HeartbeatManager extends Disposable { private async sendHeartBeat(wasClosed?: true) { const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat'; try { - await withServerApi(this.accessToken, this.gitpodHost, service => service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed }), this.logger); - this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, clientKind: 'vscode' }); - // if (wasClosed) { - this.logger.trace('send ' + suffix); - // } + await withServerApi(this.accessToken, this.gitpodHost, async service => { + const workspaceInfo = await service.server.getWorkspace(this.workspaceId); + this.isWorkspaceRunning = workspaceInfo.latestInstance?.status?.phase === 'running'; + if (this.isWorkspaceRunning) { + await service.server.sendHeartBeat({ instanceId: this.instanceId, wasClosed }); + } else { + this.stopHeartbeat(); + } + }, this.logger); + if (wasClosed) { + this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, clientKind: 'vscode' }); + this.logger.trace('send ' + suffix); + } } 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 { - await this.sendHeartBeat(true); + this.stopHeartbeat(); + if (this.isWorkspaceRunning) { + await this.sendHeartBeat(true); + } super.dispose(); } } diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index a0900550..edaedc29 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -797,7 +797,6 @@ export default class RemoteConnector extends Disposable { } this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, accessToken, this.logger, this.telemetry); - this.logger.trace(`Heartbeat manager for workspace ${connectionInfo.workspaceId} (${connectionInfo.instanceId}) started`); } private async onGitpodRemoteConnection() { From bdc8d2cabf88c5954648bda4ef65dec46d131712 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Wed, 6 Jul 2022 11:15:32 -0500 Subject: [PATCH 10/15] More feedback --- src/heartbeat.ts | 16 +++++++++------- src/remoteConnector.ts | 43 ------------------------------------------ 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index 4fd47296..c04b830d 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -11,7 +11,7 @@ import TelemetryReporter from './telemetryReporter'; export class HeartbeatManager extends Disposable { - static HEARTBEAT_INTERVAL = 10000; + static HEARTBEAT_INTERVAL = 30000; private lastActivity = new Date().getTime(); private isWorkspaceRunning = true; @@ -79,7 +79,9 @@ export class HeartbeatManager extends Disposable { // Start heatbeating interval this.sendHeartBeat(); this.heartBeatHandle = setInterval(() => { - if (this.lastActivity + HeartbeatManager.HEARTBEAT_INTERVAL < new Date().getTime()) { + // 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; } @@ -97,17 +99,17 @@ export class HeartbeatManager extends Disposable { try { await withServerApi(this.accessToken, this.gitpodHost, async service => { const workspaceInfo = await service.server.getWorkspace(this.workspaceId); - this.isWorkspaceRunning = workspaceInfo.latestInstance?.status?.phase === 'running'; + 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); - if (wasClosed) { - this.telemetry.sendTelemetryEvent('ide_close_signal', { workspaceId: this.workspaceId, instanceId: this.instanceId, clientKind: 'vscode' }); - this.logger.trace('send ' + suffix); - } } catch (err) { this.logger.error(`failed to send ${suffix}:`, err); } diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index edaedc29..e6169086 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -817,29 +817,6 @@ export default class RemoteConnector extends Disposable { if (session) { this.startHeartBeat(session.accessToken, connectionInfo); } - - // const sshDest = parseSSHDest(sshDestStr); - // const connectionSuccessful = await isRemoteExtensionHostRunning(); - // const usingSSHGateway = typeof sshDest !== 'string'; - // const kind = usingSSHGateway ? 'gateway' : 'local-app'; - // if (connectionInfo.isFirstConnection && connectionSuccessful) { - // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { - // kind, - // status: 'connected', - // instanceId: connectionInfo.instanceId, - // workspaceId: connectionInfo.workspaceId, - // gitpodHost: connectionInfo.gitpodHost - // }); - // } else { - // this.telemetry.sendTelemetryEvent('vscode_desktop_ssh', { - // kind, - // status: 'failed', - // reason: 'remote-ssh extension: connection failed', - // instanceId: connectionInfo.instanceId, - // workspaceId: connectionInfo.workspaceId, - // gitpodHost: connectionInfo.gitpodHost - // }); - // } } public override async dispose(): Promise { @@ -859,23 +836,3 @@ function isGitpodRemoteWindow(context: vscode.ExtensionContext) { return false; } - -// async function isRemoteExtensionHostRunning() { -// try { -// // Invoke command from gitpot-remote extension to test if connection is successful -// await vscode.commands.executeCommand('__gitpod.getGitpodRemoteLogsUri'); -// return true; -// } catch { -// return false; -// } -// } - -// function parseSSHDest(sshDestStr: string): { user: string; hostName: string } | string { -// let decoded; -// try { -// decoded = JSON.parse(Buffer.from(sshDestStr, 'hex').toString('utf8')); -// } catch { -// // no-op -// } -// return decoded && typeof decoded.hostName === 'string' ? decoded : sshDestStr; -// } From 2165d281beff7b17753b327266071b134479660a Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Sat, 9 Jul 2022 02:09:39 -0500 Subject: [PATCH 11/15] Add check for gitpod-remote version --- package.json | 2 ++ src/remoteConnector.ts | 22 +++++++++++++++++++--- yarn.lock | 10 ++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6bf7ff3e..59eeedee 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "eslint-plugin-jsdoc": "^19.1.0", "eslint-plugin-header": "3.1.1", "minimist": "^1.2.6", + "@types/semver": "^5.5.0", "ts-loader": "^9.2.7", "typescript": "^4.6.3", "webpack": "^5.42.0", @@ -131,6 +132,7 @@ "analytics-node": "^6.0.0", "node-fetch": "2.6.7", "pkce-challenge": "^3.0.0", + "semver": "5.5.1", "ssh2": "^1.10.0", "tmp": "^0.2.1", "uuid": "8.1.0", diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index e6169086..8908c458 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -16,6 +16,7 @@ import { Client as sshClient, utils as sshUtils } from 'ssh2'; import * as tmp from 'tmp'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as semver from 'semver'; import Log from './common/logger'; import { Disposable } from './common/dispose'; import { withServerApi } from './internalApi'; @@ -813,9 +814,13 @@ export default class RemoteConnector extends Disposable { await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); - const session = await this.getGitpodSession(connectionInfo.gitpodHost); - if (session) { - this.startHeartBeat(session.accessToken, connectionInfo); + // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time + const gitpodRemoteVersion = await getGitpodRemoteVersion(); + if (gitpodRemoteVersion && semver.gte(gitpodRemoteVersion, '0.0.40')) { + const session = await this.getGitpodSession(connectionInfo.gitpodHost); + if (session) { + this.startHeartBeat(session.accessToken, connectionInfo); + } } } @@ -836,3 +841,14 @@ function isGitpodRemoteWindow(context: vscode.ExtensionContext) { return false; } + +async function getGitpodRemoteVersion() { + let extVersion: string | undefined; + try { + // Invoke command from gitpot-remote extension + extVersion = await vscode.commands.executeCommand('__gitpod.getGitpodRemoteVersion'); + } catch { + // Ignore if not found + } + return extVersion; +} diff --git a/yarn.lock b/yarn.lock index 8d307ba3..53124f0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -212,6 +212,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/semver@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" + integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== + "@types/ssh2-streams@*": version "0.1.9" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz#8ca51b26f08750a780f82ee75ff18d7160c07a87" @@ -1882,6 +1887,11 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +semver@5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== + semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" From cb87d52dfa422da7f7874c46c05eb68f4e143ac3 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Mon, 11 Jul 2022 10:57:07 -0500 Subject: [PATCH 12/15] :broom: --- src/internalApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internalApi.ts b/src/internalApi.ts index cf10fe48..57b1daf1 100644 --- a/src/internalApi.ts +++ b/src/internalApi.ts @@ -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', 'sendHeartBeat']; +type UsedGitpodFunction = ['getLoggedInUser', 'getWorkspace', 'getOwnerToken', 'sendHeartBeat']; type Union = Tuple[number] | Union; export type GitpodConnection = Omit, 'server'> & { server: Pick>; From ffa578dadeb26134188ee15509b7e84a6154a479 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Jul 2022 01:06:25 -0500 Subject: [PATCH 13/15] Use `__gitpod.cancelGitpodRemoteHeartbeat` command --- package.json | 2 -- src/remoteConnector.ts | 14 +++++++------- yarn.lock | 10 ---------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 59eeedee..6bf7ff3e 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,6 @@ "eslint-plugin-jsdoc": "^19.1.0", "eslint-plugin-header": "3.1.1", "minimist": "^1.2.6", - "@types/semver": "^5.5.0", "ts-loader": "^9.2.7", "typescript": "^4.6.3", "webpack": "^5.42.0", @@ -132,7 +131,6 @@ "analytics-node": "^6.0.0", "node-fetch": "2.6.7", "pkce-challenge": "^3.0.0", - "semver": "5.5.1", "ssh2": "^1.10.0", "tmp": "^0.2.1", "uuid": "8.1.0", diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 8908c458..ba4555eb 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -16,7 +16,6 @@ import { Client as sshClient, utils as sshUtils } from 'ssh2'; import * as tmp from 'tmp'; import * as path from 'path'; import * as vscode from 'vscode'; -import * as semver from 'semver'; import Log from './common/logger'; import { Disposable } from './common/dispose'; import { withServerApi } from './internalApi'; @@ -815,13 +814,14 @@ export default class RemoteConnector extends Disposable { await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time - const gitpodRemoteVersion = await getGitpodRemoteVersion(); - if (gitpodRemoteVersion && semver.gte(gitpodRemoteVersion, '0.0.40')) { + const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat(); + if (isGitpodRemoteHeartbeatCancelled) { const session = await this.getGitpodSession(connectionInfo.gitpodHost); if (session) { this.startHeartBeat(session.accessToken, connectionInfo); } } + this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(!!this.heartbeatManager), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId }); } public override async dispose(): Promise { @@ -842,13 +842,13 @@ function isGitpodRemoteWindow(context: vscode.ExtensionContext) { return false; } -async function getGitpodRemoteVersion() { - let extVersion: string | undefined; +async function cancelGitpodRemoteHeartbeat() { + let result = false; try { // Invoke command from gitpot-remote extension - extVersion = await vscode.commands.executeCommand('__gitpod.getGitpodRemoteVersion'); + result = await vscode.commands.executeCommand('__gitpod.cancelGitpodRemoteHeartbeat'); } catch { // Ignore if not found } - return extVersion; + return result; } diff --git a/yarn.lock b/yarn.lock index 53124f0d..8d307ba3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -212,11 +212,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== -"@types/semver@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" - integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== - "@types/ssh2-streams@*": version "0.1.9" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz#8ca51b26f08750a780f82ee75ff18d7160c07a87" @@ -1887,11 +1882,6 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -semver@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" - integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== - semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" From dfa0aca3dc2162539c3b4ef45173331dd3840a97 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Fri, 15 Jul 2022 00:42:25 -0500 Subject: [PATCH 14/15] Fix --- src/remoteConnector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index 693d8fea..a32b204c 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -666,7 +666,7 @@ export default class RemoteConnector extends Disposable { return vscode.authentication.getSession( 'gitpod', - ['function:sendHeartBeat', 'function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'resource:default'], + ['function:getWorkspace', 'function:getOwnerToken', 'function:getLoggedInUser', 'function:getSSHPublicKeys', 'function:sendHeartBeat', 'resource:default'], { createIfNone: true } ); } From 0cad7fb62ee477673ce51a014790bf4be46de516 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Fri, 15 Jul 2022 13:15:35 -0500 Subject: [PATCH 15/15] Add small delay --- src/remoteConnector.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/remoteConnector.ts b/src/remoteConnector.ts index a32b204c..d6066414 100644 --- a/src/remoteConnector.ts +++ b/src/remoteConnector.ts @@ -829,15 +829,19 @@ export default class RemoteConnector extends Disposable { await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false }); - // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time - const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat(); - if (isGitpodRemoteHeartbeatCancelled) { - const session = await this.getGitpodSession(connectionInfo.gitpodHost); - if (session) { - this.startHeartBeat(session.accessToken, connectionInfo); + // gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote + // let's wait a few seconds for it to finish install + setTimeout(async () => { + // Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time + const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat(); + if (isGitpodRemoteHeartbeatCancelled) { + const session = await this.getGitpodSession(connectionInfo.gitpodHost); + if (session) { + this.startHeartBeat(session.accessToken, connectionInfo); + } } - } - this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(!!this.heartbeatManager), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId }); + this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(!!this.heartbeatManager), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId }); + }, 7000); } public override async dispose(): Promise {