Skip to content

Commit

Permalink
Merge pull request #135774 from microsoft/alex/main-process-extension…
Browse files Browse the repository at this point in the history
…-host

Create extension host processes from a node worker in the main process
  • Loading branch information
alexdima committed Nov 11, 2021
2 parents ef7b823 + 778014d commit 37794df
Show file tree
Hide file tree
Showing 15 changed files with 375 additions and 32 deletions.
1 change: 1 addition & 0 deletions build/gulpfile.vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const vscodeResources = [
'out-build/vs/base/browser/ui/codicons/codicon/**',
'out-build/vs/base/parts/sandbox/electron-browser/preload.js',
'out-build/vs/platform/environment/node/userDataPath.js',
'out-build/vs/platform/extensions/node/extensionHostStarterWorkerMain.js',
'out-build/vs/workbench/browser/media/*-theme.css',
'out-build/vs/workbench/contrib/debug/**/*.json',
'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt',
Expand Down
3 changes: 3 additions & 0 deletions build/lib/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const vm = require("vm");
function bundle(entryPoints, config, callback) {
const entryPointsMap = {};
entryPoints.forEach((module) => {
if (entryPointsMap[module.name]) {
throw new Error(`Cannot have two entry points with the same name '${module.name}'`);
}
entryPointsMap[module.name] = module;
});
const allMentionedModulesMap = {};
Expand Down
3 changes: 3 additions & 0 deletions build/lib/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export interface ILoaderConfig {
export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callback: (err: any, result: IBundleResult | null) => void): void {
const entryPointsMap: IEntryPointMap = {};
entryPoints.forEach((module: IEntryPoint) => {
if (entryPointsMap[module.name]) {
throw new Error(`Cannot have two entry points with the same name '${module.name}'`);
}
entryPointsMap[module.name] = module;
});

Expand Down
23 changes: 16 additions & 7 deletions src/buildfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@

const { createModuleDescription, createEditorWorkerModuleDescription } = require('./vs/base/buildfile');

exports.base = [{
name: 'vs/base/common/worker/simpleWorker',
include: ['vs/editor/common/services/editorSimpleWorker'],
prepend: ['vs/loader.js', 'vs/nls.js'],
append: ['vs/base/worker/workerMain'],
dest: 'vs/base/worker/workerMain.js'
}];
exports.base = [
{
name: 'vs/editor/common/services/editorSimpleWorker',
include: ['vs/base/common/worker/simpleWorker'],
prepend: ['vs/loader.js', 'vs/nls.js'],
append: ['vs/base/worker/workerMain'],
dest: 'vs/base/worker/workerMain.js'
},
{
name: 'vs/base/common/worker/simpleWorker',
},
{
name: 'vs/platform/extensions/node/extensionHostStarterWorker',
exclude: ['vs/base/common/worker/simpleWorker']
}
];

exports.workerExtensionHost = [createEditorWorkerModuleDescription('vs/workbench/services/extensions/worker/extensionHostWorker')];
exports.workerNotebook = [createEditorWorkerModuleDescription('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')];
Expand Down
9 changes: 9 additions & 0 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService';
import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter';
import { WorkerMainProcessExtensionHostStarter } from 'vs/platform/extensions/electron-main/workerMainProcessExtensionHostStarter';
import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal';
import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService';
import { IFileService } from 'vs/platform/files/common/files';
Expand Down Expand Up @@ -514,6 +516,9 @@ export class CodeApplication extends Disposable {
// Extension URL Trust
services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService));

// Extension Host Starter
services.set(IExtensionHostStarter, new SyncDescriptor(WorkerMainProcessExtensionHostStarter));

// Storage
services.set(IStorageMainService, new SyncDescriptor(StorageMainService));

Expand Down Expand Up @@ -640,6 +645,10 @@ export class CodeApplication extends Disposable {
// Extension Host Debug Broadcasting
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);

// Extension Host Starter
const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter));
mainProcessElectronServer.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel);
}

