Path: blob/main/src/vs/platform/extensionManagement/node/extensionLifecycle.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ChildProcess, fork } from 'child_process';6import { Limiter } from '../../../base/common/async.js';7import { toErrorMessage } from '../../../base/common/errorMessage.js';8import { Event } from '../../../base/common/event.js';9import { Disposable } from '../../../base/common/lifecycle.js';10import { Schemas } from '../../../base/common/network.js';11import { join } from '../../../base/common/path.js';12import { Promises } from '../../../base/node/pfs.js';13import { ILocalExtension } from '../common/extensionManagement.js';14import { ILogService } from '../../log/common/log.js';15import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';1617export class ExtensionsLifecycle extends Disposable {1819private processesLimiter: Limiter<void> = new Limiter(5); // Run max 5 processes in parallel2021constructor(22@IUserDataProfilesService private userDataProfilesService: IUserDataProfilesService,23@ILogService private readonly logService: ILogService24) {25super();26}2728async postUninstall(extension: ILocalExtension): Promise<void> {29const script = this.parseScript(extension, 'uninstall');30if (script) {31this.logService.info(extension.identifier.id, extension.manifest.version, `Running post uninstall script`);32await this.processesLimiter.queue(async () => {33try {34await this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension);35this.logService.info(`Finished running post uninstall script`, extension.identifier.id, extension.manifest.version);36} catch (error) {37this.logService.error('Failed to run post uninstall script', extension.identifier.id, extension.manifest.version);38this.logService.error(error);39}40});41}42try {43await Promises.rm(this.getExtensionStoragePath(extension));44} catch (error) {45this.logService.error('Error while removing extension storage path', extension.identifier.id);46this.logService.error(error);47}48}4950private parseScript(extension: ILocalExtension, type: string): { script: string; args: string[] } | null {51const scriptKey = `vscode:${type}`;52if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts'][scriptKey] === 'string') {53const script = (<string>extension.manifest['scripts'][scriptKey]).split(' ');54if (script.length < 2 || script[0] !== 'node' || !script[1]) {55this.logService.warn(extension.identifier.id, extension.manifest.version, `${scriptKey} should be a node script`);56return null;57}58return { script: join(extension.location.fsPath, script[1]), args: script.slice(2) || [] };59}60return null;61}6263private runLifecycleHook(lifecycleHook: string, lifecycleType: string, args: string[], timeout: boolean, extension: ILocalExtension): Promise<void> {64return new Promise<void>((c, e) => {6566const extensionLifecycleProcess = this.start(lifecycleHook, lifecycleType, args, extension);67let timeoutHandler: Timeout | null;6869const onexit = (error?: string) => {70if (timeoutHandler) {71clearTimeout(timeoutHandler);72timeoutHandler = null;73}74if (error) {75e(error);76} else {77c(undefined);78}79};8081// on error82extensionLifecycleProcess.on('error', (err) => {83onexit(toErrorMessage(err) || 'Unknown');84});8586// on exit87extensionLifecycleProcess.on('exit', (code: number, signal: string) => {88onexit(code ? `post-${lifecycleType} process exited with code ${code}` : undefined);89});9091if (timeout) {92// timeout: kill process after waiting for 5s93timeoutHandler = setTimeout(() => {94timeoutHandler = null;95extensionLifecycleProcess.kill();96e('timed out');97}, 5000);98}99});100}101102private start(uninstallHook: string, lifecycleType: string, args: string[], extension: ILocalExtension): ChildProcess {103const opts = {104silent: true,105execArgv: undefined106};107const extensionUninstallProcess = fork(uninstallHook, [`--type=extension-post-${lifecycleType}`, ...args], opts);108109// Catch all output coming from the process110type Output = { data: string; format: string[] };111extensionUninstallProcess.stdout!.setEncoding('utf8');112extensionUninstallProcess.stderr!.setEncoding('utf8');113114const onStdout = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stdout!, 'data');115const onStderr = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stderr!, 'data');116117// Log output118this._register(onStdout(data => this.logService.info(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)));119this._register(onStderr(data => this.logService.error(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)));120121const onOutput = Event.any(122Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] }), this._store),123Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }), this._store)124);125// Debounce all output, so we can render it in the Chrome console as a group126const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {127return r128? { data: r.data + o.data, format: [...r.format, ...o.format] }129: { data: o.data, format: o.format };130}, 100, undefined, undefined, undefined, this._store);131132// Print out output133onDebouncedOutput(data => {134console.group(extension.identifier.id);135console.log(data.data, ...data.format);136console.groupEnd();137});138139return extensionUninstallProcess;140}141142private getExtensionStoragePath(extension: ILocalExtension): string {143return join(this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath, extension.identifier.id.toLowerCase());144}145}146147148