Skip to content

Commit 03e6172

Browse files
committed
Workaorund sync local extensions to remote
1 parent 7623214 commit 03e6172

File tree

4 files changed

+91
-124
lines changed

4 files changed

+91
-124
lines changed

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export async function activate(context: vscode.ExtensionContext) {
131131
if (remoteConnectionInfo) {
132132
commandManager.register({ id: 'gitpod.api.autoTunnel', execute: () => remoteConnector.autoTunnelCommand });
133133

134-
remoteSession = new RemoteSession(remoteConnectionInfo.connectionInfo, context, remoteService, hostService, sessionService, settingsSync, experiments, logger!, telemetryService!, notificationService);
134+
remoteSession = new RemoteSession(remoteConnectionInfo.connectionInfo, context, remoteService, hostService, sessionService, experiments, logger!, telemetryService!, notificationService);
135135
await remoteSession.initialize();
136136
} else if (sessionService.isSignedIn()) {
137137
remoteService.checkForStoppedWorkspaces(async wsInfo => {

src/remoteSession.ts

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { v4 as uuid } from 'uuid';
87
import { NoRunningInstanceError, SSHConnectionParams, SSH_DEST_KEY, getGitpodRemoteWindowConnectionInfo } from './remote';
98
import { Disposable } from './common/dispose';
109
import { HeartbeatManager } from './heartbeat';
1110
import { WorkspaceState } from './workspaceState';
12-
import { ISyncExtension, NoSettingsSyncSession, NoSyncStoreError, SettingsSync, SyncResource, parseSyncData } from './settingsSync';
1311
import { IExperimentsService } from './experiments';
1412
import { ITelemetryService, UserFlowTelemetryProperties } from './common/telemetry';
1513
import { INotificationService } from './services/notificationService';
16-
import { retry } from './common/async';
1714
import { withServerApi } from './internalApi';
1815
import { ISessionService } from './services/sessionService';
1916
import { IHostService } from './services/hostService';
@@ -35,7 +32,6 @@ export class RemoteSession extends Disposable {
3532
private readonly remoteService: IRemoteService,
3633
private readonly hostService: IHostService,
3734
private readonly sessionService: ISessionService,
38-
private readonly settingsSync: SettingsSync,
3935
private readonly experiments: IExperimentsService,
4036
private readonly logService: ILogService,
4137
private readonly telemetryService: ITelemetryService,
@@ -101,10 +97,9 @@ export class RemoteSession extends Disposable {
10197

10298
this.heartbeatManager = new HeartbeatManager(this.connectionInfo, this.workspaceState, this.sessionService, this.logService, this.telemetryService);
10399

104-
const syncExtFlow = { ...this.connectionInfo, userId: this.sessionService.getUserId(), flow: 'sync_local_extensions' };
105-
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: true, flowId: uuid() });
100+
this.remoteService.initializeRemoteExtensions();
106101
this._register(vscode.commands.registerCommand('gitpod.installLocalExtensions', () => {
107-
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: false, flowId: uuid() });
102+
this.remoteService.initializeRemoteExtensions();
108103
}));
109104

110105
vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', true);
@@ -134,122 +129,6 @@ export class RemoteSession extends Disposable {
134129
}
135130
}
136131

137-
private async initializeRemoteExtensions(flow: UserFlowTelemetryProperties & { quiet: boolean; flowId: string }) {
138-
this.telemetryService.sendUserFlowStatus('enabled', flow);
139-
let syncData: { ref: string; content: string } | undefined;
140-
try {
141-
syncData = await this.settingsSync.readResource(SyncResource.Extensions);
142-
} catch (e) {
143-
if (e instanceof NoSyncStoreError) {
144-
const msg = `Could not install local extensions on remote workspace. Please enable [Settings Sync](https://www.gitpod.io/docs/ides-and-editors/settings-sync#enabling-settings-sync-in-vs-code-desktop) with Gitpod.`;
145-
this.logService.error(msg);
146-
147-
const status = 'no_sync_store';
148-
if (flow.quiet) {
149-
this.telemetryService.sendUserFlowStatus(status, flow);
150-
} else {
151-
const addSyncProvider = 'Settings Sync: Enable Sign In with Gitpod';
152-
const action = await this.notificationService.showInformationMessage(msg, { flow, id: status }, addSyncProvider);
153-
if (action === addSyncProvider) {
154-
vscode.commands.executeCommand('gitpod.syncProvider.add');
155-
}
156-
}
157-
} else if (e instanceof NoSettingsSyncSession) {
158-
const msg = `Could not install local extensions on remote workspace. Please enable [Settings Sync](https://www.gitpod.io/docs/ides-and-editors/settings-sync#enabling-settings-sync-in-vs-code-desktop) with Gitpod.`;
159-
this.logService.error(msg);
160-
161-
const status = 'no_settings_sync';
162-
if (flow.quiet) {
163-
this.telemetryService.sendUserFlowStatus(status, flow);
164-
} else {
165-
const enableSettingsSync = 'Enable Settings Sync';
166-
const action = await this.notificationService.showInformationMessage(msg, { flow, id: status }, enableSettingsSync);
167-
if (action === enableSettingsSync) {
168-
vscode.commands.executeCommand('workbench.userDataSync.actions.turnOn');
169-
}
170-
}
171-
} else {
172-
this.logService.error('Error while fetching settings sync extension data:', e);
173-
174-
const status = 'failed_to_fetch';
175-
if (flow.quiet) {
176-
this.telemetryService.sendUserFlowStatus(status, flow);
177-
} else {
178-
const seeLogs = 'See Logs';
179-
const action = await this.notificationService.showErrorMessage(`Error while fetching settings sync extension data.`, { flow, id: status }, seeLogs);
180-
if (action === seeLogs) {
181-
this.logService.show();
182-
}
183-
}
184-
}
185-
return;
186-
}
187-
188-
const syncDataContent = parseSyncData(syncData.content);
189-
if (!syncDataContent) {
190-
const msg = `Error while parsing settings sync extension data.`;
191-
this.logService.error(msg);
192-
193-
const status = 'failed_to_parse_content';
194-
if (flow.quiet) {
195-
this.telemetryService.sendUserFlowStatus(status, flow);
196-
} else {
197-
await this.notificationService.showErrorMessage(msg, { flow, id: status });
198-
}
199-
return;
200-
}
201-
202-
let extensions: ISyncExtension[];
203-
try {
204-
extensions = JSON.parse(syncDataContent.content);
205-
} catch {
206-
const msg = `Error while parsing settings sync extension data, malformed JSON.`;
207-
this.logService.error(msg);
208-
209-
const status = 'failed_to_parse_json';
210-
if (flow.quiet) {
211-
this.telemetryService.sendUserFlowStatus(status, flow);
212-
} else {
213-
await this.notificationService.showErrorMessage(msg, { flow, id: status });
214-
}
215-
return;
216-
}
217-
218-
extensions = extensions.filter(e => e.installed);
219-
flow.extensions = extensions.length;
220-
if (!extensions.length) {
221-
this.telemetryService.sendUserFlowStatus('synced', flow);
222-
return;
223-
}
224-
225-
try {
226-
try {
227-
this.logService.trace(`Installing local extensions on remote: `, extensions.map(e => e.identifier.id).join('\n'));
228-
await retry(async () => {
229-
await vscode.commands.executeCommand('__gitpod.initializeRemoteExtensions', extensions);
230-
}, 3000, 15);
231-
} catch (e) {
232-
this.logService.error(`Could not execute '__gitpod.initializeRemoteExtensions' command`);
233-
throw e;
234-
}
235-
this.telemetryService.sendUserFlowStatus('synced', flow);
236-
} catch {
237-
const msg = `Error while installing local extensions on remote.`;
238-
this.logService.error(msg);
239-
240-
const status = 'failed';
241-
if (flow.quiet) {
242-
this.telemetryService.sendUserFlowStatus(status, flow);
243-
} else {
244-
const seeLogs = 'See Logs';
245-
const action = await this.notificationService.showErrorMessage(msg, { flow, id: status }, seeLogs);
246-
if (action === seeLogs) {
247-
this.logService.show();
248-
}
249-
}
250-
}
251-
}
252-
253132
private async showRevertGitpodHostDialog() {
254133
const flow: UserFlowTelemetryProperties = { ...this.connectionInfo, flow: 'remote_session' };
255134
const revert: vscode.MessageItem = { title: 'Revert change' };

src/services/remoteService.ts

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

66
import * as vscode from 'vscode';
7+
import * as path from 'path';
78
import fsp from 'fs/promises';
89
import lockfile from 'proper-lockfile';
910
import { Disposable } from '../common/dispose';
@@ -28,6 +29,8 @@ import * as crypto from 'crypto';
2829
import { utils as sshUtils } from 'ssh2';
2930
import { INotificationService } from './notificationService';
3031
import { getOpenSSHVersion } from '../ssh/nativeSSH';
32+
import { retry } from '../common/async';
33+
import { IStoredProfileExtension } from '../settingsSync';
3134

3235
export interface IRemoteService {
3336
flow?: UserFlowTelemetryProperties;
@@ -39,6 +42,8 @@ export interface IRemoteService {
3942

4043
getWorkspaceSSHDestination(wsData: WorkspaceData): Promise<{ destination: SSHDestination; password?: string }>;
4144
showSSHPasswordModal(wsData: WorkspaceData, password: string): Promise<void>;
45+
46+
initializeRemoteExtensions(): Promise<void>;
4247
}
4348

4449
type FailedToInitializeCode = 'Unknown' | 'LockFailed' | string;
@@ -319,6 +324,54 @@ export class RemoteService extends Disposable implements IRemoteService {
319324
throw new Error('SSH password modal dialog, Canceled');
320325
}
321326

327+
async initializeRemoteExtensions() {
328+
let flowData = this.flow ?? { gitpodHost: this.hostService.gitpodHost, userId: this.sessionService.safeGetUserId() };
329+
flowData = { ...flowData, flow: 'sync_local_extensions', useLocalAPP: String(Configuration.getUseLocalApp()) };
330+
331+
let extensionsJson: IStoredProfileExtension[] = [];
332+
const extensionsDir = path.posix.dirname(this.context.extensionMode === vscode.ExtensionMode.Production ? this.context.extensionPath : vscode.extensions.getExtension('ms-vscode-remote.remote-ssh')!.extensionPath);
333+
const extensionFile = path.join(extensionsDir, 'extensions.json');
334+
try {
335+
const rawContent = await vscode.workspace.fs.readFile(vscode.Uri.file(extensionFile));
336+
const jsonSting = new TextDecoder().decode(rawContent);
337+
extensionsJson = JSON.parse(jsonSting);
338+
} catch (e) {
339+
this.logService.error(`Could not read ${extensionFile} file contents`, e);
340+
return;
341+
}
342+
343+
const localExtensions = extensionsJson.filter(e => !e.metadata?.isBuiltin && !e.metadata?.isSystem).map(e => ({ identifier: { id: e.identifier.id.toLowerCase() } }));
344+
345+
const allUserActiveExtensions = vscode.extensions.all.filter(ext => !ext.packageJSON['isBuiltin'] && !ext.packageJSON['isUserBuiltin']);
346+
const localActiveExtensions = new Set<string>();
347+
allUserActiveExtensions.forEach(e => localActiveExtensions.add(e.id.toLowerCase()));
348+
349+
const extensionsToInstall = localExtensions.filter(e => !localActiveExtensions.has(e.identifier.id));
350+
351+
try {
352+
try {
353+
this.logService.trace(`Installing local extensions on remote: `, extensionsToInstall.map(e => e.identifier.id).join('\n'));
354+
await retry(async () => {
355+
await vscode.commands.executeCommand('__gitpod.initializeRemoteExtensions', extensionsToInstall);
356+
}, 3000, 15);
357+
} catch (e) {
358+
this.logService.error(`Could not execute '__gitpod.initializeRemoteExtensions' command`);
359+
throw e;
360+
}
361+
this.telemetryService.sendUserFlowStatus('synced', flowData);
362+
} catch {
363+
const msg = `Error while installing local extensions on remote.`;
364+
this.logService.error(msg);
365+
366+
const status = 'failed';
367+
const seeLogs = 'See Logs';
368+
const action = await this.notificationService.showErrorMessage(msg, { flow: flowData, id: status }, seeLogs);
369+
if (action === seeLogs) {
370+
this.logService.show();
371+
}
372+
}
373+
}
374+
322375
private async withLock(path: string, cb: () => Promise<void>) {
323376
let release: () => Promise<void>;
324377
try {

src/settingsSync.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,41 @@ export interface ISyncData {
6666
content: string;
6767
}
6868

69+
// From https://github.com/microsoft/vscode/blob/5413247e57fb7e3d29cd36f08266005fe72bbde4/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts#L24-L30
70+
export interface IStoredProfileExtension {
71+
identifier: IExtensionIdentifier;
72+
location: UriComponents | string;
73+
relativeLocation: string | undefined;
74+
version: string;
75+
metadata?: Metadata;
76+
}
77+
interface UriComponents {
78+
scheme: string;
79+
authority?: string;
80+
path?: string;
81+
query?: string;
82+
fragment?: string;
83+
}
84+
85+
interface IGalleryMetadata {
86+
id: string;
87+
publisherId: string;
88+
publisherDisplayName: string;
89+
isPreReleaseVersion: boolean;
90+
targetPlatform?: string;
91+
}
92+
93+
type Metadata = Partial<IGalleryMetadata & {
94+
isApplicationScoped: boolean;
95+
isMachineScoped: boolean;
96+
isBuiltin: boolean;
97+
isSystem: boolean;
98+
updated: boolean;
99+
preRelease: boolean;
100+
installedTimestamp: number;
101+
pinned: boolean;
102+
}>;
103+
69104
function isSyncData(thing: any): thing is ISyncData {
70105
if (thing
71106
&& (thing.version !== undefined && typeof thing.version === 'number')

0 commit comments

Comments
 (0)