private openFirstWindow(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer): ICodeWindow[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ExtensionHostStarter, IPartialLogService } from 'vs/platform/extensions/node/extensionHostStarter';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { ILogService } from 'vs/platform/log/common/log';

export class DirectMainProcessExtensionHostStarter extends ExtensionHostStarter {

constructor(
@ILogService logService: IPartialLogService,
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
) {
super(logService);

// Abnormal shutdown: terminate extension hosts asap
lifecycleMainService.onWillKill(() => {
this.killAllNow();
});

// Normal shutdown: gracefully await extension host shutdowns
lifecycleMainService.onWillShutdown((e) => {
e.join(this.waitForAllExit(6000));
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { canceled, SerializedError } from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';
import { Event } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network';
import { ILogService } from 'vs/platform/log/common/log';
import { Worker } from 'worker_threads';
import { IWorker, IWorkerCallback, IWorkerFactory, SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker';
import { IExtensionHostStarterWorkerHost } from 'vs/platform/extensions/node/extensionHostStarterWorker';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { ExtensionHostStarter } from 'vs/platform/extensions/node/extensionHostStarter';

class NodeWorker implements IWorker {

private readonly _worker: Worker;

constructor(callback: IWorkerCallback, onErrorCallback: (err: any) => void) {
this._worker = new Worker(
FileAccess.asFileUri('vs/platform/extensions/node/extensionHostStarterWorkerMain.js', require).fsPath,
);
this._worker.on('message', callback);
this._worker.on('error', onErrorCallback);
// this._worker.on('exit', (code) => {
// console.log(`worker exited with code `, code);
// });
}

getId(): number {
return 1;
}

postMessage(message: any, transfer: ArrayBuffer[]): void {
this._worker.postMessage(message, transfer);
}

dispose(): void {
this._worker.terminate();
}
}

class ExtensionHostStarterWorkerHost implements IExtensionHostStarterWorkerHost {
constructor(
@ILogService private readonly _logService: ILogService
) { }

public async logInfo(message: string): Promise<void> {
this._logService.info(message);
}
}

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

private _proxy: ExtensionHostStarter | null;
private readonly _worker: SimpleWorkerClient<ExtensionHostStarter, IExtensionHostStarterWorkerHost>;
private _shutdown = false;

constructor(
@ILogService private readonly _logService: ILogService,
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
) {
this._proxy = null;

const workerFactory: IWorkerFactory = {
create: (moduleId: string, callback: IWorkerCallback, onErrorCallback: (err: any) => void): IWorker => {
const worker = new NodeWorker(callback, onErrorCallback);
worker.postMessage(moduleId, []);
return worker;
}
};
this._worker = new SimpleWorkerClient<ExtensionHostStarter, IExtensionHostStarterWorkerHost>(
workerFactory,
'vs/platform/extensions/node/extensionHostStarterWorker',
new ExtensionHostStarterWorkerHost(this._logService)
);
this._initialize();

// Abnormal shutdown: terminate extension hosts asap
lifecycleMainService.onWillKill(async () => {
this._shutdown = true;
if (this._proxy) {
this._proxy.killAllNow();
}
});

// Normal shutdown: gracefully await extension host shutdowns
lifecycleMainService.onWillShutdown((e) => {
this._shutdown = true;
if (this._proxy) {
e.join(this._proxy.waitForAllExit(6000));
}
});
}

dispose(): void {
// Intentionally not killing the extension host processes
}

async _initialize(): Promise<void> {
this._proxy = await this._worker.getProxyObject();
this._logService.info(`ExtensionHostStarterWorker created`);
}

onDynamicStdout(id: string): Event<string> {
return this._proxy!.onDynamicStderr(id);
}

onDynamicStderr(id: string): Event<string> {
return this._proxy!.onDynamicStderr(id);
}

onDynamicMessage(id: string): Event<any> {
return this._proxy!.onDynamicMessage(id);
}

onDynamicError(id: string): Event<{ error: SerializedError; }> {
return this._proxy!.onDynamicError(id);
}

onDynamicExit(id: string): Event<{ code: number; signal: string; }> {
return this._proxy!.onDynamicExit(id);
}

async createExtensionHost(): Promise<{ id: string; }> {
const proxy = await this._worker.getProxyObject();
if (this._shutdown) {
throw canceled();
}
return proxy.createExtensionHost();
}

async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }> {
const proxy = await this._worker.getProxyObject();
if (this._shutdown) {
throw canceled();
}
return proxy.start(id, opts);
}

async enableInspectPort(id: string): Promise<boolean> {
const proxy = await this._worker.getProxyObject();
if (this._shutdown) {
throw canceled();
}
return proxy.enableInspectPort(id);
}

async kill(id: string): Promise<void> {
const proxy = await this._worker.getProxyObject();
if (this._shutdown) {
throw canceled();
}
return proxy.kill(id);
}
}
52 changes: 44 additions & 8 deletions src/vs/platform/extensions/node/extensionHostStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import { ILogService } from 'vs/platform/log/common/log';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { mixin } from 'vs/base/common/objects';
import { cwd } from 'vs/base/common/process';
import { StopWatch } from 'vs/base/common/stopwatch';
import { Promises, timeout } from 'vs/base/common/async';

export interface IPartialLogService {
readonly _serviceBrand: undefined;
info(message: string): void;
}

class ExtensionHostProcess extends Disposable {

Expand All @@ -34,27 +41,26 @@ class ExtensionHostProcess extends Disposable {
readonly onExit = this._onExit.event;

private _process: ChildProcess | null = null;
private _hasExited: boolean = false;

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

register(disposable: IDisposable) {
this._register(disposable);
}

start(opts: IExtensionHostProcessOptions): { pid: number; } {
const sw = StopWatch.create(false);
this._process = fork(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
['--type=extensionHost', '--skipWorkspaceStorageLock'],
mixin({ cwd: cwd() }, opts),
);
const forkTime = sw.elapsed();
const pid = this._process.pid;

this._logService.info(`Starting extension host with pid ${pid}.`);
this._logService.info(`Starting extension host with pid ${pid} (fork() took ${forkTime} ms).`);

const stdoutDecoder = new StringDecoder('utf-8');
this._process.stdout?.on('data', (chunk) => {
Expand All @@ -77,6 +83,7 @@ class ExtensionHostProcess extends Disposable {
});

this._process.on('exit', (code: number, signal: string) => {
this._hasExited = true;
this._onExit.fire({ pid, code, signal });
});

Expand Down Expand Up @@ -115,17 +122,32 @@ class ExtensionHostProcess extends Disposable {
this._logService.info(`Killing extension host with pid ${this._process.pid}.`);
this._process.kill();
}

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();
}
}
}

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

private static _lastId: number = 0;

private readonly _extHosts: Map<string, ExtensionHostProcess>;
protected readonly _extHosts: Map<string, ExtensionHostProcess>;

constructor(
@ILogService private readonly _logService: ILogService
@ILogService private readonly _logService: IPartialLogService
) {
this._extHosts = new Map<string, ExtensionHostProcess>();
}
Expand Down Expand Up @@ -196,6 +218,20 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter
}
extHostProcess.kill();
}

async killAllNow(): Promise<void> {
for (const [, extHost] of this._extHosts) {
extHost.kill();
}
}

async waitForAllExit(maxWaitTimeMs: number): Promise<void> {
const exitPromises: Promise<void>[] = [];
for (const [, extHost] of this._extHosts) {
exitPromises.push(extHost.waitForExit(maxWaitTimeMs));
}
return Promises.settled(exitPromises).then(() => { });
}
}

registerSingleton(IExtensionHostStarter, ExtensionHostStarter, true);

0 comments on commit 37794df

Please sign in to comment.