Skip to content

Commit

Permalink
Add failed() to IRemoteExtensionsScannerService (first pass)
Browse files Browse the repository at this point in the history
Provides a way for the remote server to report back to the workbench a
set of extensions that it could not install.  The workbench will
then attempt to install the set of extensions.

This is useful in cases when a remote does not have access to the
marketplace, but a local machine does.  This will effectively proxy
the installation of an extension through the user's machine.
  • Loading branch information
joshspicer committed Jan 27, 2025
1 parent d21929c commit b73d660
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const useId = localize('useId', "Make sure you use the full extension ID, includ
type InstallVSIXInfo = { vsix: URI; installOptions: InstallOptions };
type InstallGalleryExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };

export class ExtensionInstallationError extends Error {
constructor(message: string, public failed: string[], public innerError?: Error) {
super(innerError?.message ?? message);
this.name = innerError?.name ?? 'ExtensionInstallationError';
this.stack = innerError?.stack;
}
}

export class ExtensionManagementCLI {

constructor(
Expand Down Expand Up @@ -119,11 +127,19 @@ export class ExtensionManagementCLI {
}
} catch (error) {
this.logger.error(localize('error while installing extensions', "Error while installing extensions: {0}", getErrorMessage(error)));
throw error;
throw new ExtensionInstallationError(
'Error while installing extensions',
extensions.map(ext => ext instanceof URI ? ext.toString() : ext), // TODO: This could use some work
error);
}

if (failed.length) {
throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));
throw new ExtensionInstallationError(
localize(
'installation failed',
"Failed installing extensions: {0}",
failed.join(', ')),
failed);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/remote/common/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface IRemoteExtensionsScannerService {

whenExtensionsReady(): Promise<void>;
scanExtensions(): Promise<IExtensionDescription[]>;
failed(): Promise<string[]>;
}
21 changes: 20 additions & 1 deletion src/vs/server/node/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IURITransformer, transformOutgoingURIs } from '../../base/common/uriIpc
import { IServerChannel } from '../../base/parts/ipc/common/ipc.js';
import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from '../../platform/contextkey/common/contextkey.js';
import { IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js';
import { ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js';
import { ExtensionInstallationError, ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js';
import { IExtensionsScannerService, toExtensionDescription } from '../../platform/extensionManagement/common/extensionsScannerService.js';
import { ExtensionType, IExtensionDescription } from '../../platform/extensions/common/extensions.js';
import { ILogService } from '../../platform/log/common/log.js';
Expand All @@ -30,6 +30,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS

private readonly _whenBuiltinExtensionsReady = Promise.resolve();
private readonly _whenExtensionsReady = Promise.resolve();
private failedExtensionInstallations: Set<string> = new Set(); // TODO: Should probably track a more complex type than 'string'. Could also have a VSIX uri

constructor(
private readonly _extensionManagementCLI: ExtensionManagementCLI,
Expand All @@ -51,6 +52,12 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
_logService.trace('Finished installing builtin extensions');
}, error => {
_logService.error(error);
if (error instanceof ExtensionInstallationError) {
_logService.error(`Capturing failed builtin extension installations: ${error.failed.join(', ')}`);
for (const extension of error.failed) {
this.failedExtensionInstallations.add(extension);
}
}
});
}

Expand All @@ -67,10 +74,21 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
_logService.trace('Finished installing extensions');
}, error => {
_logService.error(error);
if (error instanceof ExtensionInstallationError) {
_logService.error(`Capturing failed extension installations: ${error.failed.join(', ')}`);
for (const extension of error.failed) {
this.failedExtensionInstallations.add(extension);
}
}
});
}
}

async failed(): Promise<string[]> {
await this._whenExtensionsReady;
return Array.from(this.failedExtensionInstallations);
}

private _asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] {
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input);
}
Expand Down Expand Up @@ -308,6 +326,7 @@ export class RemoteExtensionsScannerChannel implements IServerChannel {
async call(context: any, command: string, args?: any): Promise<any> {
const uriTransformer = this.getUriTransformer(context);
switch (command) {
case 'failed': return this.service.failed();
case 'whenExtensionsReady': return this.service.whenExtensionsReady();
case 'scanExtensions': {
const language = args[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,18 @@ export class NativeExtensionService extends AbstractExtensionService implements
connection.onReconnecting(() => this._resolveAuthorityAgain());
}

// Install extensions that remote server could not install on its own
// ie: Due to lack of internet access on the remote
const failedFromRemote = await this._remoteExtensionsScannerService.failed();
if (failedFromRemote.length) {
this._logService.debug('Failed from remote', failedFromRemote);
const exts = await this._extensionGalleryService.getExtensions(failedFromRemote.map(id => ({ id })), CancellationToken.None);
for (const ext of exts) {
// TODO: These could be VSIX's
await this._extensionManagementService.installFromGallery(ext);
}
}

// fetch the remote environment
[remoteEnv, remoteExtensions] = await Promise.all([
this._remoteAgentService.getEnvironment(),
Expand Down
12 changes: 12 additions & 0 deletions src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService
);
}

async failed(): Promise<string[]> {
try {
return await this.withChannel(
channel => channel.call<string[]>('failed'),
[]
);
} catch (error) {
this.logService.error(error);
return [];
}
}

async scanExtensions(): Promise<IExtensionDescription[]> {
try {
const languagePack = await this.activeLanguagePackService.getExtensionIdProvidingCurrentLocale();
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2162,6 +2162,9 @@ export class TestRemoteAgentService implements IRemoteAgentService {
}

export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScannerService {
failed(): Promise<string[]> {
throw new Error('Method not implemented.');
}
declare readonly _serviceBrand: undefined;
async whenExtensionsReady(): Promise<void> { }
scanExtensions(): Promise<IExtensionDescription[]> { throw new Error('Method not implemented.'); }
Expand Down

0 comments on commit b73d660

Please sign in to comment.