Skip to content

Commit d0aec88

Browse files
authored
Initialize remote extensions (#16)
1 parent ee077dc commit d0aec88

File tree

4 files changed

+270
-49
lines changed

4 files changed

+270
-49
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
"description": "Use the local companion app to connect to a remote workspace.\nWarning: Connecting to a remote workspace using local companion app will be removed in the near future.",
6464
"default": false,
6565
"scope": "application"
66+
},
67+
"gitpod.remote.syncExtensions": {
68+
"type": "boolean",
69+
"description": "Automatically install sync extensions from Gitpod Sync Server on the remote machine.",
70+
"default": true,
71+
"scope": "application"
6672
}
6773
}
6874
},

src/extension.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
88
import Log from './common/logger';
99
import GitpodAuthenticationProvider from './authentication';
1010
import RemoteConnector from './remoteConnector';
11-
import SettingsSync from './settingsSync';
11+
import { SettingsSync } from './settingsSync';
1212
import GitpodServer from './gitpodServer';
1313
import TelemetryReporter from './telemetryReporter';
1414
import { exportLogs } from './exportLogs';
@@ -42,10 +42,11 @@ export async function activate(context: vscode.ExtensionContext) {
4242
}
4343
}));
4444

45-
context.subscriptions.push(new SettingsSync(logger, telemetry));
45+
const settingsSync = new SettingsSync(logger, telemetry);
46+
context.subscriptions.push(settingsSync);
4647

