diff --git a/src/vs/workbench/node/extensionPoints.ts b/src/vs/workbench/node/extensionPoints.ts index 0e922acbd99..74eb2199663 100644 --- a/src/vs/workbench/node/extensionPoints.ts +++ b/src/vs/workbench/node/extensionPoints.ts @@ -101,17 +101,29 @@ export class PluginScanner { isBuiltin:boolean ) : TPromise { - return pfs.readDirsInDir(absoluteFolderPath) - .then(folders => TPromise.join(folders.map(f => this.scanPlugin(version, collector, paths.join(absoluteFolderPath, f), isBuiltin)))) - .then(plugins => plugins.filter(item => item !== null)) - .then(plugins => { - const pluginsById = values(groupBy(plugins, p => p.id)); - return pluginsById.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); - }) - .then(null, err => { - collector.error(absoluteFolderPath, err); - return []; - }); + let obsolete = TPromise.as({}); + + if (!isBuiltin) { + obsolete = pfs.readFile(paths.join(absoluteFolderPath, '.obsolete'), 'utf8') + .then(raw => JSON.parse(raw)) + .then(null, err => ({})); + } + + return obsolete.then(obsolete => { + return pfs.readDirsInDir(absoluteFolderPath) + .then(folders => TPromise.join(folders.map(f => this.scanPlugin(version, collector, paths.join(absoluteFolderPath, f), isBuiltin)))) + .then(plugins => plugins.filter(item => item !== null)) + // TODO: align with extensionsService + .then(plugins => plugins.filter(p => !obsolete[`${ p.publisher }.${ p.name }-${ p.version }`])) + .then(plugins => { + const pluginsById = values(groupBy(plugins, p => p.id)); + return pluginsById.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); + }) + .then(null, err => { + collector.error(absoluteFolderPath, err); + return []; + }); + }); } /** diff --git a/src/vs/workbench/parts/extensions/node/extensionsService.ts b/src/vs/workbench/parts/extensions/node/extensionsService.ts index c4a65221686..f1bf06719bc 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsService.ts @@ -78,11 +78,17 @@ function createExtension(manifest: IExtensionManifest, galleryInformation?: IGal return extension; } +function getExtensionId(extension: IExtension): string { + return `${ extension.publisher }.${ extension.name }-${ extension.version }`; +} + export class ExtensionsService implements IExtensionsService { public serviceId = IExtensionsService; private extensionsPath: string; + private obsoletePath: string; + private obsoleteFileLimiter: Limiter; private _onInstallExtension = new Emitter(); @ServiceEvent onInstallExtension = this._onInstallExtension.event; @@ -101,6 +107,8 @@ export class ExtensionsService implements IExtensionsService { ) { const env = contextService.getConfiguration().env; this.extensionsPath = env.userPluginsHome; + this.obsoletePath = path.join(this.extensionsPath, '.obsolete'); + this.obsoleteFileLimiter = new Limiter(1); } public install(extension: IExtension): TPromise; @@ -122,7 +130,7 @@ export class ExtensionsService implements IExtensionsService { const url = galleryInformation.downloadUrl; const zipPath = path.join(tmpdir(), galleryInformation.id); - const extensionPath = path.join(this.extensionsPath, `${ extension.publisher }.${ extension.name }-${ extension.version }`); + const extensionPath = path.join(this.extensionsPath, getExtensionId(extension)); const manifestPath = path.join(extensionPath, 'package.json'); const settings = TPromise.join([ @@ -149,7 +157,7 @@ export class ExtensionsService implements IExtensionsService { private installFromZip(zipPath: string): TPromise { return validate(zipPath).then(manifest => { - const extensionPath = path.join(this.extensionsPath, `${ manifest.publisher }.${ manifest.name }-${ manifest.version }`); + const extensionPath = path.join(this.extensionsPath, getExtensionId(manifest)); this._onInstallExtension.fire(manifest); return extract(zipPath, extensionPath, { sourcePath: 'extension', overwrite: true }) @@ -159,12 +167,14 @@ export class ExtensionsService implements IExtensionsService { } public uninstall(extension: IExtension): TPromise { - const extensionPath = this.getInstallationPath(extension); + const extensionPath = extension.path || path.join(this.extensionsPath, getExtensionId(extension)); return pfs.exists(extensionPath) .then(exists => exists ? null : Promise.wrapError(new Error(nls.localize('notExists', "Could not find extension")))) .then(() => this._onUninstallExtension.fire(extension)) + .then(() => this.setObsolete(extension)) .then(() => pfs.rimraf(extensionPath)) + .then(() => this.unsetObsolete(extension)) .then(() => this._onDidUninstallExtension.fire(extension)); } @@ -181,36 +191,85 @@ export class ExtensionsService implements IExtensionsService { }); } - private getDeprecated(): TPromise { - return this.getAllInstalled().then(plugins => { - const byId = values(groupBy(plugins, p => `${ p.publisher }.${ p.name }`)); - return flatten(byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version)).slice(1))); - }); - } - private getAllInstalled(): TPromise { const limiter = new Limiter(10); - return pfs.readdir(this.extensionsPath) - .then(extensions => Promise.join(extensions.map(e => { - const extensionPath = path.join(this.extensionsPath, e); + return this.getObsoleteExtensions() + .then(obsolete => { + return pfs.readdir(this.extensionsPath) + .then(extensions => extensions.filter(e => !obsolete[e])) + .then(extensions => Promise.join(extensions.map(e => { + const extensionPath = path.join(this.extensionsPath, e); - return limiter.queue( - () => pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') - .then(raw => parseManifest(raw)) - .then(manifest => createExtension(manifest, ( manifest).__metadata, extensionPath)) - .then(null, () => null) - ); - }))) - .then(result => result.filter(a => !!a)); + return limiter.queue( + () => pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') + .then(raw => parseManifest(raw)) + .then(manifest => createExtension(manifest, ( manifest).__metadata, extensionPath)) + .then(null, () => null) + ); + }))) + .then(result => result.filter(a => !!a)); + }); } - private getInstallationPath(extension: IExtension): string { - return extension.path || path.join(this.extensionsPath, `${ extension.publisher }.${ extension.name }-${ extension.version }`); + private getExtensionId(extension: IExtension): string { + return `${ extension.publisher }.${ extension.name }-${ extension.version }`; } public removeDeprecatedExtensions(): TPromise { - return this.getDeprecated() - .then(extensions => TPromise.join(extensions.filter(e => !!e.path).map(e => pfs.rimraf(e.path)))); - } + const outdated = this.getAllInstalled() + .then(plugins => { + const byId = values(groupBy(plugins, p => `${ p.publisher }.${ p.name }`)); + const extensions = flatten(byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version)).slice(1))); + + return extensions + .filter(e => !!e.path) + .map(e => getExtensionId(e)); + }); + + const obsolete = this.getObsoleteExtensions() + .then(obsolete => Object.keys(obsolete)); + + return TPromise.join([outdated, obsolete]) + .then(result => flatten(result)) + .then(extensionsIds => { + return TPromise.join(extensionsIds.map(id => { + return pfs.rimraf(path.join(this.extensionsPath, id)) + .then(() => this.doUpdateObsoleteExtensions(obsolete => delete obsolete[id])); + })); + }); + } + + private setObsolete(extension: IExtension): TPromise { + const id = getExtensionId(extension); + return this.doUpdateObsoleteExtensions(obsolete => assign(obsolete, { [id]: true })); + } + + private unsetObsolete(extension: IExtension): TPromise { + const id = getExtensionId(extension); + return this.doUpdateObsoleteExtensions(obsolete => delete obsolete[id]); + } + + private getObsoleteExtensions(): TPromise<{ [id:string]: boolean; }> { + return this.doUpdateObsoleteExtensions(obsolete => obsolete); + } + + private doUpdateObsoleteExtensions(fn: (obsolete: { [id:string]: boolean; }) => T): TPromise { + return this.obsoleteFileLimiter.queue(() => { + let result: T = null; + return pfs.readFile(this.obsoletePath, 'utf8') + .then(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err)) + .then<{ [id: string]: boolean }>(raw => JSON.parse(raw)) + .then(obsolete => { result = fn(obsolete); return obsolete; }) + .then(obsolete => { + if (Object.keys(obsolete).length === 0) { + return pfs.rimraf(this.obsoletePath); + } else { + const raw = JSON.stringify(obsolete); + return pfs.writeFile(this.obsoletePath, raw); + } + }) + .then(() => result); + }); + } }