Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/node/extensionLifecycle.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ChildProcess, fork } from 'child_process';
7
import { Limiter } from '../../../base/common/async.js';
8
import { toErrorMessage } from '../../../base/common/errorMessage.js';
9
import { Event } from '../../../base/common/event.js';
10
import { Disposable } from '../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../base/common/network.js';
12
import { join } from '../../../base/common/path.js';
13
import { Promises } from '../../../base/node/pfs.js';
14
import { ILocalExtension } from '../common/extensionManagement.js';
15
import { ILogService } from '../../log/common/log.js';
16
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
17
18
export class ExtensionsLifecycle extends Disposable {
19
20
private processesLimiter: Limiter<void> = new Limiter(5); // Run max 5 processes in parallel
21
22
constructor(
23
@IUserDataProfilesService private userDataProfilesService: IUserDataProfilesService,
24
@ILogService private readonly logService: ILogService
25
) {
26
super();
27
}
28
29
async postUninstall(extension: ILocalExtension): Promise<void> {
30
const script = this.parseScript(extension, 'uninstall');
31
if (script) {
32
this.logService.info(extension.identifier.id, extension.manifest.version, `Running post uninstall script`);
33
await this.processesLimiter.queue(async () => {
34
try {
35
await this.runLifecycleHook(script.script, 'uninstall', script.args, true, extension);
36
this.logService.info(`Finished running post uninstall script`, extension.identifier.id, extension.manifest.version);
37
} catch (error) {
38
this.logService.error('Failed to run post uninstall script', extension.identifier.id, extension.manifest.version);
39
this.logService.error(error);
40
}
41
});
42
}
43
try {
44
await Promises.rm(this.getExtensionStoragePath(extension));
45
} catch (error) {
46
this.logService.error('Error while removing extension storage path', extension.identifier.id);
47
this.logService.error(error);
48
}
49
}
50
51
private parseScript(extension: ILocalExtension, type: string): { script: string; args: string[] } | null {
52
const scriptKey = `vscode:${type}`;
53
if (extension.location.scheme === Schemas.file && extension.manifest && extension.manifest['scripts'] && typeof extension.manifest['scripts'][scriptKey] === 'string') {
54
const script = (<string>extension.manifest['scripts'][scriptKey]).split(' ');
55
if (script.length < 2 || script[0] !== 'node' || !script[1]) {
56
this.logService.warn(extension.identifier.id, extension.manifest.version, `${scriptKey} should be a node script`);
57
return null;
58
}
59
return { script: join(extension.location.fsPath, script[1]), args: script.slice(2) || [] };
60
}
61
return null;
62
}
63
64
private runLifecycleHook(lifecycleHook: string, lifecycleType: string, args: string[], timeout: boolean, extension: ILocalExtension): Promise<void> {
65
return new Promise<void>((c, e) => {
66
67
const extensionLifecycleProcess = this.start(lifecycleHook, lifecycleType, args, extension);
68
let timeoutHandler: Timeout | null;
69
70
const onexit = (error?: string) => {
71
if (timeoutHandler) {
72
clearTimeout(timeoutHandler);
73
timeoutHandler = null;
74
}
75
if (error) {
76
e(error);
77
} else {
78
c(undefined);
79
}
80
};
81
82
// on error
83
extensionLifecycleProcess.on('error', (err) => {
84
onexit(toErrorMessage(err) || 'Unknown');
85
});
86
87
// on exit
88
extensionLifecycleProcess.on('exit', (code: number, signal: string) => {
89
onexit(code ? `post-${lifecycleType} process exited with code ${code}` : undefined);
90
});
91
92
if (timeout) {
93
// timeout: kill process after waiting for 5s
94
timeoutHandler = setTimeout(() => {
95
timeoutHandler = null;
96
extensionLifecycleProcess.kill();
97
e('timed out');
98
}, 5000);
99
}
100
});
101
}
102
103
private start(uninstallHook: string, lifecycleType: string, args: string[], extension: ILocalExtension): ChildProcess {
104
const opts = {
105
silent: true,
106
execArgv: undefined
107
};
108
const extensionUninstallProcess = fork(uninstallHook, [`--type=extension-post-${lifecycleType}`, ...args], opts);
109
110
// Catch all output coming from the process
111
type Output = { data: string; format: string[] };
112
extensionUninstallProcess.stdout!.setEncoding('utf8');
113
extensionUninstallProcess.stderr!.setEncoding('utf8');
114
115
const onStdout = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stdout!, 'data');
116
const onStderr = Event.fromNodeEventEmitter<string>(extensionUninstallProcess.stderr!, 'data');
117
118
// Log output
119
this._register(onStdout(data => this.logService.info(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)));
120
this._register(onStderr(data => this.logService.error(extension.identifier.id, extension.manifest.version, `post-${lifecycleType}`, data)));
121
122
const onOutput = Event.any(
123
Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] }), this._store),
124
Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }), this._store)
125
);
126
// Debounce all output, so we can render it in the Chrome console as a group
127
const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {
128
return r
129
? { data: r.data + o.data, format: [...r.format, ...o.format] }
130
: { data: o.data, format: o.format };
131
}, 100, undefined, undefined, undefined, this._store);
132
133
// Print out output
134
onDebouncedOutput(data => {
135
console.group(extension.identifier.id);
136
console.log(data.data, ...data.format);
137
console.groupEnd();
138
});
139
140
return extensionUninstallProcess;
141
}
142
143
private getExtensionStoragePath(extension: ILocalExtension): string {
144
return join(this.userDataProfilesService.defaultProfile.globalStorageHome.fsPath, extension.identifier.id.toLowerCase());
145
}
146
}
147
148