提交 d7f719b1 编写于 作者: S Sandeep Somavarapu

Fix #35006

上级 87860160
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { TPromise } from 'vs/base/common/winjs.base';
import { ILogService } from 'vs/platform/log/common/log';
import { fork, ChildProcess } from 'child_process';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { join } from 'vs/base/common/paths';
import { Limiter } from 'vs/base/common/async';
import { fromNodeEventEmitter, anyEvent, mapEvent, debounceEvent } from 'vs/base/common/event';
export class ExtensionsLifecycle extends Disposable {
private processesLimiter: Limiter<void> = new Limiter(5); // Run max 5 processes in parallel
constructor(
@ILogService private logService: ILogService
) {
super();
}
uninstall(extension: ILocalExtension): TPromise<void> {
const uninstallScript = this.parseUninstallScript(extension);
if (uninstallScript) {
this.logService.info(extension.identifier.id, 'Running Uninstall hook');
return this.processesLimiter.queue(() =>
this.runUninstallHook(uninstallScript.uninstallHook, uninstallScript.args, extension)
.then(() => this.logService.info(extension.identifier.id, 'Finished running uninstall hook'), err => this.logService.error(extension.identifier.id, `Failed to run uninstall hook: ${err}`)));
}
return TPromise.as(null);
}
private parseUninstallScript(extension: ILocalExtension): { uninstallHook: string, args: string[] } {
if (extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts']['vscode:uninstall'] === 'string') {
const uninstallScript = (<string>extension.manifest['scripts']['vscode:uninstall']).split(' ');
if (uninstallScript.length < 2 || uninstallScript[0] !== 'node' || !uninstallScript[1]) {
this.logService.warn(extension.identifier.id, 'Uninstall script should be a node script');
return null;
}
return { uninstallHook: join(extension.path, uninstallScript[1]), args: uninstallScript.slice(2) || [] };
}
return null;
}
private runUninstallHook(lifecycleHook: string, args: string[], extension: ILocalExtension): TPromise<void> {
return new TPromise((c, e) => {
const extensionLifecycleProcess = this.start(lifecycleHook, args, extension);
let timeoutHandler;
const onexit = (error?: string) => {
clearTimeout(timeoutHandler);
timeoutHandler = null;
if (error) {
e(error);
} else {
c(null);
}
};
// on error
extensionLifecycleProcess.on('error', (err) => {
if (timeoutHandler) {
onexit(toErrorMessage(err) || 'Unknown');
}
});
// on exit
extensionLifecycleProcess.on('exit', (code: number, signal: string) => {
if (timeoutHandler) {
onexit(code ? `Process exited with code ${code}` : void 0);
}
});
// timeout: kill process after waiting for 5s
timeoutHandler = setTimeout(() => {
timeoutHandler = null;
extensionLifecycleProcess.kill();
e('timed out');
}, 5000);
});
}
private start(uninstallHook: string, args: string[], extension: ILocalExtension): ChildProcess {
const opts = {
silent: true,
execArgv: <string[]>undefined
};
const extensionUninstallProcess = fork(uninstallHook, ['--type=extensionUninstall', ...args], opts);
// Catch all output coming from the process
type Output = { data: string, format: string[] };
extensionUninstallProcess.stdout.setEncoding('utf8');
extensionUninstallProcess.stderr.setEncoding('utf8');
const onStdout = fromNodeEventEmitter<string>(extensionUninstallProcess.stdout, 'data');
const onStderr = fromNodeEventEmitter<string>(extensionUninstallProcess.stderr, 'data');
const onOutput = anyEvent(
mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
);
// Debounce all output, so we can render it in the Chrome console as a group
const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
return r
? { data: r.data + o.data, format: [...r.format, ...o.format] }
: { data: o.data, format: o.format };
}, 100);
// Print out extension host output
onDebouncedOutput(data => {
console.group(extension.identifier.id);
console.log(data.data, ...data.format);
console.groupEnd();
});
return extensionUninstallProcess;
}
}
......@@ -35,6 +35,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
......@@ -110,6 +111,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private lastReportTimestamp = 0;
private readonly installingExtensions: Map<string, TPromise<ILocalExtension>> = new Map<string, TPromise<ILocalExtension>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionLifecycle: ExtensionsLifecycle;
private readonly _onInstallExtension = new Emitter<InstallExtensionEvent>();
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
......@@ -135,6 +137,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.uninstalledFileLimiter = new Limiter(1);
this._register(toDisposable(() => this.installingExtensions.clear()));
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionLifecycle = this._register(new ExtensionsLifecycle(this.logService));
}
install(zipPath: string): TPromise<void> {
......@@ -708,24 +711,43 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
removeDeprecatedExtensions(): TPromise<any> {
return this.removeUninstalledExtensions()
.then(() => this.removeOutdatedExtensions());
}
private removeUninstalledExtensions(): TPromise<void> {
return this.getUninstalledExtensions()
.then(uninstalled => this.scanExtensions(this.extensionsPath, LocalExtensionType.User) // All user extensions
.then(extensions => {
const toRemove: { path: string, id: string }[] = [];
// Uninstalled extensions
toRemove.push(...extensions.filter(e => uninstalled[e.identifier.id]).map(e => ({ path: e.path, id: e.identifier.id })));
const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[e.identifier.id]);
return TPromise.join(toRemove.map(e => this.removeUninstalledExtension(e)));
})
).then(() => null);
}
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => ({ id: getGalleryExtensionIdFromLocal(e), uuid: e.identifier.uuid }));
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))
.map(e => ({ path: e.path, id: e.identifier.id })));
private removeOutdatedExtensions(): TPromise<void> {
return this.scanExtensions(this.extensionsPath, LocalExtensionType.User) // All user extensions
.then(extensions => {
const toRemove: ILocalExtension[] = [];
return TPromise.join(distinct(toRemove, e => e.path).map(({ path, id }) => {
return pfs.rimraf(path)
.then(() => this.withUninstalledExtensions(uninstalled => delete uninstalled[id]));
}));
})
);
// Outdated extensions
const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => ({ id: getGalleryExtensionIdFromLocal(e), uuid: e.identifier.uuid }));
toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1))));
return TPromise.join(toRemove.map(extension => this.removeExtension(extension)));
}).then(() => null);
}
private removeUninstalledExtension(extension: ILocalExtension): TPromise<void> {
return this.extensionLifecycle.uninstall(extension)
.then(() => this.removeExtension(extension))
.then(() => this.withUninstalledExtensions(uninstalled => delete uninstalled[extension.identifier.id]))
.then(() => null);
}
private removeExtension(extension: ILocalExtension): TPromise<void> {
this.logService.trace(extension.identifier.id, 'Deleting from disk');
return pfs.rimraf(extension.path).then(() => this.logService.info(extension.identifier.id, 'Deleted from disk'));
}
private isUninstalled(id: string): TPromise<boolean> {
......
......@@ -264,6 +264,10 @@ const schema: IJSONSchema = {
'vscode:prepublish': {
description: nls.localize('vscode.extension.scripts.prepublish', 'Script executed before the package is published as a VS Code extension.'),
type: 'string'
},
'vscode:uninstall': {
description: nls.localize('vscode.extension.scripts.uninstall', 'Script executed after the extension is uninstalled from VS Code. Only Node script is supported.'),
type: 'string'
}
}
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册