diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 9c090f627df..4786adc264f 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -1071,12 +1071,19 @@ export namespace ProxyChannel { return new class implements IServerChannel { - listen(_: unknown, event: string): Event { + listen(_: unknown, event: string, arg: any): Event { const eventImpl = mapEventNameToEvent.get(event); if (eventImpl) { return eventImpl as Event; } + if (propertyIsDynamicEvent(event)) { + const target = handler[event]; + if (typeof target === 'function') { + return target.call(handler, arg); + } + } + throw new Error(`Event not found: ${event}`); } @@ -1126,6 +1133,13 @@ export namespace ProxyChannel { return options.properties.get(propKey); } + // Dynamic Event + if (propertyIsDynamicEvent(propKey)) { + return function (arg: any) { + return channel.listen(propKey, arg); + }; + } + // Event if (propertyIsEvent(propKey)) { return channel.listen(propKey); @@ -1162,6 +1176,11 @@ export namespace ProxyChannel { // Assume a property is an event if it has a form of "onSomething" return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2)); } + + function propertyIsDynamicEvent(name: string): boolean { + // Assume a property is a dynamic event (a method that returns an event) if it has a form of "onScopedSomething" + return /^onScoped/.test(name) && strings.isUpperAsciiLetter(name.charCodeAt(8)); + } } const colorTables = [ diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index d6bfffb09dd..3155d2de612 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -86,6 +86,8 @@ import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; +import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ExtensionHostStarter } from 'vs/platform/extensions/node/extensionHostStarter'; class SharedProcessMain extends Disposable { @@ -289,6 +291,9 @@ class SharedProcessMain extends Disposable { ) ); + // Extension Host + services.set(IExtensionHostStarter, this._register(new ExtensionHostStarter())); + return new InstantiationService(services); } @@ -339,6 +344,10 @@ class SharedProcessMain extends Disposable { const localPtyService = accessor.get(ILocalPtyService); const localPtyChannel = ProxyChannel.fromService(localPtyService); this.server.registerChannel(TerminalIpcChannels.LocalPty, localPtyChannel); + + // Extension Host + const extensionHostStarterChannel = ProxyChannel.fromService(accessor.get(IExtensionHostStarter)); + this.server.registerChannel(ipcExtensionHostStarterChannelName, extensionHostStarterChannel); } private registerErrorHandler(logService: ILogService): void { diff --git a/src/vs/platform/extensions/common/extensionHostStarter.ts b/src/vs/platform/extensions/common/extensionHostStarter.ts new file mode 100644 index 00000000000..63cc553fffe --- /dev/null +++ b/src/vs/platform/extensions/common/extensionHostStarter.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SerializedError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IExtensionHostStarter = createDecorator('extensionHostStarter'); + +export const ipcExtensionHostStarterChannelName = 'extensionHostStarter'; + +export interface IExtensionHostProcessOptions { + env: { [key: string]: string | undefined; }; + detached: boolean; + execArgv: string[] | undefined; + silent: boolean; +} + +export interface IExtensionHostStarter { + readonly _serviceBrand: undefined; + + onScopedStdout(id: string): Event; + onScopedStderr(id: string): Event; + onScopedMessage(id: string): Event; + onScopedError(id: string): Event<{ error: SerializedError; }>; + onScopedExit(id: string): Event<{ code: number; signal: string }>; + + createExtensionHost(): Promise<{ id: string; }>; + start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }>; + enableInspectPort(id: string): Promise; + kill(id: string): Promise; + +} diff --git a/src/vs/platform/extensions/node/extensionHostStarter.ts b/src/vs/platform/extensions/node/extensionHostStarter.ts new file mode 100644 index 00000000000..2bde6fb3056 --- /dev/null +++ b/src/vs/platform/extensions/node/extensionHostStarter.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SerializedError, transformErrorForSerialization } from 'vs/base/common/errors'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ChildProcess, fork } from 'child_process'; +import { FileAccess } from 'vs/base/common/network'; +import { StringDecoder } from 'string_decoder'; +import * as platform from 'vs/base/common/platform'; + +class ExtensionHostProcess extends Disposable { + + readonly _onStdout = this._register(new Emitter()); + readonly onStdout = this._onStdout.event; + + readonly _onStderr = this._register(new Emitter()); + readonly onStderr = this._onStderr.event; + + readonly _onMessage = this._register(new Emitter()); + readonly onMessage = this._onMessage.event; + + readonly _onError = this._register(new Emitter<{ error: SerializedError; }>()); + readonly onError = this._onError.event; + + readonly _onExit = this._register(new Emitter<{ code: number; signal: string }>()); + readonly onExit = this._onExit.event; + + private _process: ChildProcess | null = null; + + constructor( + public readonly id: string + ) { + super(); + } + + register(disposable: IDisposable) { + this._register(disposable); + } + + start(opts: IExtensionHostProcessOptions): { pid: number; } { + this._process = fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, ['--type=extensionHost', '--skipWorkspaceStorageLock'], opts); + + const stdoutDecoder = new StringDecoder('utf-8'); + this._process.stdout?.on('data', (chunk) => { + const strChunk = typeof chunk === 'string' ? chunk : stdoutDecoder.write(chunk); + this._onStdout.fire(strChunk); + }); + + const stderrDecoder = new StringDecoder('utf-8'); + this._process.stderr?.on('data', (chunk) => { + const strChunk = typeof chunk === 'string' ? chunk : stderrDecoder.write(chunk); + this._onStderr.fire(strChunk); + }); + + this._process.on('message', msg => { + this._onMessage.fire(msg); + }); + + this._process.on('error', (err) => { + this._onError.fire({ error: transformErrorForSerialization(err) }); + }); + + this._process.on('exit', (code: number, signal: string) => { + this._onExit.fire({ code, signal }); + }); + + return { pid: this._process.pid }; + } + + enableInspectPort(): boolean { + if (!this._process) { + return false; + } + + interface ProcessExt { + _debugProcess?(n: number): any; + } + + if (typeof (process)._debugProcess === 'function') { + // use (undocumented) _debugProcess feature of node + (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 { + this._process!.kill(); + } +} + +export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter { + _serviceBrand: undefined; + + private static _lastId: number = 0; + + private readonly _extHosts: Map; + + constructor() { + this._extHosts = new Map(); + } + + dispose(): void { + this._extHosts.forEach((extHost) => { + extHost.kill(); + }); + } + + private _getExtHost(id: string): ExtensionHostProcess { + const extHostProcess = this._extHosts.get(id); + if (!extHostProcess) { + throw new Error(`Unknown extension host!`); + } + return extHostProcess; + } + + onScopedStdout(id: string): Event { + return this._getExtHost(id).onStdout; + } + + onScopedStderr(id: string): Event { + return this._getExtHost(id).onStderr; + } + + onScopedMessage(id: string): Event { + return this._getExtHost(id).onMessage; + } + + onScopedError(id: string): Event<{ error: SerializedError; }> { + return this._getExtHost(id).onError; + } + + onScopedExit(id: string): Event<{ code: number; signal: string; }> { + return this._getExtHost(id).onExit; + } + + async createExtensionHost(): Promise<{ id: string; }> { + const id = String(++ExtensionHostStarter._lastId); + const extHost = new ExtensionHostProcess(id); + this._extHosts.set(id, extHost); + extHost.onExit(() => { + setTimeout(() => { + extHost.dispose(); + this._extHosts.delete(id); + }); + }); + return { id }; + } + + async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number; }> { + return this._getExtHost(id).start(opts); + } + + async enableInspectPort(id: string): Promise { + return this._getExtHost(id).enableInspectPort(); + } + + async kill(id: string): Promise { + this._getExtHost(id).kill(); + } +} diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 482aca0aaf9..76d903b30ba 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -4,10 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { ChildProcess, fork } from 'child_process'; -import { Server, Socket, createServer } from 'net'; import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; -import { FileAccess } from 'vs/base/common/network'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; @@ -17,10 +14,8 @@ import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; -import { findFreePort } from 'vs/base/node/ports'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; -import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -45,9 +40,13 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { isUUID } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; -import { Readable, Writable } from 'stream'; -import { StringDecoder } from 'string_decoder'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; +import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; + +import { Server, Socket, createServer } from 'net'; +import { findFreePort } from 'vs/base/node/ports'; +import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { SerializedError } from 'vs/base/common/errors'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -63,6 +62,50 @@ const enum NativeLogMarkers { End = 'END_NATIVE_LOG', } +class ExtensionHostProcess { + + private readonly _id: string; + + public get onStdout(): Event { + return this._extensionHostStarter.onScopedStdout(this._id); + } + + public get onStderr(): Event { + return this._extensionHostStarter.onScopedStderr(this._id); + } + + public get onMessage(): Event { + return this._extensionHostStarter.onScopedMessage(this._id); + } + + public get onError(): Event<{ error: SerializedError; }> { + return this._extensionHostStarter.onScopedError(this._id); + } + + public get onExit(): Event<{ code: number; signal: string }> { + return this._extensionHostStarter.onScopedExit(this._id); + } + + constructor( + id: string, + private readonly _extensionHostStarter: IExtensionHostStarter, + ) { + this._id = id; + } + + public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number; }> { + return this._extensionHostStarter.start(this._id, opts); + } + + public enableInspectPort(): Promise { + return this._extensionHostStarter.enableInspectPort(this._id); + } + + public kill(): Promise { + return this._extensionHostStarter.kill(this._id); + } +} + export class LocalProcessExtensionHost implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalProcess; @@ -88,7 +131,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // Resources, in order they get acquired/created when .start() is called: private _namedPipeServer: Server | null; private _inspectPort: number | null; - private _extensionHostProcess: ChildProcess | null; + private _extensionHostProcess: ExtensionHostProcess | null; private _extensionHostConnection: Socket | null; private _messageProtocol: Promise | null; @@ -107,7 +150,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, - @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService + @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, + @IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter, ) { const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; @@ -159,10 +203,14 @@ export class LocalProcessExtensionHost implements IExtensionHost { if (!this._messageProtocol) { this._messageProtocol = Promise.all([ + this._extensionHostStarter.createExtensionHost(), this._tryListenOnPipe(), this._tryFindDebugPort(), - this._shellEnvironmentService.getShellEnv() - ]).then(([pipeName, portNumber, processEnv]) => { + this._shellEnvironmentService.getShellEnv(), + ]).then(([extensionHostCreationResult, pipeName, portNumber, processEnv]) => { + + this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); + const env = objects.mixin(processEnv, { VSCODE_AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess', VSCODE_PIPE_LOGGING: 'true', @@ -238,13 +286,10 @@ export class LocalProcessExtensionHost implements IExtensionHost { opts.env.VSCODE_CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterStartOptions); } - // Run Extension Host as fork of current process - this._extensionHostProcess = fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, ['--type=extensionHost', '--skipWorkspaceStorageLock'], opts); - // Catch all output coming from the extension host process type Output = { data: string, format: string[] }; - const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.stdout!); - const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.stderr!); + const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); + const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); const onOutput = Event.any( Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) @@ -278,15 +323,16 @@ export class LocalProcessExtensionHost implements IExtensionHost { }); // Support logging from extension host - this._extensionHostProcess.on('message', msg => { + this._extensionHostProcess.onMessage(msg => { if (msg && (msg).type === '__$console') { this._logExtensionHostMessage(msg); } }); // Lifecycle - this._extensionHostProcess.on('error', (err) => this._onExtHostProcessError(err)); - this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal)); + + this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error)); + this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); // Notify debugger that we are ready to attach to the process if we run a development extension if (portNumber) { @@ -315,10 +361,12 @@ export class LocalProcessExtensionHost implements IExtensionHost { }, 10000); } - // Initialize extension host process with hand shakes - return this._tryExtHostHandshake().then((protocol) => { - clearTimeout(startupTimeoutHandle); - return protocol; + return this._extensionHostProcess.start(opts).then(() => { + // Initialize extension host process with hand shakes + return this._tryExtHostHandshake().then((protocol) => { + clearTimeout(startupTimeoutHandle); + return protocol; + }); }); }); } @@ -515,7 +563,15 @@ export class LocalProcessExtensionHost implements IExtensionHost { } } - private _onExtHostProcessError(err: any): void { + private _onExtHostProcessError(_err: SerializedError): void { + let err: any = _err; + if (_err && _err.$isError) { + err = new Error(); + err.name = _err.name; + err.message = _err.message; + err.stack = _err.stack; + } + let errorMessage = toErrorMessage(err); if (errorMessage === this._lastExtensionHostError) { return; // prevent error spam @@ -535,40 +591,35 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._onExit.fire([code, signal]); } - private _handleProcessOutputStream(stream: Readable) { + private _handleProcessOutputStream(stream: Event) { let last = ''; let isOmitting = false; const event = new Emitter(); - const decoder = new StringDecoder('utf-8'); - stream.pipe(new Writable({ - write(chunk, _encoding, callback) { - // not a fancy approach, but this is the same approach used by the split2 - // module which is well-optimized (https://github.com/mcollina/split2) - last += typeof chunk === 'string' ? chunk : decoder.write(chunk); - let lines = last.split(/\r?\n/g); - last = lines.pop()!; + stream((chunk) => { + // not a fancy approach, but this is the same approach used by the split2 + // module which is well-optimized (https://github.com/mcollina/split2) + last += chunk; + let lines = last.split(/\r?\n/g); + last = lines.pop()!; - // protected against an extension spamming and leaking memory if no new line is written. - if (last.length > 10_000) { - lines.push(last); - last = ''; - } - - for (const line of lines) { - if (isOmitting) { - if (line === NativeLogMarkers.End) { - isOmitting = false; - } - } else if (line === NativeLogMarkers.Start) { - isOmitting = true; - } else if (line.length) { - event.fire(line + '\n'); - } - } - - callback(); + // protected against an extension spamming and leaking memory if no new line is written. + if (last.length > 10_000) { + lines.push(last); + last = ''; } - })); + + for (const line of lines) { + if (isOmitting) { + if (line === NativeLogMarkers.End) { + isOmitting = false; + } + } else if (line === NativeLogMarkers.Start) { + isOmitting = true; + } else if (line.length) { + event.fire(line + '\n'); + } + } + }); return event; } @@ -582,26 +633,13 @@ export class LocalProcessExtensionHost implements IExtensionHost { return false; } - interface ProcessExt { - _debugProcess?(n: number): any; - } - - if (typeof (process)._debugProcess === 'function') { - // use (undocumented) _debugProcess feature of node - (process)._debugProcess!(this._extensionHostProcess.pid); - await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]); - return typeof this._inspectPort === 'number'; - - } else if (!platform.isWindows) { - // use KILL USR1 on non-windows platforms (fallback) - this._extensionHostProcess.kill('SIGUSR1'); - await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]); - return typeof this._inspectPort === 'number'; - - } else { - // not supported... + const result = await this._extensionHostProcess.enableInspectPort(); + if (!result) { return false; } + + await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]); + return typeof this._inspectPort === 'number'; } public getInspectPort(): number | undefined { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostStarter.ts b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostStarter.ts new file mode 100644 index 00000000000..3464798a9ac --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostStarter.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; +import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; + +registerSharedProcessRemoteService(IExtensionHostStarter, ipcExtensionHostStarterChannelName, { supportsDelayedInstantiation: true }); diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 9f43f7332b4..8569bab68ad 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -52,6 +52,7 @@ import 'vs/workbench/services/credentials/electron-sandbox/credentialsService'; import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; +import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService';