Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use an UtilityProcess for the extension host #150167

Merged
merged 3 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/bootstrap-fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ if (process.env['VSCODE_PARENT_PID']) {
terminateWhenParentTerminates();
}

// Listen for message ports
if (process.env['VSCODE_WILL_SEND_MESSAGE_PORT']) {
listenForMessagePort();
}

// Load AMD entry point
require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']);

Expand Down Expand Up @@ -264,4 +269,17 @@ function terminateWhenParentTerminates() {
}
}

function listenForMessagePort() {
// We need to listen for the 'port' event as soon as possible,
// otherwise we might miss the event. But we should also be
// prepared in case the event arrives late.
process.on('port', (e) => {
if (global.vscodePortsCallback) {
global.vscodePortsCallback(e.ports);
} else {
global.vscodePorts = e.ports;
}
});
}

//#endregion
6 changes: 5 additions & 1 deletion src/vs/platform/extensions/common/extensionHostStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const IExtensionHostStarter = createDecorator<IExtensionHostStarter>('ext
export const ipcExtensionHostStarterChannelName = 'extensionHostStarter';

export interface IExtensionHostProcessOptions {
responseWindowId: number;
responseChannel: string;
responseNonce: string;
env: { [key: string]: string | undefined };
detached: boolean;
execArgv: string[] | undefined;
Expand All @@ -27,8 +30,9 @@ export interface IExtensionHostStarter {
onDynamicError(id: string): Event<{ error: SerializedError }>;
onDynamicExit(id: string): Event<{ code: number; signal: string }>;

usesUtilityProcess(): Promise<boolean>;
createExtensionHost(): Promise<{ id: string }>;
start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }>;
start(id: string, opts: IExtensionHostProcessOptions): Promise<void>;
enableInspectPort(id: string): Promise<boolean>;
kill(id: string): Promise<void>;

Expand Down
151 changes: 143 additions & 8 deletions src/vs/platform/extensions/electron-main/extensionHostStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,41 @@ import { FileAccess } from 'vs/base/common/network';
import { mixin } from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import { cwd } from 'vs/base/common/process';
import type { EventEmitter } from 'events';
import * as electron from 'electron';
import { constants } from 'os';

declare namespace UtilityProcessProposedApi {
interface UtilityProcessOptions {
displayName?: string | undefined;
execArgv?: string[] | undefined;
env?: NodeJS.ProcessEnv | undefined;
}
export class UtilityProcess extends EventEmitter {
readonly pid?: number | undefined;
constructor(modulePath: string, args?: string[] | undefined, options?: UtilityProcessOptions);
postMessage(channel: string, message: any, transfer?: Electron.MessagePortMain[]): void;
kill(signal: number): boolean;
on(event: 'exit', listener: (code: number) => void): this;
on(event: 'spawn', listener: () => void): this;
}
}
const UtilityProcess = <typeof UtilityProcessProposedApi.UtilityProcess>((electron as any).UtilityProcess);
const canUseUtilityProcess = (typeof UtilityProcess !== 'undefined');

export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter {
_serviceBrand: undefined;

private static _lastId: number = 0;

protected readonly _extHosts: Map<string, ExtensionHostProcess>;
protected readonly _extHosts: Map<string, ExtensionHostProcess | UtilityExtensionHostProcess>;
private _shutdown = false;

constructor(
@ILogService private readonly _logService: ILogService,
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
) {
this._extHosts = new Map<string, ExtensionHostProcess>();
this._extHosts = new Map<string, ExtensionHostProcess | UtilityExtensionHostProcess>();

// On shutdown: gracefully await extension host shutdowns
lifecycleMainService.onWillShutdown((e) => {
Expand All @@ -43,7 +64,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
// Intentionally not killing the extension host processes
}

private _getExtHost(id: string): ExtensionHostProcess {
private _getExtHost(id: string): ExtensionHostProcess | UtilityExtensionHostProcess {
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
throw new Error(`Unknown extension host!`);
Expand Down Expand Up @@ -71,12 +92,20 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
return this._getExtHost(id).onExit;
}

async usesUtilityProcess(): Promise<boolean> {
return canUseUtilityProcess;
}

async createExtensionHost(): Promise<{ id: string }> {
if (this._shutdown) {
throw canceled();
}
const id = String(++ExtensionHostStarter._lastId);
const extHost = new ExtensionHostProcess(id, this._logService);
const extHost = (
canUseUtilityProcess
? new UtilityExtensionHostProcess(id, this._logService)
: new ExtensionHostProcess(id, this._logService)
);
this._extHosts.set(id, extHost);
extHost.onExit(({ pid, code, signal }) => {
this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`);
Expand All @@ -88,7 +117,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
return { id };
}

async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }> {
async start(id: string, opts: IExtensionHostProcessOptions): Promise<void> {
if (this._shutdown) {
throw canceled();
}
Expand Down Expand Up @@ -160,7 +189,7 @@ class ExtensionHostProcess extends Disposable {
super();
}

start(opts: IExtensionHostProcessOptions): { pid: number } {
start(opts: IExtensionHostProcessOptions): void {
if (platform.isCI) {
this._logService.info(`Calling fork to start extension host...`);
}
Expand Down Expand Up @@ -199,8 +228,6 @@ class ExtensionHostProcess extends Disposable {
this._hasExited = true;
this._onExit.fire({ pid, code, signal });
});

return { pid };
}

enableInspectPort(): boolean {
Expand Down Expand Up @@ -251,3 +278,111 @@ class ExtensionHostProcess extends Disposable {
}
}
}

class UtilityExtensionHostProcess extends Disposable {

readonly onStdout = Event.None;
readonly onStderr = Event.None;
readonly onError = Event.None;

readonly _onMessage = this._register(new Emitter<any>());
readonly onMessage = this._onMessage.event;

readonly _onExit = this._register(new Emitter<{ pid: number; code: number; signal: string }>());
readonly onExit = this._onExit.event;

private _process: UtilityProcessProposedApi.UtilityProcess | null = null;
// private _hasExited: boolean = false;

constructor(
public readonly id: string,
@ILogService private readonly _logService: ILogService,
) {
super();
}

start(opts: IExtensionHostProcessOptions): void {
const responseWindow = electron.BrowserWindow.fromId(opts.responseWindowId);
if (!responseWindow || responseWindow.isDestroyed() || responseWindow.webContents.isDestroyed()) {
this._logService.info(`Refusing to create new Extension Host UtilityProcess because requesting window cannot be found...`);
return;
}

const modulePath = FileAccess.asFileUri('bootstrap-fork.js', require).fsPath;
const args: string[] = ['--type=extensionHost', '--skipWorkspaceStorageLock'];
const argv: string[] = opts.execArgv || [];
const env: { [key: string]: any } = { ...opts.env };

// Make sure all values are strings, otherwise the process will not start
for (const key of Object.keys(env)) {
env[key] = String(env[key]);
}

this._logService.info(`Creating new UtilityProcess to start extension host...`);

this._process = new UtilityProcess(modulePath, args, { env: env, execArgv: argv });

this._process.on('spawn', () => {
this._logService.info(`Utility process emits spawn!`);
});
this._process.on('exit', (code: number) => {
this._logService.info(`Utility process emits exit!`);
// this._hasExited = true;
this._onExit.fire({ pid: this._process!.pid!, code, signal: '' });
});

const { port1, port2 } = new electron.MessageChannelMain();

this._process.postMessage('port', null, [port2]);
responseWindow.webContents.postMessage(opts.responseChannel, opts.responseNonce, [port1]);
}

enableInspectPort(): boolean {
return false;
// if (!this._process) {
// return false;
// }

// this._logService.info(`Enabling inspect port on extension host with pid ${this._process.pid}.`);

// interface ProcessExt {
// _debugProcess?(n: number): any;
// }

// if (typeof (<ProcessExt>process)._debugProcess === 'function') {
// // use (undocumented) _debugProcess feature of node
// (<ProcessExt>process)._debugProcess!(this._process.pid!);
// return true;
// } else if (!platform.isWindows) {
// // use KILL USR1 on non-windows platforms (fallback)
// this._process.kill('SIGUSR1');
// return true;
// } else {
// // not supported...
// return false;
// }
}

kill(): void {
if (!this._process) {
return;
}
this._logService.info(`Killing extension host with pid ${this._process.pid}.`);
this._process.kill(constants.signals.SIGTERM);
}

async waitForExit(maxWaitTimeMs: number): Promise<void> {
if (!this._process) {
return;
}
// const pid = this._process.pid;
// this._logService.info(`Waiting for extension host with pid ${pid} to exit.`);
// await Promise.race([Event.toPromise(this.onExit), timeout(maxWaitTimeMs)]);

// if (!this._hasExited) {
// // looks like we timed out
// this._logService.info(`Extension host with pid ${pid} did not exit within ${maxWaitTimeMs}ms.`);
// this._process.kill();
// }
}
}
37 changes: 33 additions & 4 deletions src/vs/workbench/api/node/extensionHostProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService';
import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async';
import { boolean } from 'vs/editor/common/config/editorOptions';
import { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
import { MessagePortMain } from 'electron';

import 'vs/workbench/api/common/extHost.common.services';
import 'vs/workbench/api/node/extHost.node.services';
Expand Down Expand Up @@ -102,8 +103,34 @@ let onTerminate = function (reason: string) {
nativeExit();
};

function _createExtHostProtocol(): Promise<PersistentProtocol> {
if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) {
function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
if (process.env.VSCODE_WILL_SEND_MESSAGE_PORT) {

return new Promise<IMessagePassingProtocol>((resolve, reject) => {

const withPorts = (ports: MessagePortMain[]) => {
const port = ports[0];
const onMessage = new BufferedEmitter<VSBuffer>();
port.on('message', (e) => onMessage.fire(VSBuffer.wrap(e.data)));
port.start();

resolve({
onMessage: onMessage.event,
send: message => port.postMessage(message.buffer)
});
};

if ((<any>global).vscodePorts) {
const ports = (<any>global).vscodePorts;
delete (<any>global).vscodePorts;
withPorts(ports);
} else {
(<any>global).vscodePortsCallback = withPorts;
}

});

} else if (process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET) {

return new Promise<PersistentProtocol>((resolve, reject) => {

Expand Down Expand Up @@ -220,8 +247,10 @@ async function createExtHostProtocol(): Promise<IMessagePassingProtocol> {
}
}

drain(): Promise<void> {
return protocol.drain();
async drain(): Promise<void> {
if (protocol.drain) {
return protocol.drain();
}
}
};
}
Expand Down
Loading