4748
const authProvider = new GitpodAuthenticationProvider(context, logger, telemetry);
48-
remoteConnector = new RemoteConnector(context, experiments, logger, telemetry);
49+
remoteConnector = new RemoteConnector(context, settingsSync, experiments, logger, telemetry);
4950
context.subscriptions.push(authProvider);
5051
context.subscriptions.push(vscode.window.registerUriHandler({
5152
handleUri(uri: vscode.Uri) {

src/remoteConnector.ts

+95-31
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import TelemetryReporter from './telemetryReporter';
2525
import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
2626
import { DEFAULT_IDENTITY_FILES } from './ssh/identityFiles';
2727
import { HeartbeatManager } from './heartbeat';
28-
import { getGitpodVersion, isFeatureSupported } from './featureSupport';
28+
import { getGitpodVersion, GitpodVersion, isFeatureSupported } from './featureSupport';
2929
import SSHConfiguration from './ssh/sshConfig';
3030
import { isWindows } from './common/platform';
3131
import { untildify } from './common/files';
3232
import { ExperimentalSettings, isUserOverrideSetting } from './experiments';
33+
import { ISyncExtension, NoSettingsSyncSession, NoSyncStoreError, parseSyncData, SettingsSync, SyncResource } from './settingsSync';
34+
import { retry } from './common/async';
3335

3436
interface SSHConnectionParams {
3537
workspaceId: string;
@@ -121,6 +123,7 @@ export default class RemoteConnector extends Disposable {
121123

122124
constructor(
123125
private readonly context: vscode.ExtensionContext,
126+
private readonly settingsSync: SettingsSync,
124127
private readonly experiments: ExperimentalSettings,
125128
private readonly logger: Log,
126129
private readonly telemetry: TelemetryReporter
@@ -900,12 +903,96 @@ export default class RemoteConnector extends Disposable {
900903
}
901904
}
902905

903-
private startHeartBeat(accessToken: string, connectionInfo: SSHConnectionParams) {
906+
private async startHeartBeat(accessToken: string, connectionInfo: SSHConnectionParams, gitpodVersion: GitpodVersion) {
904907
if (this.heartbeatManager) {
905908
return;
906909
}
907910

908911
this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, accessToken, this.logger, this.telemetry);
912+
913+
// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
914+
// let's try a few times for it to finish install
915+
try {
916+
await retry(async () => {
917+
await vscode.commands.executeCommand('__gitpod.cancelGitpodRemoteHeartbeat');
918+
}, 3000, 15);
919+
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(true), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
920+
} catch {
921+
this.logger.error(`Could not execute '__gitpod.cancelGitpodRemoteHeartbeat' command`);
922+
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(false), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
923+
}
924+
}
925+
926+
private async initializeRemoteExtensions() {
927+
let syncData: { ref: string; content: string } | undefined;
928+
try {
929+
syncData = await this.settingsSync.readResource(SyncResource.Extensions);
930+
} catch (e) {
931+
if (e instanceof NoSyncStoreError) {
932+
const action = 'Settings Sync: Enable Sign In with Gitpod';
933+
const result = await vscode.window.showInformationMessage(`Couldn't initialize remote extensions, Settings Sync with Gitpod is required.`, action);
934+
if (result === action) {
935+
vscode.commands.executeCommand('gitpod.syncProvider.add');
936+
}
937+
} else if (e instanceof NoSettingsSyncSession) {
938+
const action = 'Enable Settings Sync';
939+
const result = await vscode.window.showInformationMessage(`Couldn't initialize remote extensions, please enable Settings Sync.`, action);
940+
if (result === action) {
941+
vscode.commands.executeCommand('workbench.userDataSync.actions.turnOn');
942+
}
943+
} else {
944+
this.logger.error('Error while fetching settings sync extension data:', e);
945+
946+
const seeLogs = 'See Logs';
947+
const action = await vscode.window.showErrorMessage(`Error while fetching settings sync extension data.`, seeLogs);
948+
if (action === seeLogs) {
949+
this.logger.show();
950+
}
951+
}
952+
return;
953+
}
954+
955+
const syncDataContent = parseSyncData(syncData.content);
956+
if (!syncDataContent) {
957+
this.logger.error('Error while parsing sync data');
958+
return;
959+
}
960+
961+
let extensions: ISyncExtension[];
962+
try {
963+
extensions = JSON.parse(syncDataContent.content);
964+
} catch {
965+
this.logger.error('Error while parsing settings sync extension data, malformed json');
966+
return;
967+
}
968+
969+
extensions = extensions.filter(e => e.installed);
970+
if (!extensions.length) {
971+
return;
972+
}
973+
974+
try {
975+
await vscode.window.withProgress<void>({
976+
title: 'Installing extensions on remote',
977+
location: vscode.ProgressLocation.Notification
978+
}, async () => {
979+
try {
980+
this.logger.trace(`Installing extensions on remote: `, extensions.map(e => e.identifier.id).join('\n'));
981+
await retry(async () => {
982+
await vscode.commands.executeCommand('__gitpod.initializeRemoteExtensions', extensions);
983+
}, 3000, 15);
984+
} catch (e) {
985+
this.logger.error(`Could not execute '__gitpod.initializeRemoteExtensions' command`);
986+
throw e;
987+
}
988+
});
989+
} catch {
990+
const seeLogs = 'See Logs';
991+
const action = await vscode.window.showErrorMessage(`Error while installing extensions on remote.`, seeLogs);
992+
if (action === seeLogs) {
993+
this.logger.show();
994+
}
995+
}
909996
}
910997

911998
private async onGitpodRemoteConnection() {
@@ -939,27 +1026,15 @@ export default class RemoteConnector extends Disposable {
9391026

9401027
const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost, this.logger);
9411028
if (isFeatureSupported(gitpodVersion, 'localHeartbeat')) {
942-
// gitpod remote extension installation is async so sometimes gitpod-desktop will activate before gitpod-remote
943-
// let's try a few times for it to finish install
944-
let retryCount = 15;
945-
const tryStopRemoteHeartbeat = async () => {
946-
// Check for gitpod remote extension version to avoid sending heartbeat in both extensions at the same time
947-
const isGitpodRemoteHeartbeatCancelled = await cancelGitpodRemoteHeartbeat();
948-
if (isGitpodRemoteHeartbeatCancelled) {
949-
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(true), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
950-
} else if (retryCount > 0) {
951-
retryCount--;
952-
setTimeout(tryStopRemoteHeartbeat, 3000);
953-
} else {
954-
this.telemetry.sendTelemetryEvent('vscode_desktop_heartbeat_state', { enabled: String(false), gitpodHost: connectionInfo.gitpodHost, workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, gitpodVersion: gitpodVersion.raw });
955-
}
956-
};
957-
958-
this.startHeartBeat(session.accessToken, connectionInfo);
959-
tryStopRemoteHeartbeat();
1029+
this.startHeartBeat(session.accessToken, connectionInfo, gitpodVersion);
9601030
} else {
9611031
this.logger.warn(`Local heatbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.version}`);
9621032
}
1033+
1034+
const syncExtensions = vscode.workspace.getConfiguration('gitpod').get<boolean>('remote.syncExtensions')!;
1035+
if (syncExtensions) {
1036+
this.initializeRemoteExtensions();
1037+
}
9631038
}
9641039

9651040
public override async dispose(): Promise<void> {
@@ -980,17 +1055,6 @@ function isGitpodRemoteWindow(context: vscode.ExtensionContext) {
9801055
return false;
9811056
}
9821057

983-
async function cancelGitpodRemoteHeartbeat() {
984-
let result = false;
985-
try {
986-
// Invoke command from gitpot-remote extension
987-
result = await vscode.commands.executeCommand('__gitpod.cancelGitpodRemoteHeartbeat');
988-
} catch {
989-
// Ignore if not found
990-
}
991-
return result;
992-
}
993-
9941058
function getServiceURL(gitpodHost: string): string {
9951059
return new URL(gitpodHost).toString().replace(/\/$/, '');
9961060
}

0 commit comments

Comments
 (0)