UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

381 lines (380 loc) 12.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { canceled, transformErrorForSerialization } from '@sussudio/base/common/errors.mjs'; import { Disposable } from '@sussudio/base/common/lifecycle.mjs'; import { Emitter, Event } from '@sussudio/base/common/event.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.mjs'; import { StopWatch } from '@sussudio/base/common/stopwatch.mjs'; import { fork } from 'child_process'; import { StringDecoder } from 'string_decoder'; import { Promises, timeout } from '@sussudio/base/common/async.mjs'; import { FileAccess } from '@sussudio/base/common/network.mjs'; import { mixin } from '@sussudio/base/common/objects.mjs'; import * as platform from '@sussudio/base/common/platform.mjs'; import { cwd } from '@sussudio/base/common/process.mjs'; import * as electron from 'electron'; import { IWindowsMainService } from '../../windows/electron-main/windows.mjs'; const UtilityProcess = electron.utilityProcess; const canUseUtilityProcess = typeof UtilityProcess !== 'undefined'; let ExtensionHostStarter = class ExtensionHostStarter { _logService; _windowsMainService; _serviceBrand; static _lastId = 0; _extHosts; _shutdown = false; constructor(_logService, lifecycleMainService, _windowsMainService) { this._logService = _logService; this._windowsMainService = _windowsMainService; this._extHosts = new Map(); // On shutdown: gracefully await extension host shutdowns lifecycleMainService.onWillShutdown((e) => { this._shutdown = true; e.join(this._waitForAllExit(6000)); }); } dispose() { // Intentionally not killing the extension host processes } _getExtHost(id) { const extHostProcess = this._extHosts.get(id); if (!extHostProcess) { throw new Error(`Unknown extension host!`); } return extHostProcess; } onDynamicStdout(id) { return this._getExtHost(id).onStdout; } onDynamicStderr(id) { return this._getExtHost(id).onStderr; } onDynamicMessage(id) { return this._getExtHost(id).onMessage; } onDynamicError(id) { return this._getExtHost(id).onError; } onDynamicExit(id) { return this._getExtHost(id).onExit; } async canUseUtilityProcess() { return canUseUtilityProcess; } async createExtensionHost(useUtilityProcess) { if (this._shutdown) { throw canceled(); } const id = String(++ExtensionHostStarter._lastId); let extHost; if (useUtilityProcess) { if (!canUseUtilityProcess) { throw new Error(`Cannot use UtilityProcess!`); } extHost = new UtilityExtensionHostProcess(id, this._logService, this._windowsMainService); } else { extHost = 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}.`); setTimeout(() => { extHost.dispose(); this._extHosts.delete(id); }); }); return { id }; } async start(id, opts) { if (this._shutdown) { throw canceled(); } return this._getExtHost(id).start(opts); } async enableInspectPort(id) { if (this._shutdown) { throw canceled(); } const extHostProcess = this._extHosts.get(id); if (!extHostProcess) { return false; } return extHostProcess.enableInspectPort(); } async kill(id) { if (this._shutdown) { throw canceled(); } const extHostProcess = this._extHosts.get(id); if (!extHostProcess) { // already gone! return; } extHostProcess.kill(); } async _killAllNow() { for (const [, extHost] of this._extHosts) { extHost.kill(); } } async _waitForAllExit(maxWaitTimeMs) { const exitPromises = []; for (const [, extHost] of this._extHosts) { exitPromises.push(extHost.waitForExit(maxWaitTimeMs)); } return Promises.settled(exitPromises).then(() => {}); } }; ExtensionHostStarter = __decorate( [__param(0, ILogService), __param(1, ILifecycleMainService), __param(2, IWindowsMainService)], ExtensionHostStarter, ); export { ExtensionHostStarter }; let ExtensionHostProcess = class ExtensionHostProcess extends Disposable { id; _logService; _onStdout = this._register(new Emitter()); onStdout = this._onStdout.event; _onStderr = this._register(new Emitter()); onStderr = this._onStderr.event; _onMessage = this._register(new Emitter()); onMessage = this._onMessage.event; _onError = this._register(new Emitter()); onError = this._onError.event; _onExit = this._register(new Emitter()); onExit = this._onExit.event; _process = null; _hasExited = false; constructor(id, _logService) { super(); this.id = id; this._logService = _logService; } start(opts) { if (platform.isCI) { this._logService.info(`Calling fork to start extension host...`); } const sw = StopWatch.create(false); this._process = fork( FileAccess.asFileUri('bootstrap-fork').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} (fork() took ${forkTime} ms).`); 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, signal) => { this._hasExited = true; this._onExit.fire({ pid, code, signal }); }); } enableInspectPort() { if (!this._process) { return false; } this._logService.info(`Enabling inspect port on extension host with pid ${this._process.pid}.`); 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() { if (!this._process) { return; } this._logService.info(`Killing extension host with pid ${this._process.pid}.`); this._process.kill(); } async waitForExit(maxWaitTimeMs) { 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(); } } }; ExtensionHostProcess = __decorate([__param(1, ILogService)], ExtensionHostProcess); let UtilityExtensionHostProcess = class UtilityExtensionHostProcess extends Disposable { id; _logService; _windowsMainService; onError = Event.None; _onStdout = this._register(new Emitter()); onStdout = this._onStdout.event; _onStderr = this._register(new Emitter()); onStderr = this._onStderr.event; _onMessage = this._register(new Emitter()); onMessage = this._onMessage.event; _onExit = this._register(new Emitter()); onExit = this._onExit.event; _process = null; _hasExited = false; constructor(id, _logService, _windowsMainService) { super(); this.id = id; this._logService = _logService; this._windowsMainService = _windowsMainService; } start(opts) { const codeWindow = this._windowsMainService.getWindowById(opts.responseWindowId); if (!codeWindow) { this._logService.info( `UtilityProcess<${this.id}>: Refusing to create new Extension Host UtilityProcess because requesting window cannot be found...`, ); return; } const responseWindow = codeWindow.win; if (!responseWindow || responseWindow.isDestroyed() || responseWindow.webContents.isDestroyed()) { this._logService.info( `UtilityProcess<${this.id}>: Refusing to create new Extension Host UtilityProcess because requesting window cannot be found...`, ); return; } const serviceName = `extensionHost${this.id}`; const modulePath = FileAccess.asFileUri('bootstrap-fork.js').fsPath; const args = ['--type=extensionHost', '--skipWorkspaceStorageLock']; const execArgv = opts.execArgv || []; const env = { ...opts.env }; const allowLoadingUnsignedLibraries = true; const stdio = 'pipe'; // 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(`UtilityProcess<${this.id}>: Creating new...`); this._process = UtilityProcess.fork(modulePath, args, { serviceName, env, execArgv, allowLoadingUnsignedLibraries, stdio, }); 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._register( Event.fromNodeEventEmitter( this._process, 'spawn', )(() => { this._logService.info(`UtilityProcess<${this.id}>: received spawn event.`); }), ); const onExit = Event.fromNodeEventEmitter(this._process, 'exit', (code) => code); this._register( onExit((code) => { this._logService.info(`UtilityProcess<${this.id}>: received exit event with code ${code}.`); this._hasExited = true; this._onExit.fire({ pid: this._process.pid, code, signal: '' }); }), ); const { port1, port2 } = new electron.MessageChannelMain(); this._process.postMessage('null', [port2]); responseWindow.webContents.postMessage(opts.responseChannel, opts.responseNonce, [port1]); } enableInspectPort() { if (!this._process) { return false; } this._logService.info( `UtilityProcess<${this.id}>: Enabling inspect port on extension host with pid ${this._process.pid}.`, ); if (typeof process._debugProcess === 'function') { // use (undocumented) _debugProcess feature of node process._debugProcess(this._process.pid); return true; } else { // not supported... return false; } } kill() { if (!this._process) { return; } this._logService.info(`UtilityProcess<${this.id}>: Killing extension host with pid ${this._process.pid}.`); this._process.kill(); } async waitForExit(maxWaitTimeMs) { if (!this._process) { return; } const pid = this._process.pid; this._logService.info(`UtilityProcess<${this.id}>: 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( `UtilityProcess<${this.id}>: Extension host with pid ${pid} did not exit within ${maxWaitTimeMs}ms, will kill it now.`, ); this._process.kill(); } } }; UtilityExtensionHostProcess = __decorate( [__param(1, ILogService), __param(2, IWindowsMainService)], UtilityExtensionHostProcess, );