From c198925570cff7dc1079f04acea597cfb483a590 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 15 Jan 2021 15:39:13 +0100 Subject: [PATCH] extensionsManagement for remote CLI --- src/vs/code/node/cliProcessMain.ts | 315 +---------------- .../common/extensionManagement.ts | 16 + .../common/extensionManagementCLIService.ts | 322 ++++++++++++++++++ .../localizations/common/localizations.ts | 2 + .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadCLICommands.ts | 55 +++ src/vs/workbench/api/common/apiCommands.ts | 9 +- src/vs/workbench/api/node/extHostCLIServer.ts | 31 +- src/vs/workbench/workbench.common.main.ts | 4 +- 9 files changed, 440 insertions(+), 315 deletions(-) create mode 100644 src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts create mode 100644 src/vs/workbench/api/browser/mainThreadCLICommands.ts diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 84fa06d76d5..fde265c3295 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,11 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { raceTimeout } from 'vs/base/common/async'; -import * as semver from 'vs/base/common/semver/semver'; import product from 'vs/platform/product/common/product'; -import * as path from 'vs/base/common/path'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -15,7 +12,7 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { IExtensionManagementService, IExtensionGalleryService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -28,17 +25,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { mkdirp, writeFile } from 'vs/base/node/pfs'; -import { getBaseLabel } from 'vs/base/common/labels'; import { IStateService } from 'vs/platform/state/node/state'; import { StateService } from 'vs/platform/state/node/stateService'; import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; -import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { URI } from 'vs/base/common/uri'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IExtensionManifest, ExtensionType, isLanguagePackExtension, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { Schemas } from 'vs/base/common/network'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry'; @@ -47,51 +36,29 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; - -const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); -const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); -const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); - -function getId(manifest: IExtensionManifest, withVersion?: boolean): string { - if (withVersion) { - return `${manifest.publisher}.${manifest.name}@${manifest.version}`; - } else { - return `${manifest.publisher}.${manifest.name}`; - } -} - -const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; - -export function getIdAndVersion(id: string): [string, string | undefined] { - const matches = EXTENSION_ID_REGEX.exec(id); - if (matches && matches[1]) { - return [adoptToGalleryExtensionId(matches[1]), matches[2]]; - } - return [adoptToGalleryExtensionId(id), undefined]; -} - -type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; export class Main { constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IExtensionManagementCLIService private readonly extensionManagementCLIService: IExtensionManagementCLIService ) { } async run(argv: NativeParsedArgs): Promise { if (argv['install-source']) { await this.setInstallSource(argv['install-source']); - } else if (argv['list-extensions']) { - await this.listExtensions(!!argv['show-versions'], argv['category']); + return; + } + + if (argv['list-extensions']) { + await this.extensionManagementCLIService.listExtensions(!!argv['show-versions'], argv['category']); } else if (argv['install-extension'] || argv['install-builtin-extension']) { - await this.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); + await this.extensionManagementCLIService.installExtensions(argv['install-extension'] || [], argv['install-builtin-extension'] || [], !!argv['do-not-sync'], !!argv['force']); } else if (argv['uninstall-extension']) { - await this.uninstallExtension(argv['uninstall-extension'], !!argv['force']); + await this.extensionManagementCLIService.uninstallExtensions(argv['uninstall-extension'], !!argv['force']); } else if (argv['locate-extension']) { - await this.locateExtension(argv['locate-extension']); + await this.extensionManagementCLIService.locateExtension(argv['locate-extension']); } else if (argv['telemetry']) { console.log(buildTelemetryMessage(this.environmentService.appRoot, this.environmentService.extensionsPath)); } @@ -101,265 +68,6 @@ export class Main { return writeFile(this.environmentService.installSourcePath, installSource.slice(0, 30)); } - private async listExtensions(showVersions: boolean, category?: string): Promise { - let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); - if (category && category !== '') { - if (categories.indexOf(category.toLowerCase()) < 0) { - console.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); - return; - } - extensions = extensions.filter(e => { - if (e.manifest.categories) { - const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); - return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; - } - return false; - }); - } else if (category === '') { - console.log('Possible Categories: '); - categories.forEach(category => { - console.log(category); - }); - return; - } - extensions.forEach(e => console.log(getId(e.manifest, showVersions))); - } - - async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean): Promise { - const failed: string[] = []; - const installedExtensionsManifests: IExtensionManifest[] = []; - if (extensions.length) { - console.log(localize('installingExtensions', "Installing extensions...")); - } - - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); - const checkIfNotInstalled = (id: string, version?: string): boolean => { - const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); - if (installedExtension) { - if (!version && !force) { - console.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); - return false; - } - if (version && installedExtension.manifest.version === version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); - return false; - } - } - return true; - }; - const vsixs: string[] = []; - const installExtensionInfos: InstallExtensionInfo[] = []; - for (const extension of extensions) { - if (/\.vsix$/i.test(extension)) { - vsixs.push(extension); - } else { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); - } - } - } - for (const extension of builtinExtensionIds) { - const [id, version] = getIdAndVersion(extension); - if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); - } - } - - if (vsixs.length) { - await Promise.all(vsixs.map(async vsix => { - try { - const manifest = await this.installVSIX(vsix, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(vsix); - } - })); - } - - if (installExtensionInfos.length) { - - const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); - - await Promise.all(installExtensionInfos.map(async extensionInfo => { - const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); - if (gallery) { - try { - const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); - if (manifest) { - installedExtensionsManifests.push(manifest); - } - } catch (err) { - console.error(err.message || err.stack || err); - failed.push(extensionInfo.id); - } - } else { - console.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); - failed.push(extensionInfo.id); - } - })); - - } - - if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { - await this.updateLocalizationsCache(); - } - - if (failed.length) { - throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); - } - } - - private async installVSIX(vsix: string, force: boolean): Promise { - vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); - const manifest = await getManifest(vsix); - const valid = await this.validate(manifest, force); - if (valid) { - try { - await this.extensionManagementService.install(URI.file(vsix)); - console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); - return null; - } else { - throw error; - } - } - } - return null; - } - - private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { - const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); - const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); - - const galleryExtensions = new Map(); - await Promise.all([ - (async () => { - const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); - result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); - })(), - Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { - const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); - if (extension) { - galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); - } - })) - ]); - - return galleryExtensions; - } - - private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean): Promise { - const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); - const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); - if (installedExtension) { - if (galleryExtension.version === installedExtension.manifest.version) { - console.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); - return null; - } - console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); - } - - try { - if (installOptions.isBuiltin) { - console.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); - } else { - console.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); - } - await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); - console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); - return manifest; - } catch (error) { - if (isPromiseCanceledError(error)) { - console.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); - return null; - } else { - throw error; - } - } - } - - private async validate(manifest: IExtensionManifest, force: boolean): Promise { - if (!manifest) { - throw new Error('Invalid vsix'); - } - - const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); - - if (newer && !force) { - console.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); - return false; - } - - return true; - } - - private async uninstallExtension(extensions: string[], force: boolean): Promise { - async function getExtensionId(extensionDescription: string): Promise { - if (!/\.vsix$/i.test(extensionDescription)) { - return extensionDescription; - } - - const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription); - const manifest = await getManifest(zipPath); - return getId(manifest); - } - - const uninstalledExtensions: ILocalExtension[] = []; - for (const extension of extensions) { - const id = await getExtensionId(extension); - const installed = await this.extensionManagementService.getInstalled(); - const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id })); - if (!extensionToUninstall) { - throw new Error(`${notInstalled(id)}\n${useId}`); - } - if (extensionToUninstall.type === ExtensionType.System) { - console.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); - return; - } - if (extensionToUninstall.isBuiltin && !force) { - console.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); - return; - } - console.log(localize('uninstalling', "Uninstalling {0}...", id)); - await this.extensionManagementService.uninstall(extensionToUninstall); - uninstalledExtensions.push(extensionToUninstall); - console.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); - } - - if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { - await this.updateLocalizationsCache(); - } - } - - private async locateExtension(extensions: string[]): Promise { - const installed = await this.extensionManagementService.getInstalled(); - extensions.forEach(e => { - installed.forEach(i => { - if (i.identifier.id === e) { - if (i.location.scheme === Schemas.file) { - console.log(i.location.fsPath); - return; - } - } - }); - }); - } - - private async updateLocalizationsCache(): Promise { - const localizationService = this.instantiationService.createInstance(LocalizationsService); - await localizationService.update(); - localizationService.dispose(); - } } const eventPrefix = 'monacoworkbench'; @@ -414,6 +122,7 @@ export async function main(argv: NativeParsedArgs): Promise { services.set(IRequestService, new SyncDescriptor(RequestService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); const appenders: AppInsightsAppender[] = []; if (isBuilt && !extensionDevelopmentLocationURI && !environmentService.disableTelemetry && product.enableTelemetry) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 0e46a92d69f..f9245cf1590 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -278,3 +278,19 @@ export const ExtensionsLocalizedLabel = { value: ExtensionsLabel, original: 'Ext export const ExtensionsChannelId = 'extensions'; export const PreferencesLabel = localize('preferences', "Preferences"); export const PreferencesLocalizedLabel = { value: PreferencesLabel, original: 'Preferences' }; + + +export interface CLIOutput { + log(s: string): void; + error(s: string): void; +} + +export const IExtensionManagementCLIService = createDecorator('IExtensionManagementCLIService'); +export interface IExtensionManagementCLIService { + readonly _serviceBrand: undefined; + + listExtensions(showVersions: boolean, category?: string, output?: CLIOutput): Promise; + installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output?: CLIOutput): Promise; + uninstallExtensions(extensions: string[], force: boolean, output?: CLIOutput): Promise; + locateExtension(extensions: string[], output?: CLIOutput): Promise; +} diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts new file mode 100644 index 00000000000..036fad132a3 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -0,0 +1,322 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; + +import { isPromiseCanceledError } from 'vs/base/common/errors'; +import * as path from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import * as semver from 'vs/base/common/semver/semver'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; + +const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); +const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); +const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp'); + + +function getId(manifest: IExtensionManifest, withVersion?: boolean): string { + if (withVersion) { + return `${manifest.publisher}.${manifest.name}@${manifest.version}`; + } else { + return `${manifest.publisher}.${manifest.name}`; + } +} + +const EXTENSION_ID_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; + +export function getIdAndVersion(id: string): [string, string | undefined] { + const matches = EXTENSION_ID_REGEX.exec(id); + if (matches && matches[1]) { + return [adoptToGalleryExtensionId(matches[1]), matches[2]]; + } + return [adoptToGalleryExtensionId(id), undefined]; +} + +type InstallExtensionInfo = { id: string, version?: string, installOptions: InstallOptions }; + + +export class ExtensionManagementCLIService implements IExtensionManagementCLIService { + + _serviceBrand: any; + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILocalizationsService private readonly localizationsService: ILocalizationsService + ) { } + + + public async listExtensions(showVersions: boolean, category?: string, output: CLIOutput = console): Promise { + let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase()); + if (category && category !== '') { + if (categories.indexOf(category.toLowerCase()) < 0) { + output.log('Invalid category please enter a valid category. To list valid categories run --category without a category specified'); + return; + } + extensions = extensions.filter(e => { + if (e.manifest.categories) { + const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase()); + return lowerCaseCategories.indexOf(category.toLowerCase()) > -1; + } + return false; + }); + } else if (category === '') { + output.log('Possible Categories: '); + categories.forEach(category => { + output.log(category); + }); + return; + } + const idSeen = new Map(); + for (let extension of extensions) { + if (!idSeen.get(extension.identifier.id)) { + idSeen.set(extension.identifier.id, true); + output.log(getId(extension.manifest, showVersions)); + } + } + } + + async installExtensions(extensions: string[], builtinExtensionIds: string[], isMachineScoped: boolean, force: boolean, output: CLIOutput = console): Promise { + const failed: string[] = []; + const installedExtensionsManifests: IExtensionManifest[] = []; + if (extensions.length) { + output.log(localize('installingExtensions', "Installing extensions...")); + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const checkIfNotInstalled = (id: string, version?: string): boolean => { + const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); + if (installedExtension) { + if (!version && !force) { + output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); + return false; + } + if (version && installedExtension.manifest.version === version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`)); + return false; + } + } + return true; + }; + const vsixs: string[] = []; + const installExtensionInfos: InstallExtensionInfo[] = []; + for (const extension of extensions) { + if (/\.vsix$/i.test(extension)) { + vsixs.push(extension); + } else { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: false, isMachineScoped } }); + } + } + } + for (const extension of builtinExtensionIds) { + const [id, version] = getIdAndVersion(extension); + if (checkIfNotInstalled(id, version)) { + installExtensionInfos.push({ id, version, installOptions: { isBuiltin: true, isMachineScoped: false } }); + } + } + + if (vsixs.length) { + await Promise.all(vsixs.map(async vsix => { + try { + const manifest = await this.installVSIX(vsix, force); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(vsix); + } + })); + } + + if (installExtensionInfos.length) { + + const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos); + + await Promise.all(installExtensionInfos.map(async extensionInfo => { + const gallery = galleryExtensions.get(extensionInfo.id.toLowerCase()); + if (gallery) { + try { + const manifest = await this.installFromGallery(extensionInfo, gallery, installed, force); + if (manifest) { + installedExtensionsManifests.push(manifest); + } + } catch (err) { + output.error(err.message || err.stack || err); + failed.push(extensionInfo.id); + } + } else { + output.error(`${notFound(extensionInfo.version ? `${extensionInfo.id}@${extensionInfo.version}` : extensionInfo.id)}\n${useId}`); + failed.push(extensionInfo.id); + } + })); + + } + + if (installedExtensionsManifests.some(manifest => isLanguagePackExtension(manifest))) { + await this.updateLocalizationsCache(); + } + + if (failed.length) { + throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))); + } + } + + private async installVSIX(vsix: string, force: boolean, output: CLIOutput = console): Promise { + vsix = path.isAbsolute(vsix) ? vsix : path.join(process.cwd(), vsix); + const manifest = await this.extensionManagementService.getManifest(URI.file(vsix)); + const valid = await this.validate(manifest, force, output); + if (valid) { + try { + await this.extensionManagementService.install(URI.file(vsix)); + output.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(vsix))); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", getBaseLabel(vsix))); + return null; + } else { + throw error; + } + } + } + return null; + } + + private async getGalleryExtensions(extensions: InstallExtensionInfo[]): Promise> { + const extensionIds = extensions.filter(({ version }) => version === undefined).map(({ id }) => id); + const extensionsWithIdAndVersion = extensions.filter(({ version }) => version !== undefined); + + const galleryExtensions = new Map(); + await Promise.all([ + (async () => { + const result = await this.extensionGalleryService.getExtensions(extensionIds, CancellationToken.None); + result.forEach(extension => galleryExtensions.set(extension.identifier.id.toLowerCase(), extension)); + })(), + Promise.all(extensionsWithIdAndVersion.map(async ({ id, version }) => { + const extension = await this.extensionGalleryService.getCompatibleExtension({ id }, version); + if (extension) { + galleryExtensions.set(extension.identifier.id.toLowerCase(), extension); + } + })) + ]); + + return galleryExtensions; + } + + private async installFromGallery({ id, version, installOptions }: InstallExtensionInfo, galleryExtension: IGalleryExtension, installed: ILocalExtension[], force: boolean, output: CLIOutput = console): Promise { + const manifest = await this.extensionGalleryService.getManifest(galleryExtension, CancellationToken.None); + const installedExtension = installed.find(e => areSameExtensions(e.identifier, galleryExtension.identifier)); + if (installedExtension) { + if (galleryExtension.version === installedExtension.manifest.version) { + output.log(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id)); + return null; + } + output.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, galleryExtension.version)); + } + + try { + if (installOptions.isBuiltin) { + output.log(localize('installing builtin ', "Installing builtin extension '{0}' v{1}...", id, galleryExtension.version)); + } else { + output.log(localize('installing', "Installing extension '{0}' v{1}...", id, galleryExtension.version)); + } + await this.extensionManagementService.installFromGallery(galleryExtension, installOptions); + output.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, galleryExtension.version)); + return manifest; + } catch (error) { + if (isPromiseCanceledError(error)) { + output.log(localize('cancelInstall', "Cancelled installing extension '{0}'.", id)); + return null; + } else { + throw error; + } + } + } + + private async validate(manifest: IExtensionManifest, force: boolean, output: CLIOutput = console): Promise { + if (!manifest) { + throw new Error('Invalid vsix'); + } + + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); + + if (newer && !force) { + output.log(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version)); + return false; + } + + return true; + } + + public async uninstallExtensions(extensions: string[], force: boolean, output: CLIOutput = console): Promise { + const getExtensionId = async (extensionDescription: string): Promise => { + if (!/\.vsix$/i.test(extensionDescription)) { + return extensionDescription; + } + + const zipPath = path.isAbsolute(extensionDescription) ? extensionDescription : path.join(process.cwd(), extensionDescription); + const manifest = await this.extensionManagementService.getManifest(URI.file(zipPath)); + return getId(manifest); + }; + + const uninstalledExtensions: ILocalExtension[] = []; + for (const extension of extensions) { + const id = await getExtensionId(extension); + const installed = await this.extensionManagementService.getInstalled(); + const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, { id })); + if (!extensionToUninstall) { + throw new Error(`${notInstalled(id)}\n${useId}`); + } + if (extensionToUninstall.type === ExtensionType.System) { + output.log(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be installed", id)); + return; + } + if (extensionToUninstall.isBuiltin && !force) { + output.log(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id)); + return; + } + output.log(localize('uninstalling', "Uninstalling {0}...", id)); + await this.extensionManagementService.uninstall(extensionToUninstall); + uninstalledExtensions.push(extensionToUninstall); + output.log(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id)); + } + + if (uninstalledExtensions.some(e => isLanguagePackExtension(e.manifest))) { + await this.updateLocalizationsCache(); + } + } + + public async locateExtension(extensions: string[], output: CLIOutput = console): Promise { + const installed = await this.extensionManagementService.getInstalled(); + extensions.forEach(e => { + installed.forEach(i => { + if (i.identifier.id === e) { + if (i.location.scheme === Schemas.file) { + output.log(i.location.fsPath); + return; + } + } + }); + }); + } + + + private updateLocalizationsCache(): Promise { + return this.localizationsService.update(); + } +} + diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index 509c4ada830..27fa81bae5f 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -25,6 +25,8 @@ export interface ILocalizationsService { readonly onDidLanguagesChange: Event; getLanguageIds(): Promise; + + update(): Promise; } export function isValidLocalization(localization: ILocalization): boolean { diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index e94dfa8a519..64e531872ae 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -17,6 +17,7 @@ import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEdito // --- mainThread participants import './mainThreadBulkEdits'; import './mainThreadCodeInsets'; +import './mainThreadCLICommands'; import './mainThreadClipboard'; import './mainThreadCommands'; import './mainThreadConfiguration'; diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts new file mode 100644 index 00000000000..a3cd51487ca --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI, UriComponents } from 'vs/base/common/uri'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + + +// this class contains the command that the CLI server is reying on + +CommandsRegistry.registerCommand('_cli.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { + // TODO: discuss martin, ben where to put this + const openerService = accessor.get(IOpenerService); + openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true }); +}); + +interface ManageExtensionsArgs { + list?: { showVersions?: boolean, category?: string; }; + install?: string[]; + uninstall?: string[]; + force?: boolean; +} + +CommandsRegistry.registerCommand('_cli.manageExtensions', async function (accessor: ServicesAccessor, args: ManageExtensionsArgs) { + + const cliService = accessor.get(IExtensionManagementCLIService); + + const lines: string[] = []; + const output = { log: lines.push.bind(lines), error: lines.push.bind(lines) }; + + if (args.list) { + await cliService.listExtensions(!!args.list.showVersions, args.list.category, output); + } else { + if (Array.isArray(args.install) && args.install.length) { + try { + await cliService.installExtensions(args.install, [], false, !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + if (Array.isArray(args.uninstall) && args.uninstall.length) { + try { + await cliService.uninstallExtensions(args.uninstall, !!args.force, output); + } catch (e) { + lines.push(e.message); + } + } + } + return lines.join('\n'); +}); + diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 063e8566414..d4a7ea40738 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -12,7 +12,6 @@ import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/works import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -128,12 +127,6 @@ CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (access } }); -CommandsRegistry.registerCommand('_workbench.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) { - // TODO: discuss martin, ben where to put this - const openerService = accessor.get(IOpenerService); - openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true }); -}); - CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) { const logService = accessor.get(ILogService); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index bb5ec1cd7e0..9ae6f7b379b 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -39,7 +39,15 @@ export interface RunCommandPipeArgs { args: any[]; } -export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs; +export interface ExtensionManagementPipeArgs { + type: 'extensionManagement'; + list?: { showVersions?: boolean, category?: string; }; + install?: string[]; + uninstall?: string[]; + force?: boolean; +} + +export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs | ExtensionManagementPipeArgs; export interface ICommandsExecuter { executeCommand(id: string, ...args: any[]): Promise; @@ -95,6 +103,10 @@ export class CLIServerBase { this.runCommand(data, res) .catch(this.logService.error); break; + case 'extensionManagement': + this.manageExtensions(data, res) + .catch(this.logService.error); + break; default: res.writeHead(404); res.write(`Unknown message type: ${data.type}`, err => { @@ -143,14 +155,27 @@ export class CLIServerBase { res.end(); } - private openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { + private async openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) { for (const uri of data.uris) { - this._commands.executeCommand('_workbench.openExternal', URI.parse(uri), { allowTunneling: true }); + await this._commands.executeCommand('_cli.openExternal', URI.parse(uri), { allowTunneling: true }); } res.writeHead(200); res.end(); } + private async manageExtensions(data: ExtensionManagementPipeArgs, res: http.ServerResponse) { + console.log('server: manageExtensions'); + try { + const output = await this._commands.executeCommand('_cli.manageExtensions', data, { allowTunneling: true }); + res.writeHead(200); + res.write(output); + } catch (e) { + res.writeHead(500); + res.write(toString()); + } + res.end(); + } + private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) { try { const status = await this._commands.executeCommand('_issues.getSystemStatus'); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 301cf09054b..dcc96e5901c 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -93,8 +93,9 @@ import 'vs/workbench/services/outline/browser/outlineService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; @@ -126,6 +127,7 @@ registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManageme registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService); registerSingleton(IExtensionsStorageSyncService, ExtensionsStorageSyncService); registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); +registerSingleton(IExtensionManagementCLIService, ExtensionManagementCLIService); registerSingleton(IContextViewService, ContextViewService, true); registerSingleton(IListService, ListService, true); registerSingleton(IEditorWorkerService, EditorWorkerServiceImpl); -- GitLab