From cf2ebd91b8e42e3bc5ab0e85e3323c886a977ffe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 18 Dec 2024 07:45:55 +0100 Subject: [PATCH] Revert "debt: clean up obsolete file usage" (#236433) Revert "debt: clean up obsolete file usage (#236379)" This reverts commit 625bae23758002b62954fd10b53d25409421d718. --- .../common/extensionManagement.ts | 4 +- .../common/extensionsScannerService.ts | 51 ++++-- .../node/extensionManagementService.ts | 163 +++++++++--------- .../node/extensionsWatcher.ts | 20 +-- .../node/extensionsScannerService.test.ts | 33 +++- 5 files changed, 156 insertions(+), 115 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f08b46ae65b30..155079831fcc7 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -456,8 +456,8 @@ export const enum ExtensionManagementErrorCode { Extract = 'Extract', Scanning = 'Scanning', ScanningExtension = 'ScanningExtension', - ReadRemoved = 'ReadRemoved', - UnsetRemoved = 'UnsetRemoved', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', IntializeDefaultProfile = 'IntializeDefaultProfile', diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index a186b6fa04567..99868cddb5d63 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -7,6 +7,7 @@ import { coalesce } from '../../../base/common/arrays.js'; import { ThrottledDelayer } from '../../../base/common/async.js'; import * as objects from '../../../base/common/objects.js'; import { VSBuffer } from '../../../base/common/buffer.js'; +import { IStringDictionary } from '../../../base/common/collections.js'; import { getErrorMessage } from '../../../base/common/errors.js'; import { getNodeType, parse, ParseError } from '../../../base/common/json.js'; import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js'; @@ -17,11 +18,12 @@ import * as platform from '../../../base/common/platform.js'; import { basename, isEqual, joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; import Severity from '../../../base/common/severity.js'; +import { isEmptyObject } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { IProductVersion, Metadata } from './extensionManagement.js'; -import { areSameExtensions, computeTargetPlatform, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from './extensionManagementUtil.js'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap, parseEnabledApiProposalNames } from '../../extensions/common/extensions.js'; import { validateExtensionManifest } from '../../extensions/common/extensionValidator.js'; import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js'; @@ -104,6 +106,7 @@ export type ScanOptions = { readonly profileLocation?: URI; readonly includeInvalid?: boolean; readonly includeAllVersions?: boolean; + readonly includeUninstalled?: boolean; readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; @@ -142,9 +145,10 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private readonly _onDidChangeCache = this._register(new Emitter()); readonly onDidChangeCache = this._onDidChangeCache.event; - private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile)); - private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner)); + private readonly obsoleteFile = joinPath(this.userExtensionsLocation, '.obsolete'); + private readonly systemExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); + private readonly userExtensionsCachedScanner = this._register(this.instantiationService.createInstance(CachedExtensionsScanner, this.currentProfile, this.obsoleteFile)); + private readonly extensionsScanner = this._register(this.instantiationService.createInstance(ExtensionsScanner, this.obsoleteFile)); constructor( readonly systemExtensionsLocation: URI, @@ -195,8 +199,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); - const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode ? this.userExtensionsCachedScanner : this.extensionsScanner; + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); @@ -217,7 +221,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +237,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,7 +249,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -401,7 +405,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, language, true, undefined, this.getProductVersion()); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -431,7 +435,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, language, true, undefined, this.getProductVersion()))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -445,7 +449,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -458,6 +462,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem profile, profileScanOptions, type, + excludeObsolete, validate, productVersion.version, productVersion.date, @@ -499,6 +504,7 @@ export class ExtensionScannerInput { public readonly profile: boolean, public readonly profileScanOptions: IProfileExtensionsScanOptions | undefined, public readonly type: ExtensionType, + public readonly excludeObsolete: boolean, public readonly validate: boolean, public readonly productVersion: string, public readonly productDate: string | undefined, @@ -528,6 +534,7 @@ export class ExtensionScannerInput { && a.profile === b.profile && objects.equals(a.profileScanOptions, b.profileScanOptions) && a.type === b.type + && a.excludeObsolete === b.excludeObsolete && a.validate === b.validate && a.productVersion === b.productVersion && a.productDate === b.productDate @@ -551,6 +558,7 @@ class ExtensionsScanner extends Disposable { private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( + private readonly obsoleteFile: URI, @IExtensionsProfileScannerService protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @IFileService protected readonly fileService: IFileService, @@ -563,9 +571,15 @@ class ExtensionsScanner extends Disposable { } async scanExtensions(input: ExtensionScannerInput): Promise { - return input.profile - ? this.scanExtensionsFromProfile(input) - : this.scanExtensionsFromLocation(input); + const extensions = input.profile ? await this.scanExtensionsFromProfile(input) : await this.scanExtensionsFromLocation(input); + let obsolete: IStringDictionary = {}; + if (input.excludeObsolete && input.type === ExtensionType.User) { + try { + const raw = (await this.fileService.readFile(this.obsoleteFile)).value.toString(); + obsolete = JSON.parse(raw); + } catch (error) { /* ignore */ } + } + return isEmptyObject(obsolete) ? extensions : extensions.filter(e => !obsolete[ExtensionKey.create(e).toString()]); } private async scanExtensionsFromLocation(input: ExtensionScannerInput): Promise { @@ -582,7 +596,7 @@ class ExtensionsScanner extends Disposable { if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { return null; } - const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput); })); return coalesce(extensions) @@ -608,7 +622,7 @@ class ExtensionsScanner extends Disposable { const extensions = await Promise.all( scannedProfileExtensions.map(async extensionInfo => { if (filter(extensionInfo)) { - const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extensionScannerInput = new ExtensionScannerInput(extensionInfo.location, input.mtime, input.applicationExtensionslocation, input.applicationExtensionslocationMtime, input.profile, input.profileScanOptions, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); return this.scanExtension(extensionScannerInput, extensionInfo.metadata); } return null; @@ -877,6 +891,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { constructor( private readonly currentProfile: IUserDataProfile, + obsoleteFile: URI, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IExtensionsProfileScannerService extensionsProfileScannerService: IExtensionsProfileScannerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -885,7 +900,7 @@ class CachedExtensionsScanner extends ExtensionsScanner { @IEnvironmentService environmentService: IEnvironmentService, @ILogService logService: ILogService ) { - super(extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); + super(obsoleteFile, extensionsProfileScannerService, uriIdentityService, fileService, productService, environmentService, logService); } override async scanExtensions(input: ExtensionScannerInput): Promise { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 015c3dff393a0..92405eefb753c 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -60,7 +60,7 @@ export interface INativeServerExtensionManagementService extends IExtensionManag readonly _serviceBrand: undefined; scanAllUserInstalledExtensions(): Promise; scanInstalledExtensionAtLocation(location: URI): Promise; - deleteExtensions(...extensions: IExtension[]): Promise; + markAsUninstalled(...extensions: IExtension[]): Promise; } type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionSignatureVerificationCode }; @@ -222,8 +222,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } - deleteExtensions(...extensions: IExtension[]): Promise { - return this.extensionsScanner.setExtensionsForRemoval(...extensions); + markAsUninstalled(...extensions: IExtension[]): Promise { + return this.extensionsScanner.setUninstalled(...extensions); } async cleanUp(): Promise { @@ -480,20 +480,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi continue; } - // Ignore changes to the deleted folder - if (this.uriIdentityService.extUri.basename(resource).endsWith(DELETED_FOLDER_POSTFIX)) { - continue; - } - - try { - // Check if this is a directory - if (!(await this.fileService.stat(resource)).isDirectory) { - continue; - } - } catch (error) { - if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { - this.logService.error(error); - } + // Check if this is a directory + if (!(await this.fileService.stat(resource)).isDirectory) { continue; } @@ -514,10 +502,23 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise { const localExtensions = extensions.map(e => e[0]); - await this.extensionsScanner.unsetExtensionsForRemoval(...localExtensions.map(extension => ExtensionKey.create(extension))); + await this.setInstalled(localExtensions); await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation); this._onDidInstallExtensions.fire(localExtensions.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); + } + } } type UpdateMetadataErrorClassification = { @@ -535,8 +536,8 @@ type UpdateMetadataErrorEvent = { export class ExtensionsScanner extends Disposable { - private readonly obsoletedResource: URI; - private readonly obsoleteFileLimiter: Queue; + private readonly uninstalledResource: URI; + private readonly uninstalledFileLimiter: Queue; private readonly _onExtract = this._register(new Emitter()); readonly onExtract = this._onExtract.event; @@ -554,13 +555,13 @@ export class ExtensionsScanner extends Disposable { @ILogService private readonly logService: ILogService, ) { super(); - this.obsoletedResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); - this.obsoleteFileLimiter = new Queue(); + this.uninstalledResource = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete'); + this.uninstalledFileLimiter = new Queue(); } async cleanUp(): Promise { await this.removeTemporarilyDeletedFolders(); - await this.deleteExtensionsMarkedForRemoval(); + await this.removeUninstalledExtensions(); await this.initializeMetadata(); } @@ -719,38 +720,40 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(local.location, local.type, profileLocation); } - async setExtensionsForRemoval(...extensions: IExtension[]): Promise { + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } + } + + async setUninstalled(...extensions: IExtension[]): Promise { const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withRemovedExtensions(removedExtensions => + await this.withUninstalledExtensions(uninstalled => extensionKeys.forEach(extensionKey => { - removedExtensions[extensionKey.toString()] = true; - this.logService.info('Marked extension as removed', extensionKey.toString()); + uninstalled[extensionKey.toString()] = true; + this.logService.info('Marked extension as uninstalled', extensionKey.toString()); })); } - async unsetExtensionsForRemoval(...extensionKeys: ExtensionKey[]): Promise { + async setInstalled(extensionKey: ExtensionKey): Promise { try { - const results: boolean[] = []; - await this.withRemovedExtensions(removedExtensions => - extensionKeys.forEach(extensionKey => { - if (removedExtensions[extensionKey.toString()]) { - results.push(true); - delete removedExtensions[extensionKey.toString()]; - } else { - results.push(false); - } - })); - return results; + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetRemoved); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); } } - async deleteExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { if (this.uriIdentityService.extUri.isEqualOrParent(extension.location, this.extensionsScannerService.userExtensionsLocation)) { return this.deleteExtensionFromLocation(extension.identifier.id, extension.location, type); } - await this.unsetExtensionsForRemoval(ExtensionKey.create(extension)); + } + + async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); } async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial): Promise { @@ -789,11 +792,11 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private withRemovedExtensions(updateFn?: (removed: IStringDictionary) => void): Promise> { - return this.obsoleteFileLimiter.queue(async () => { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { - const content = await this.fileService.readFile(this.obsoletedResource, 'utf8'); + const content = await this.fileService.readFile(this.uninstalledResource, 'utf8'); raw = content.value.toString(); } catch (error) { if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { @@ -801,23 +804,23 @@ export class ExtensionsScanner extends Disposable { } } - let removed = {}; + let uninstalled = {}; if (raw) { try { - removed = JSON.parse(raw); + uninstalled = JSON.parse(raw); } catch (e) { /* ignore */ } } if (updateFn) { - updateFn(removed); - if (Object.keys(removed).length) { - await this.fileService.writeFile(this.obsoletedResource, VSBuffer.fromString(JSON.stringify(removed))); + updateFn(uninstalled); + if (Object.keys(uninstalled).length) { + await this.fileService.writeFile(this.uninstalledResource, VSBuffer.fromString(JSON.stringify(uninstalled))); } else { - await this.fileService.del(this.obsoletedResource); + await this.fileService.del(this.uninstalledResource); } } - return removed; + return uninstalled; }); } @@ -895,25 +898,19 @@ export class ExtensionsScanner extends Disposable { })); } - private async deleteExtensionsMarkedForRemoval(): Promise { - let removed: IStringDictionary; - try { - removed = await this.withRemovedExtensions(); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadRemoved); - } - - if (Object.keys(removed).length === 0) { - this.logService.debug(`No extensions are marked as removed.`); + private async removeUninstalledExtensions(): Promise { + const uninstalled = await this.getUninstalledExtensions(); + if (Object.keys(uninstalled).length === 0) { + this.logService.debug(`No uninstalled extensions found.`); return; } - this.logService.debug(`Deleting extensions marked as removed:`, Object.keys(removed)); + this.logService.debug(`Removing uninstalled extensions:`, Object.keys(uninstalled)); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeInvalid: true }); // All user extensions + 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) { - if (!removed[ExtensionKey.create(e).toString()]) { + if (!uninstalled[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } @@ -931,8 +928,8 @@ export class ExtensionsScanner extends Disposable { this.logService.error(error); } - const toRemove = extensions.filter(e => e.metadata /* Installed by System */ && removed[ExtensionKey.create(e).toString()]); - await Promise.allSettled(toRemove.map(e => this.deleteExtension(e, 'marked for removal'))); + 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 async removeTemporarilyDeletedFolders(): Promise { @@ -1024,7 +1021,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask { - // If the same version of extension is marked as removed, remove it from there and return the local. - const [removed] = await this.extensionsScanner.unsetExtensionsForRemoval(extensionKey); - if (removed) { - this.logService.info('Removed the extension from removed list:', extensionKey.id); - const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); - return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); + private async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { + return undefined; } - return undefined; + + 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. + await this.extensionsScanner.setInstalled(extensionKey); + this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); + + const userExtensions = await this.extensionsScanner.scanAllUserExtensions(true); + return userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)); } private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { @@ -1148,8 +1149,8 @@ class UninstallExtensionInProfileTask extends AbstractExtensionTask implem super(); } - protected doRun(token: CancellationToken): Promise { - return this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); + protected async doRun(token: CancellationToken): Promise { + await this.extensionsProfileScannerService.removeExtensionFromProfile(this.extension, this.options.profileLocation); } } diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 2c4e976a5a648..d2b65eaea553c 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -48,7 +48,7 @@ export class ExtensionsWatcher extends Disposable { await this.extensionsScannerService.initializeDefaultProfileExtensions(); await this.onDidChangeProfiles(this.userDataProfilesService.profiles); this.registerListeners(); - await this.deleteExtensionsNotInProfiles(); + await this.uninstallExtensionsNotInProfiles(); } private registerListeners(): void { @@ -102,7 +102,7 @@ export class ExtensionsWatcher extends Disposable { } private async onDidRemoveExtensions(e: DidRemoveProfileExtensionsEvent): Promise { - const extensionsToDelete: IExtension[] = []; + const extensionsToUninstall: IExtension[] = []; const promises: Promise[] = []; for (const extension of e.extensions) { const key = this.getKey(extension.identifier, extension.version); @@ -115,7 +115,7 @@ export class ExtensionsWatcher extends Disposable { promises.push(this.extensionManagementService.scanInstalledExtensionAtLocation(extension.location) .then(result => { if (result) { - extensionsToDelete.push(result); + extensionsToUninstall.push(result); } else { this.logService.info('Extension not found at the location', extension.location.toString()); } @@ -125,8 +125,8 @@ export class ExtensionsWatcher extends Disposable { } try { await Promise.all(promises); - if (extensionsToDelete.length) { - await this.deleteExtensionsNotInProfiles(extensionsToDelete); + if (extensionsToUninstall.length) { + await this.uninstallExtensionsNotInProfiles(extensionsToUninstall); } } catch (error) { this.logService.error(error); @@ -180,13 +180,13 @@ export class ExtensionsWatcher extends Disposable { } } - private async deleteExtensionsNotInProfiles(toDelete?: IExtension[]): Promise { - if (!toDelete) { + private async uninstallExtensionsNotInProfiles(toUninstall?: IExtension[]): Promise { + if (!toUninstall) { const installed = await this.extensionManagementService.scanAllUserInstalledExtensions(); - toDelete = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); + toUninstall = installed.filter(installedExtension => !this.allExtensions.has(this.getKey(installedExtension.identifier, installedExtension.manifest.version))); } - if (toDelete.length) { - await this.extensionManagementService.deleteExtensions(...toDelete); + if (toUninstall.length) { + await this.extensionManagementService.markAsUninstalled(...toUninstall); } } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts index 551ba576d4459..74d3ffcd738f8 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -224,6 +224,31 @@ suite('NativeExtensionsScanerService Test', () => { assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); }); + test('scan exclude uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan include uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = disposables.add(instantiationService.createInstance(ExtensionsScannerService)); + + const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + test('scan include invalid extensions', async () => { await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); @@ -326,7 +351,7 @@ suite('ExtensionScannerInput', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('compare inputs - location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(location, mtime, undefined, undefined, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -336,7 +361,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - application location', () => { - const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (location: URI, mtime: number | undefined) => new ExtensionScannerInput(ROOT, undefined, location, mtime, false, undefined, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, undefined), anInput(ROOT, undefined)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ROOT, 100), anInput(ROOT, 100)), true); @@ -346,7 +371,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - profile', () => { - const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (profile: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, profile, profileScanOptions, ExtensionType.User, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(true, { bailOutWhenFileNotFound: true }), anInput(true, { bailOutWhenFileNotFound: true })), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(false, { bailOutWhenFileNotFound: true }), anInput(false, { bailOutWhenFileNotFound: true })), true); @@ -359,7 +384,7 @@ suite('ExtensionScannerInput', () => { }); test('compare inputs - extension type', () => { - const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, '1.1.1', undefined, undefined, true, undefined, {}); + const anInput = (type: ExtensionType) => new ExtensionScannerInput(ROOT, undefined, undefined, undefined, false, undefined, type, true, true, '1.1.1', undefined, undefined, true, undefined, {}); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.System), anInput(ExtensionType.System)), true); assert.strictEqual(ExtensionScannerInput.equals(anInput(ExtensionType.User), anInput(ExtensionType.User)), true);