deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
231
packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts
Normal file
231
packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ConnectionErrorHandler, ContributionProvider, ILogger, MessageService } from '@theia/core/lib/common';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { BinaryMessagePipe } from '@theia/core/lib/node/messaging/binary-message-pipe';
|
||||
import { createIpcEnv } from '@theia/core/lib/node/messaging/ipc-protocol';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import * as cp from 'child_process';
|
||||
import { Duplex } from 'stream';
|
||||
import { HostedPluginClient, PLUGIN_HOST_BACKEND, PluginHostEnvironmentVariable, ServerPluginRunner } from '../../common/plugin-protocol';
|
||||
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
|
||||
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
|
||||
import { ProcessTerminateMessage, ProcessTerminatedMessage } from './hosted-plugin-protocol';
|
||||
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
|
||||
|
||||
export interface IPCConnectionOptions {
|
||||
readonly serverName: string;
|
||||
readonly logger: ILogger;
|
||||
readonly args: string[];
|
||||
readonly errorHandler?: ConnectionErrorHandler;
|
||||
}
|
||||
|
||||
export const HostedPluginProcessConfiguration = Symbol('HostedPluginProcessConfiguration');
|
||||
export interface HostedPluginProcessConfiguration {
|
||||
readonly path: string
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class HostedPluginProcess implements ServerPluginRunner {
|
||||
|
||||
@inject(HostedPluginProcessConfiguration)
|
||||
protected configuration: HostedPluginProcessConfiguration;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(HostedPluginCliContribution)
|
||||
protected readonly cli: HostedPluginCliContribution;
|
||||
|
||||
@inject(ContributionProvider)
|
||||
@named(PluginHostEnvironmentVariable)
|
||||
protected readonly pluginHostEnvironmentVariables: ContributionProvider<PluginHostEnvironmentVariable>;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(HostedPluginLocalizationService)
|
||||
protected readonly localizationService: HostedPluginLocalizationService;
|
||||
|
||||
@inject(ProcessUtils)
|
||||
protected readonly processUtils: ProcessUtils;
|
||||
|
||||
private childProcess: cp.ChildProcess | undefined;
|
||||
private messagePipe?: BinaryMessagePipe;
|
||||
private client: HostedPluginClient;
|
||||
|
||||
private terminatingPluginServer = false;
|
||||
|
||||
public setClient(client: HostedPluginClient): void {
|
||||
if (this.client) {
|
||||
if (this.childProcess) {
|
||||
this.runPluginServer();
|
||||
}
|
||||
}
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public clientClosed(): void {
|
||||
|
||||
}
|
||||
|
||||
public setDefault(defaultRunner: ServerPluginRunner): void {
|
||||
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public acceptMessage(pluginHostId: string, message: Uint8Array): boolean {
|
||||
return pluginHostId === 'main';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public onMessage(pluginHostId: string, message: Uint8Array): void {
|
||||
if (this.messagePipe) {
|
||||
this.messagePipe.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
async terminatePluginServer(): Promise<void> {
|
||||
if (this.childProcess === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.terminatingPluginServer = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const cp = this.childProcess;
|
||||
this.childProcess = undefined;
|
||||
|
||||
const waitForTerminated = new Deferred<void>();
|
||||
cp.on('message', message => {
|
||||
const msg = JSON.parse(message as string);
|
||||
if (ProcessTerminatedMessage.is(msg)) {
|
||||
waitForTerminated.resolve();
|
||||
}
|
||||
});
|
||||
const stopTimeout = this.cli.pluginHostStopTimeout;
|
||||
cp.send(JSON.stringify({ type: ProcessTerminateMessage.TYPE, stopTimeout }));
|
||||
|
||||
const terminateTimeout = this.cli.pluginHostTerminateTimeout;
|
||||
if (terminateTimeout) {
|
||||
await Promise.race([
|
||||
waitForTerminated.promise,
|
||||
new Promise(resolve => setTimeout(resolve, terminateTimeout))
|
||||
]);
|
||||
} else {
|
||||
await waitForTerminated.promise;
|
||||
}
|
||||
|
||||
this.killProcessTree(cp.pid!);
|
||||
}
|
||||
|
||||
killProcessTree(parentPid: number): void {
|
||||
this.processUtils.terminateProcessTree(parentPid);
|
||||
}
|
||||
|
||||
protected killProcess(pid: number): void {
|
||||
try {
|
||||
process.kill(pid);
|
||||
} catch (e) {
|
||||
if (e && 'code' in e && e.code === 'ESRCH') {
|
||||
return;
|
||||
}
|
||||
this.logger.error(`[${pid}] failed to kill`, e);
|
||||
}
|
||||
}
|
||||
|
||||
public runPluginServer(serverName?: string): void {
|
||||
if (this.childProcess) {
|
||||
this.terminatePluginServer();
|
||||
}
|
||||
this.terminatingPluginServer = false;
|
||||
this.childProcess = this.fork({
|
||||
serverName: serverName ?? 'hosted-plugin',
|
||||
logger: this.logger,
|
||||
args: []
|
||||
});
|
||||
|
||||
this.messagePipe = new BinaryMessagePipe(this.childProcess.stdio[4] as Duplex);
|
||||
this.messagePipe.onMessage(buffer => {
|
||||
if (this.client) {
|
||||
this.client.postMessage(PLUGIN_HOST_BACKEND, buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
readonly HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION = new RegExp('HOSTED_PLUGIN*');
|
||||
private fork(options: IPCConnectionOptions): cp.ChildProcess {
|
||||
|
||||
// create env and add PATH to it so any executable from root process is available
|
||||
const env = createIpcEnv({ env: process.env });
|
||||
for (const key of Object.keys(env)) {
|
||||
if (this.HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION.test(key)) {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
env['VSCODE_NLS_CONFIG'] = JSON.stringify(this.localizationService.getNlsConfig());
|
||||
// apply external env variables
|
||||
this.pluginHostEnvironmentVariables.getContributions().forEach(envVar => envVar.process(env));
|
||||
if (this.cli.extensionTestsPath) {
|
||||
env.extensionTestsPath = this.cli.extensionTestsPath;
|
||||
}
|
||||
|
||||
const forkOptions: cp.ForkOptions = {
|
||||
silent: true,
|
||||
env: env,
|
||||
execArgv: [],
|
||||
// 5th element MUST be 'overlapped' for it to work properly on Windows.
|
||||
// 'overlapped' works just like 'pipe' on non-Windows platforms.
|
||||
// See: https://nodejs.org/docs/latest-v14.x/api/child_process.html#child_process_options_stdio
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'overlapped']
|
||||
};
|
||||
const inspectArgPrefix = `--${options.serverName}-inspect`;
|
||||
const inspectArg = process.argv.find(v => v.startsWith(inspectArgPrefix));
|
||||
if (inspectArg !== undefined) {
|
||||
forkOptions.execArgv = ['--nolazy', `--inspect${inspectArg.substring(inspectArgPrefix.length)}`];
|
||||
}
|
||||
|
||||
const childProcess = cp.fork(this.configuration.path, options.args, forkOptions);
|
||||
childProcess.stdout!.on('data', data => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
|
||||
childProcess.stderr!.on('data', data => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
|
||||
|
||||
this.logger.debug(`[${options.serverName}: ${childProcess.pid}] IPC started`);
|
||||
childProcess.once('exit', (code: number, signal: string) => this.onChildProcessExit(options.serverName, childProcess.pid!, code, signal));
|
||||
childProcess.on('error', err => this.onChildProcessError(err));
|
||||
return childProcess;
|
||||
}
|
||||
|
||||
private onChildProcessExit(serverName: string, pid: number, code: number, signal: string): void {
|
||||
if (this.terminatingPluginServer) {
|
||||
return;
|
||||
}
|
||||
this.logger.error(`[${serverName}: ${pid}] IPC exited, with signal: ${signal}, and exit code: ${code}`);
|
||||
|
||||
const message = 'Plugin runtime crashed unexpectedly, all plugins are not working, please reload the page.';
|
||||
let hintMessage: string = 'If it doesn\'t help, please check Theia server logs.';
|
||||
if (signal && signal.toUpperCase() === 'SIGKILL') {
|
||||
// May happen in case of OOM or manual force stop.
|
||||
hintMessage = 'Probably there is not enough memory for the plugins. ' + hintMessage;
|
||||
}
|
||||
|
||||
this.messageService.error(message + ' ' + hintMessage, { timeout: 15 * 60 * 1000 });
|
||||
}
|
||||
|
||||
private onChildProcessError(err: Error): void {
|
||||
this.logger.error(`Error from plugin host: ${err.message}`);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user