Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/parts/ipc/node/ipc.cp.ts
4780 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, ForkOptions } from 'child_process';
7
import { createCancelablePromise, Delayer } from '../../../common/async.js';
8
import { VSBuffer } from '../../../common/buffer.js';
9
import { CancellationToken } from '../../../common/cancellation.js';
10
import { isRemoteConsoleLog, log } from '../../../common/console.js';
11
import * as errors from '../../../common/errors.js';
12
import { Emitter, Event } from '../../../common/event.js';
13
import { dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js';
14
import { deepClone } from '../../../common/objects.js';
15
import { createQueuedSender } from '../../../node/processes.js';
16
import { removeDangerousEnvVariables } from '../../../common/processes.js';
17
import { ChannelClient as IPCClient, ChannelServer as IPCServer, IChannel, IChannelClient } from '../common/ipc.js';
18
19
/**
20
* This implementation doesn't perform well since it uses base64 encoding for buffers.
21
* We should move all implementations to use named ipc.net, so we stop depending on cp.fork.
22
*/
23
24
export class Server<TContext extends string> extends IPCServer<TContext> {
25
constructor(ctx: TContext) {
26
super({
27
send: r => {
28
try {
29
process.send?.((<Buffer>r.buffer).toString('base64'));
30
} catch (e) { /* not much to do */ }
31
},
32
onMessage: Event.fromNodeEventEmitter(process, 'message', msg => VSBuffer.wrap(Buffer.from(msg, 'base64')))
33
}, ctx);
34
35
process.once('disconnect', () => this.dispose());
36
}
37
}
38
39
export interface IIPCOptions {
40
41
/**
42
* A descriptive name for the server this connection is to. Used in logging.
43
*/
44
serverName: string;
45
46
/**
47
* Time in millies before killing the ipc process. The next request after killing will start it again.
48
*/
49
timeout?: number;
50
51
/**
52
* Arguments to the module to execute.
53
*/
54
args?: string[];
55
56
/**
57
* Environment key-value pairs to be passed to the process that gets spawned for the ipc.
58
*/
59
env?: any;
60
61
/**
62
* Allows to assign a debug port for debugging the application executed.
63
*/
64
debug?: number;
65
66
/**
67
* Allows to assign a debug port for debugging the application and breaking it on the first line.
68
*/
69
debugBrk?: number;
70
71
/**
72
* If set, starts the fork with empty execArgv. If not set, execArgv from the parent process are inherited,
73
* except --inspect= and --inspect-brk= which are filtered as they would result in a port conflict.
74
*/
75
freshExecArgv?: boolean;
76
77
/**
78
* Enables our createQueuedSender helper for this Client. Uses a queue when the internal Node.js queue is
79
* full of messages - see notes on that method.
80
*/
81
useQueue?: boolean;
82
}
83
84
export class Client implements IChannelClient, IDisposable {
85
86
private disposeDelayer: Delayer<void> | undefined;
87
private activeRequests = new Set<IDisposable>();
88
private child: ChildProcess | null;
89
private _client: IPCClient | null;
90
private channels = new Map<string, IChannel>();
91
92
private readonly _onDidProcessExit = new Emitter<{ code: number; signal: string }>();
93
readonly onDidProcessExit = this._onDidProcessExit.event;
94
95
constructor(private modulePath: string, private options: IIPCOptions) {
96
const timeout = options.timeout || 60000;
97
this.disposeDelayer = new Delayer<void>(timeout);
98
this.child = null;
99
this._client = null;
100
}
101
102
getChannel<T extends IChannel>(channelName: string): T {
103
const that = this;
104
105
// eslint-disable-next-line local/code-no-dangerous-type-assertions
106
return {
107
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
108
return that.requestPromise<T>(channelName, command, arg, cancellationToken);
109
},
110
listen(event: string, arg?: any) {
111
return that.requestEvent(channelName, event, arg);
112
}
113
} as T;
114
}
115
116
protected requestPromise<T>(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<T> {
117
if (!this.disposeDelayer) {
118
return Promise.reject(new Error('disposed'));
119
}
120
121
if (cancellationToken.isCancellationRequested) {
122
return Promise.reject(errors.canceled());
123
}
124
125
this.disposeDelayer.cancel();
126
127
const channel = this.getCachedChannel(channelName);
128
const result = createCancelablePromise(token => channel.call<T>(name, arg, token));
129
const cancellationTokenListener = cancellationToken.onCancellationRequested(() => result.cancel());
130
131
const disposable = toDisposable(() => result.cancel());
132
this.activeRequests.add(disposable);
133
134
result.finally(() => {
135
cancellationTokenListener.dispose();
136
this.activeRequests.delete(disposable);
137
138
if (this.activeRequests.size === 0 && this.disposeDelayer) {
139
this.disposeDelayer.trigger(() => this.disposeClient());
140
}
141
});
142
143
return result;
144
}
145
146
protected requestEvent<T>(channelName: string, name: string, arg?: any): Event<T> {
147
if (!this.disposeDelayer) {
148
return Event.None;
149
}
150
151
this.disposeDelayer.cancel();
152
153
let listener: IDisposable;
154
const emitter = new Emitter<any>({
155
onWillAddFirstListener: () => {
156
const channel = this.getCachedChannel(channelName);
157
const event: Event<T> = channel.listen(name, arg);
158
159
listener = event(emitter.fire, emitter);
160
this.activeRequests.add(listener);
161
},
162
onDidRemoveLastListener: () => {
163
this.activeRequests.delete(listener);
164
listener.dispose();
165
166
if (this.activeRequests.size === 0 && this.disposeDelayer) {
167
this.disposeDelayer.trigger(() => this.disposeClient());
168
}
169
}
170
});
171
172
return emitter.event;
173
}
174
175
private get client(): IPCClient {
176
if (!this._client) {
177
const args = this.options.args || [];
178
const forkOpts: ForkOptions = Object.create(null);
179
180
forkOpts.env = { ...deepClone(process.env), 'VSCODE_PARENT_PID': String(process.pid) };
181
182
if (this.options.env) {
183
forkOpts.env = { ...forkOpts.env, ...this.options.env };
184
}
185
186
if (this.options.freshExecArgv) {
187
forkOpts.execArgv = [];
188
}
189
190
if (typeof this.options.debug === 'number') {
191
forkOpts.execArgv = ['--nolazy', '--inspect=' + this.options.debug];
192
}
193
194
if (typeof this.options.debugBrk === 'number') {
195
forkOpts.execArgv = ['--nolazy', '--inspect-brk=' + this.options.debugBrk];
196
}
197
198
if (forkOpts.execArgv === undefined) {
199
forkOpts.execArgv = process.execArgv // if not set, the forked process inherits the execArgv of the parent process
200
.filter(a => !/^--inspect(-brk)?=/.test(a)) // --inspect and --inspect-brk can not be inherited as the port would conflict
201
.filter(a => !a.startsWith('--vscode-')); // --vscode-* arguments are unsupported by node.js and thus need to remove
202
}
203
204
removeDangerousEnvVariables(forkOpts.env);
205
206
this.child = fork(this.modulePath, args, forkOpts);
207
208
const onMessageEmitter = new Emitter<VSBuffer>();
209
const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg);
210
211
const rawMessageDisposable = onRawMessage(msg => {
212
213
// Handle remote console logs specially
214
if (isRemoteConsoleLog(msg)) {
215
log(msg, `IPC Library: ${this.options.serverName}`);
216
return;
217
}
218
219
// Anything else goes to the outside
220
onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64')));
221
});
222
223
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
224
const send = (r: VSBuffer) => this.child?.connected && sender.send((<Buffer>r.buffer).toString('base64'));
225
const onMessage = onMessageEmitter.event;
226
const protocol = { send, onMessage };
227
228
this._client = new IPCClient(protocol);
229
230
const onExit = () => this.disposeClient();
231
process.once('exit', onExit);
232
233
this.child.on('error', err => console.warn('IPC "' + this.options.serverName + '" errored with ' + err));
234
235
this.child.on('exit', (code: any, signal: any) => {
236
process.removeListener('exit' as 'loaded', onExit); // https://github.com/electron/electron/issues/21475
237
rawMessageDisposable.dispose();
238
239
this.activeRequests.forEach(r => dispose(r));
240
this.activeRequests.clear();
241
242
if (code !== 0 && signal !== 'SIGTERM') {
243
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code + ' and signal ' + signal);
244
}
245
246
this.disposeDelayer?.cancel();
247
this.disposeClient();
248
this._onDidProcessExit.fire({ code, signal });
249
});
250
}
251
252
return this._client;
253
}
254
255
private getCachedChannel(name: string): IChannel {
256
let channel = this.channels.get(name);
257
258
if (!channel) {
259
channel = this.client.getChannel(name);
260
this.channels.set(name, channel);
261
}
262
263
return channel;
264
}
265
266
private disposeClient() {
267
if (this._client) {
268
if (this.child) {
269
this.child.kill();
270
this.child = null;
271
}
272
this._client = null;
273
this.channels.clear();
274
}
275
}
276
277
dispose() {
278
this._onDidProcessExit.dispose();
279
this.disposeDelayer?.cancel();
280
this.disposeDelayer = undefined;
281
this.disposeClient();
282
this.activeRequests.clear();
283
}
284
}
285
286