From e549d660eded95a3c8a3b0fec9b853a4d9906cab Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 3 Feb 2023 16:24:24 +0100 Subject: [PATCH] Fix #162051 (#173308) --- .../common/extensionsScannerService.ts | 1 - .../node/extensionLifecycle.ts | 19 +++- .../node/extensionManagementService.ts | 97 +++++++++++++------ .../node/extensionsWatcher.ts | 34 +++++-- 4 files changed, 106 insertions(+), 45 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 0cdc006f552..8012c1dfcd8 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -266,7 +266,6 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem // unset if false metaData.isMachineScoped = metaData.isMachineScoped || undefined; metaData.isBuiltin = metaData.isBuiltin || undefined; - metaData.installedTimestamp = metaData.installedTimestamp || undefined; manifest.__metadata = { ...manifest.__metadata, ...metaData }; await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t'))); diff --git a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts index 0933c2390f5..8bcaf43a632 100644 --- a/src/vs/platform/extensionManagement/node/extensionLifecycle.ts +++ b/src/vs/platform/extensionManagement/node/extensionLifecycle.ts @@ -30,11 +30,22 @@ export class ExtensionsLifecycle extends Disposable { const script = this.parseScript(extension, 'uninstall'); if (script) { this.logService.info(extension.identifier.id, extension.manifest.version, `Running post uninstall script`); - await this.processesLimiter.queue(() => - this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension) - .then(() => this.logService.info(extension.identifier.id, extension.manifest.version, `Finished running post uninstall script`), err => this.logService.error(extension.identifier.id, extension.manifest.version, `Failed to run post uninstall script: ${err}`))); + await this.processesLimiter.queue(async () => { + try { + await this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension); + this.logService.info(`Finished running post uninstall script`, extension.identifier.id, extension.manifest.version); + } catch (error) { + this.logService.error('Failed to run post uninstall script', extension.identifier.id, extension.manifest.version); + this.logService.error(error); + } + }); + } + try { + await Promises.rm(this.getExtensionStoragePath(extension)); + } catch (error) { + this.logService.error('Error while removing extension storage path', extension.identifier.id); + this.logService.error(error); } - return Promises.rm(this.getExtensionStoragePath(extension)).then(undefined, e => this.logService.error('Error while removing extension storage path', e)); } private parseScript(extension: ILocalExtension, type: string): { script: string; args: string[] } | null { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 5ab2561f956..46f9f27ac9c 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Promises, Queue } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -39,7 +40,7 @@ import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/ex import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtension, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { IInstantiationService, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -56,9 +57,10 @@ interface InstallableExtension { export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; + scanAllUserInstalledExtensions(): Promise; + scanInstalledExtensionAtLocation(location: URI): Promise; markAsUninstalled(...extensions: IExtension[]): Promise; removeUninstalledExtensions(): Promise; - getAllUserInstalled(): Promise; } export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService { @@ -132,10 +134,14 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); } - getAllUserInstalled(): Promise { + scanAllUserInstalledExtensions(): Promise { return this.extensionsScanner.scanAllUserExtensions(false); } + scanInstalledExtensionAtLocation(location: URI): Promise { + return this.extensionsScanner.scanUserExtensionAtLocation(location); + } + async install(vsix: URI, options: InstallVSIXOptions = {}): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); @@ -208,8 +214,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.setUninstalled(...extensions); } - removeUninstalledExtensions(): Promise { - return this.extensionsScanner.cleanUp(); + async removeUninstalledExtensions(): Promise { + this.logService.trace('ExtensionManagementService#removeUninstalledExtensions'); + try { + await this.extensionsScanner.cleanUp(); + } catch (error) { + this.logService.error(error); + } } async download(extension: IGalleryExtension, operation: InstallOperation): Promise { @@ -361,15 +372,28 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } private async addExtensionsToProfile(extensions: ILocalExtension[], profileLocation: URI): Promise { + await this.setInstalled(extensions); await this.extensionsProfileScannerService.addExtensionsToProfile(extensions.map(local => ([local, undefined])), profileLocation); this._onDidInstallExtensions.fire(extensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); } + private async setInstalled(extensions: ILocalExtension[]): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + for (const extension of extensions) { + const extensionKey = ExtensionKey.create(extension); + if (!uninstalled[extensionKey.toString()]) { + continue; + } + this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); + await this.extensionsScanner.setInstalled(extensionKey); + this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); + } + } } export class ExtensionsScanner extends Disposable { - private readonly uninstalledPath: string; + private readonly uninstalledResource: URI; private readonly uninstalledFileLimiter: Queue; private readonly _onExtract = this._register(new Emitter()); @@ -383,7 +407,7 @@ export class ExtensionsScanner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath; + this.uninstalledResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); this.uninstalledFileLimiter = new Queue(); } @@ -484,19 +508,13 @@ export class ExtensionsScanner extends Disposable { })); } - async setInstalled(extensionKey: ExtensionKey): Promise { + async setInstalled(extensionKey: ExtensionKey): Promise { await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - const userExtensions = await this.scanAllUserExtensions(true); - const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; - if (!localExtension) { - return null; - } - return this.updateMetadata(localExtension, { installedTimestamp: Date.now() }); } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - await pfs.Promises.rm(extension.location.fsPath); + await this.fileService.del(extension.location, { recursive: true }); this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); } @@ -509,10 +527,11 @@ export class ExtensionsScanner extends Disposable { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; + const content = await this.fileService.readFile(this.uninstalledResource, 'utf8'); + raw = content.value.toString(); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + throw error; } } @@ -526,9 +545,9 @@ export class ExtensionsScanner extends Disposable { if (updateFn) { updateFn(uninstalled); if (Object.keys(uninstalled).length) { - await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); + await this.fileService.writeFile(this.uninstalledResource, VSBuffer.fromString(JSON.stringify(uninstalled))); } else { - await pfs.Promises.rm(this.uninstalledPath); + await this.fileService.del(this.uninstalledResource); } } @@ -616,6 +635,8 @@ export class ExtensionsScanner extends Disposable { private async removeUninstalledExtensions(): Promise { const uninstalled = await this.getUninstalledExtensions(); + this.logService.debug(`Removing uninstalled extensions:`, Object.keys(uninstalled)); + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions const installed: Set = new Set(); for (const e of extensions) { @@ -623,15 +644,22 @@ export class ExtensionsScanner extends Disposable { installed.add(e.identifier.id.toLowerCase()); } } - const byExtension = groupByExtension(extensions, e => e.identifier); - await Promises.settled(byExtension.map(async e => { - const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; - if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(await this.toLocalExtension(latest)); - } - })); - const toRemove = extensions.filter(e => e.metadata /* Installed by VS Code */ && uninstalled[ExtensionKey.create(e).toString()]); - await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); + + try { + // running post uninstall tasks for extensions that are not installed anymore + const byExtension = groupByExtension(extensions, e => e.identifier); + await Promises.settled(byExtension.map(async e => { + const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; + if (!installed.has(latest.identifier.id.toLowerCase())) { + await this.beforeRemovingExtension(await this.toLocalExtension(latest)); + } + })); + } catch (error) { + this.logService.error(error); + } + + const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && uninstalled[ExtensionKey.create(e).toString()]); + await Promise.allSettled(toRemove.map(e => this.removeUninstalledExtension(e))); } private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { @@ -688,10 +716,15 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILoca this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); // If the same version of extension is marked as uninstalled, remove it from there and return the local. - const local = await this.extensionsScanner.setInstalled(extensionKey); + await this.extensionsScanner.setInstalled(extensionKey); this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); - return local; + const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); + const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; + if (!localExtension) { + return null; + } + return this.extensionsScanner.updateMetadata(localExtension, { installedTimestamp: Date.now() }); } private async isUninstalled(extensionId: ExtensionKey): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 18bf8b92837..4bd05c9d29b 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -11,7 +11,7 @@ import { getIdAndVersion } from 'vs/platform/extensionManagement/common/extensio import { DidAddProfileExtensionsEvent, DidRemoveProfileExtensionsEvent, IExtensionsProfileScannerService, ProfileExtensionsEvent } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { ExtensionIdentifier, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtension, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -106,18 +106,34 @@ export class ExtensionsWatcher extends Disposable { } private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise { - let hasToUninstallExtensions = false; + const extensionsToUninstall: IExtension[] = []; + const promises: Promise[] = []; for (const extension of e.extensions) { const key = this.getKey(extension.identifier, extension.version); if (e.error) { this.addExtensionWithKey(key, e.profileLocation); } else { this.removeExtensionWithKey(key, e.profileLocation); - hasToUninstallExtensions = hasToUninstallExtensions || !this.allExtensions.has(key); + if (!this.allExtensions.has(key)) { + this.logService.debug('Extension is removed from all profiles', extension.identifier.id, extension.version); + promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location) + .then(result => { + if (result) { + extensionsToUninstall.push(result); + } else { + this.logService.info('Extension not found at the location', extension.location.toString()); + } + }, error => this.logService.error(error))); + } } } - if (hasToUninstallExtensions) { - await this.uninstallExtensionsNotInProfiles(); + try { + await Promise.all(promises); + if (extensionsToUninstall.length) { + await this.uninstallExtensionsNotInProfiles(extensionsToUninstall); + } + } catch (error) { + this.logService.error(error); } } @@ -175,9 +191,11 @@ export class ExtensionsWatcher extends Disposable { await this.uninstallExtensionsNotInProfiles(); } - private async uninstallExtensionsNotInProfiles(): Promise { - const installed = await this.extensionManagementService.getAllUserInstalled(); - const toUninstall = installed.filter(installedExtension => installedExtension.installedTimestamp /* Installed by VS Code */ && !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + private async uninstallExtensionsNotInProfiles(toUninstall?: IExtension[]): Promise { + if (!toUninstall) { + const installed = await this.extensionManagementService.scanAllUserInstalledExtensions(); + toUninstall = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + } if (toUninstall.length) { await this.extensionManagementService.markAsUninstalled(...toUninstall); }