@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
214 lines (213 loc) • 7.52 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { fork } from 'child_process';
import { log } from 'console';
import { VSBuffer } from '@sussudio/base/common/buffer.mjs';
import { isRemoteConsoleLog } from '@sussudio/base/common/console.mjs';
import { toErrorMessage } from '@sussudio/base/common/errorMessage.mjs';
import { Event, Emitter } from '@sussudio/base/common/event.mjs';
import { Disposable, toDisposable } from '@sussudio/base/common/lifecycle.mjs';
import { deepClone } from '@sussudio/base/common/objects.mjs';
import { withNullAsUndefined } from '@sussudio/base/common/types.mjs';
import { removeDangerousEnvVariables } from '@sussudio/base/common/processes.mjs';
import { hash } from '../common/sharedProcessWorkerService.mjs';
import { SharedProcessWorkerMessages } from './sharedProcessWorker.mjs';
/**
* The `create` function needs to be there by convention because
* we are loaded via the `vs/base/worker/workerMain` utility.
*/
export function create() {
const sharedProcessWorkerMain = new SharedProcessWorkerMain();
// Signal we are ready
send({ id: SharedProcessWorkerMessages.Ready });
return {
onmessage: (message, transfer) => sharedProcessWorkerMain.onMessage(message, transfer),
};
}
class SharedProcessWorkerMain {
processes = new Map();
onMessage(message, transfer) {
// Handle message from shared process
switch (message.id) {
// Spawn new process
case SharedProcessWorkerMessages.Spawn:
if (transfer && transfer[0] instanceof MessagePort && message.environment) {
this.spawn(transfer[0], message.configuration, message.environment);
}
break;
// Terminate existing process
case SharedProcessWorkerMessages.Terminate:
this.terminate(message.configuration);
break;
default:
Logger.warn(`Unexpected shared process message '${message}'`);
}
// Acknowledge message processed if we have a nonce
if (message.nonce) {
send({
id: SharedProcessWorkerMessages.Ack,
nonce: message.nonce,
});
}
}
spawn(port, configuration, environment) {
try {
// Ensure to terminate any existing process for config
this.terminate(configuration);
// Spawn a new worker process with given configuration
const process = new SharedProcessWorkerProcess(port, configuration, environment);
process.spawn();
// Handle self termination of the child process
const listener = Event.once(process.onDidProcessSelfTerminate)((reason) => {
send({
id: SharedProcessWorkerMessages.SelfTerminated,
configuration,
message: JSON.stringify(reason),
});
});
// Remember in map for lifecycle
const configurationHash = hash(configuration);
this.processes.set(
configurationHash,
toDisposable(() => {
listener.dispose();
// Terminate process
process.dispose();
// Remove from processes
this.processes.delete(configurationHash);
}),
);
} catch (error) {
Logger.error(`Unexpected error forking worker process: ${toErrorMessage(error)}`);
}
}
terminate(configuration) {
const processDisposable = this.processes.get(hash(configuration));
processDisposable?.dispose();
}
}
class SharedProcessWorkerProcess extends Disposable {
port;
configuration;
environment;
_onDidProcessSelfTerminate = this._register(new Emitter());
onDidProcessSelfTerminate = this._onDidProcessSelfTerminate.event;
child = undefined;
constructor(port, configuration, environment) {
super();
this.port = port;
this.configuration = configuration;
this.environment = environment;
}
spawn() {
Logger.trace('Forking worker process');
// Fork module via bootstrap-fork for AMD support
this.child = fork(this.environment.bootstrapPath, [`--type=${this.configuration.process.type}`], {
env: this.getEnv(),
});
Logger.info(
`Starting worker process with pid ${this.child.pid} (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId}).`,
);
// Re-emit errors to outside
const onError = Event.fromNodeEventEmitter(this.child, 'error');
this._register(onError((error) => Logger.warn(`Error from child process: ${toErrorMessage(error)}`)));
// Handle termination that happens from the process
// itself. This can either be a crash or the process
// not being long running.
const onExit = Event.fromNodeEventEmitter(this.child, 'exit', (code, signal) => ({ code, signal }));
this._register(
onExit(({ code, signal }) => {
const logMsg = `Worker process with pid ${this.child?.pid} terminated by itself with code ${code}, signal: ${signal} (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId})`;
if (code !== 0 && signal !== 'SIGTERM') {
Logger.error(logMsg);
} else {
Logger.info(logMsg);
}
this.child = undefined;
this._onDidProcessSelfTerminate.fire({
code: withNullAsUndefined(code),
signal: withNullAsUndefined(signal),
});
}),
);
const onMessageEmitter = this._register(new Emitter());
const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', (msg) => msg);
this._register(
onRawMessage((msg) => {
// Handle remote console logs specially
if (isRemoteConsoleLog(msg)) {
log(msg, `SharedProcess worker`);
}
// Anything else goes to the outside
else {
onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64')));
}
}),
);
const send = (buffer) => {
if (this.child?.connected) {
this.child.send(buffer.buffer.toString('base64'));
} else {
Logger.warn('Unable to deliver message to disconnected child');
}
};
// Re-emit messages from the process via the port
const onMessage = onMessageEmitter.event;
this._register(onMessage((message) => this.port.postMessage(message.buffer)));
// Relay message from the port into the process
this.port.onmessage = (e) => send(VSBuffer.wrap(e.data));
this._register(toDisposable(() => (this.port.onmessage = null)));
}
getEnv() {
const env = {
...deepClone(process.env),
VSCODE_AMD_ENTRYPOINT: this.configuration.process.moduleId,
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true',
VSCODE_PARENT_PID: String(process.pid),
};
// Sanitize environment
removeDangerousEnvVariables(env);
return env;
}
dispose() {
super.dispose();
if (!this.child) {
return;
}
this.child.kill();
Logger.info(
`Worker process with pid ${this.child?.pid} terminated normally (type: ${this.configuration.process.type}, window: ${this.configuration.reply.windowId}).`,
);
}
}
/**
* Helper for logging messages from the worker.
*/
var Logger;
(function (Logger) {
function error(message) {
send({ id: SharedProcessWorkerMessages.Error, message });
}
Logger.error = error;
function warn(message) {
send({ id: SharedProcessWorkerMessages.Warn, message });
}
Logger.warn = warn;
function info(message) {
send({ id: SharedProcessWorkerMessages.Info, message });
}
Logger.info = info;
function trace(message) {
send({ id: SharedProcessWorkerMessages.Trace, message });
}
Logger.trace = trace;
})(Logger || (Logger = {}));
/**
* Helper for typed `postMessage` usage.
*/
function send(message) {
postMessage(message);
}