Skip to content

Commit

Permalink
Fix #35006
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Feb 21, 2018
1 parent 8786016 commit d7f719b
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 13 deletions.
123 changes: 123 additions & 0 deletions src/vs/platform/extensionManagement/node/extensionLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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> {
Expand Down Expand Up @@ -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> {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/platform/extensions/common/extensionsRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
},
Expand Down

0 comments on commit d7f719b

Please sign in to comment.