Path: blob/main/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts
5252 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { timeout } from '../../../../base/common/async.js';6import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';7import { CancellationError } from '../../../../base/common/errors.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';10import * as objects from '../../../../base/common/objects.js';11import * as platform from '../../../../base/common/platform.js';12import { removeDangerousEnvVariables } from '../../../../base/common/processes.js';13import { StopWatch } from '../../../../base/common/stopwatch.js';14import { URI } from '../../../../base/common/uri.js';15import { generateUuid } from '../../../../base/common/uuid.js';16import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js';17import { BufferedEmitter } from '../../../../base/parts/ipc/common/ipc.net.js';18import { acquirePort } from '../../../../base/parts/ipc/electron-browser/ipc.mp.js';19import * as nls from '../../../../nls.js';20import { IExtensionHostDebugService } from '../../../../platform/debug/common/extensionHostDebug.js';21import { IExtensionHostProcessOptions, IExtensionHostStarter } from '../../../../platform/extensions/common/extensionHostStarter.js';22import { ILabelService } from '../../../../platform/label/common/label.js';23import { ILogService, ILoggerService } from '../../../../platform/log/common/log.js';24import { INativeHostService } from '../../../../platform/native/common/native.js';25import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';26import { IProductService } from '../../../../platform/product/common/productService.js';27import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';28import { isLoggingOnly } from '../../../../platform/telemetry/common/telemetryUtils.js';29import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';30import { IWorkspaceContextService, WorkbenchState, isUntitledWorkspace } from '../../../../platform/workspace/common/workspace.js';31import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js';32import { IShellEnvironmentService } from '../../environment/electron-browser/shellEnvironmentService.js';33import { MessagePortExtHostConnection, writeExtHostConnection } from '../common/extensionHostEnv.js';34import { IExtensionHostInitData, MessageType, NativeLogMarkers, UIKind, isMessageOfType } from '../common/extensionHostProtocol.js';35import { LocalProcessRunningLocation } from '../common/extensionRunningLocation.js';36import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionInspectInfo } from '../common/extensions.js';37import { IHostService } from '../../host/browser/host.js';38import { ILifecycleService, WillShutdownEvent } from '../../lifecycle/common/lifecycle.js';39import { parseExtensionDevOptions } from '../common/extensionDevOptions.js';40import { IDefaultLogLevelsService } from '../../log/common/defaultLogLevels.js';4142export interface ILocalProcessExtensionHostInitData {43readonly extensions: ExtensionHostExtensions;44}4546export interface ILocalProcessExtensionHostDataProvider {47getInitData(): Promise<ILocalProcessExtensionHostInitData>;48}4950export class ExtensionHostProcess {5152private readonly _id: string;5354public get onStdout(): Event<string> {55return this._extensionHostStarter.onDynamicStdout(this._id);56}5758public get onStderr(): Event<string> {59return this._extensionHostStarter.onDynamicStderr(this._id);60}6162public get onMessage(): Event<unknown> {63return this._extensionHostStarter.onDynamicMessage(this._id);64}6566public get onExit(): Event<{ code: number; signal: string }> {67return this._extensionHostStarter.onDynamicExit(this._id);68}6970constructor(71id: string,72private readonly _extensionHostStarter: IExtensionHostStarter,73) {74this._id = id;75}7677public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number | undefined }> {78return this._extensionHostStarter.start(this._id, opts);79}8081public enableInspectPort(): Promise<boolean> {82return this._extensionHostStarter.enableInspectPort(this._id);83}8485public kill(): Promise<void> {86return this._extensionHostStarter.kill(this._id);87}88}8990export class NativeLocalProcessExtensionHost extends Disposable implements IExtensionHost {9192public pid: number | null = null;93public readonly remoteAuthority = null;94public extensions: ExtensionHostExtensions | null = null;9596private readonly _onExit: Emitter<[number, string]> = this._register(new Emitter<[number, string]>());97public readonly onExit: Event<[number, string]> = this._onExit.event;9899private readonly _onDidSetInspectPort = this._register(new Emitter<void>());100101102private readonly _isExtensionDevHost: boolean;103private readonly _isExtensionDevDebug: boolean;104private readonly _isExtensionDevDebugBrk: boolean;105private readonly _isExtensionDevTestFromCli: boolean;106107// State108private _terminating: boolean;109110// Resources, in order they get acquired/created when .start() is called:111private _inspectListener: IExtensionInspectInfo | null;112private _extensionHostProcess: ExtensionHostProcess | null;113private _messageProtocol: Promise<IMessagePassingProtocol> | null;114115constructor(116public readonly runningLocation: LocalProcessRunningLocation,117public readonly startup: ExtensionHostStartup.EagerAutoStart | ExtensionHostStartup.EagerManualStart,118private readonly _initDataProvider: ILocalProcessExtensionHostDataProvider,119@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,120@INotificationService private readonly _notificationService: INotificationService,121@INativeHostService private readonly _nativeHostService: INativeHostService,122@ILifecycleService private readonly _lifecycleService: ILifecycleService,123@INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService,124@IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService,125@ITelemetryService private readonly _telemetryService: ITelemetryService,126@ILogService private readonly _logService: ILogService,127@ILoggerService private readonly _loggerService: ILoggerService,128@ILabelService private readonly _labelService: ILabelService,129@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService,130@IHostService private readonly _hostService: IHostService,131@IProductService private readonly _productService: IProductService,132@IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService,133@IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter,134@IDefaultLogLevelsService private readonly _defaultLogLevelsService: IDefaultLogLevelsService,135) {136super();137const devOpts = parseExtensionDevOptions(this._environmentService);138this._isExtensionDevHost = devOpts.isExtensionDevHost;139this._isExtensionDevDebug = devOpts.isExtensionDevDebug;140this._isExtensionDevDebugBrk = devOpts.isExtensionDevDebugBrk;141this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;142143this._terminating = false;144145this._inspectListener = null;146this._extensionHostProcess = null;147this._messageProtocol = null;148149this._register(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));150this._register(this._extensionHostDebugService.onClose(event => {151if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {152this._nativeHostService.closeWindow();153}154}));155this._register(this._extensionHostDebugService.onReload(event => {156if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {157this._hostService.reload();158}159}));160}161162public override dispose(): void {163if (this._terminating) {164return;165}166this._terminating = true;167super.dispose();168this._messageProtocol = null;169}170171public start(): Promise<IMessagePassingProtocol> {172if (this._terminating) {173// .terminate() was called174throw new CancellationError();175}176177if (!this._messageProtocol) {178this._messageProtocol = this._start();179}180181return this._messageProtocol;182}183184private async _start(): Promise<IMessagePassingProtocol> {185const [extensionHostCreationResult, portNumber, processEnv] = await Promise.all([186this._extensionHostStarter.createExtensionHost(),187this._tryFindDebugPort(),188this._shellEnvironmentService.getShellEnv(),189]);190191this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter);192193const env = objects.mixin(processEnv, {194VSCODE_ESM_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess',195VSCODE_HANDLES_UNCAUGHT_ERRORS: true196});197198if (this._environmentService.debugExtensionHost.env) {199objects.mixin(env, this._environmentService.debugExtensionHost.env);200}201202removeDangerousEnvVariables(env);203204if (this._isExtensionDevHost) {205// Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might206// be that dependencies, that otherwise would be cached, get modified.207delete env['VSCODE_CODE_CACHE_PATH'];208}209210const opts: IExtensionHostProcessOptions = {211responseWindowId: this._nativeHostService.windowId,212responseChannel: 'vscode:startExtensionHostMessagePortResult',213responseNonce: generateUuid(),214env,215// We only detach the extension host on windows. Linux and Mac orphan by default216// and detach under Linux and Mac create another process group.217// We detach because we have noticed that when the renderer exits, its child processes218// (i.e. extension host) are taken down in a brutal fashion by the OS219detached: !!platform.isWindows,220execArgv: undefined as string[] | undefined,221silent: true222};223224const inspectHost = '127.0.0.1';225if (portNumber !== 0) {226opts.execArgv = [227'--nolazy',228(this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + `${inspectHost}:${portNumber}`229];230} else {231opts.execArgv = ['--inspect-port=0'];232}233234if (this._environmentService.extensionTestsLocationURI) {235opts.execArgv.unshift('--expose-gc');236}237238if (this._environmentService.args['prof-v8-extensions']) {239opts.execArgv.unshift('--prof');240}241242// Refs https://github.com/microsoft/vscode/issues/189805243//244// Enable experimental network inspection245// inspector agent is always setup hence add this flag246// unconditionally.247opts.execArgv.unshift('--dns-result-order=ipv4first', '--experimental-network-inspection');248249// Catch all output coming from the extension host process250type Output = { data: string; format: string[] };251const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout);252const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr);253const onOutput = Event.any(254Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })),255Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] }))256);257258// Debounce all output, so we can render it in the Chrome console as a group259const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {260return r261? { data: r.data + o.data, format: [...r.format, ...o.format] }262: { data: o.data, format: o.format };263}, 100);264265// Print out extension host output266this._register(onDebouncedOutput(output => {267const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+):(\d+)\/([^\s]+)/);268if (inspectorUrlMatch) {269const [, host, port, auth] = inspectorUrlMatch;270const devtoolsUrl = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${host}:${port}/${auth}`;271if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) {272console.debug(`%c[Extension Host] %cdebugger inspector at ${devtoolsUrl}`, 'color: blue', 'color:');273}274if (!this._inspectListener || !this._inspectListener.devtoolsUrl) {275this._inspectListener = { host, port: Number(port), devtoolsUrl };276this._onDidSetInspectPort.fire();277}278} else {279if (!this._isExtensionDevTestFromCli) {280console.group('Extension Host');281console.log(output.data, ...output.format);282console.groupEnd();283}284}285}));286287// Lifecycle288289this._register(this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)));290291// Notify debugger that we are ready to attach to the process if we run a development extension292if (portNumber) {293if (this._isExtensionDevHost && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {294this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber);295}296this._inspectListener = { port: portNumber, host: inspectHost };297this._onDidSetInspectPort.fire();298}299300// Help in case we fail to start it301let startupTimeoutHandle: Timeout | undefined;302if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) {303startupTimeoutHandle = setTimeout(() => {304this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`);305306const msg = this._isExtensionDevDebugBrk307? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.")308: nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem.");309310this._notificationService.prompt(Severity.Warning, msg,311[{312label: nls.localize('reloadWindow', "Reload Window"),313run: () => this._hostService.reload()314}],315{316sticky: true,317priority: NotificationPriority.URGENT318}319);320}, 10000);321}322323// Initialize extension host process with hand shakes324const protocol = await this._establishProtocol(this._extensionHostProcess, opts);325await this._performHandshake(protocol);326clearTimeout(startupTimeoutHandle);327return protocol;328}329330/**331* Find a free port if extension host debugging is enabled.332*/333private async _tryFindDebugPort(): Promise<number> {334335if (typeof this._environmentService.debugExtensionHost.port !== 'number') {336return 0;337}338339const expected = this._environmentService.debugExtensionHost.port;340const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */);341342if (!this._isExtensionDevTestFromCli) {343if (!port) {344console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:');345} else {346if (port !== expected) {347console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:');348}349if (this._isExtensionDevDebugBrk) {350console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:');351} else {352console.debug(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:');353}354}355}356357return port || 0;358}359360private _establishProtocol(extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise<IMessagePassingProtocol> {361362writeExtHostConnection(new MessagePortExtHostConnection(), opts.env);363364// Get ready to acquire the message port from the shared process worker365const portPromise = acquirePort(undefined /* we trigger the request via service call! */, opts.responseChannel, opts.responseNonce);366367return new Promise<IMessagePassingProtocol>((resolve, reject) => {368369const handle = setTimeout(() => {370reject('The local extension host took longer than 60s to connect.');371}, 60 * 1000);372373portPromise.then((port) => {374this._register(toDisposable(() => {375// Close the message port when the extension host is disposed376port.close();377port.onmessage = null;378}));379clearTimeout(handle);380381const onMessage = new BufferedEmitter<VSBuffer>();382port.onmessage = ((e) => {383if (e.data) {384onMessage.fire(VSBuffer.wrap(e.data));385}386});387port.start();388389resolve({390onMessage: onMessage.event,391send: message => port.postMessage(message.buffer),392});393});394395// Now that the message port listener is installed, start the ext host process396const sw = StopWatch.create(false);397extensionHostProcess.start(opts).then(({ pid }) => {398if (pid) {399this.pid = pid;400}401this._logService.info(`Started local extension host with pid ${pid}.`);402const duration = sw.elapsed();403if (platform.isCI) {404this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`);405}406}, (err) => {407// Starting the ext host process resulted in an error408reject(err);409});410});411}412413private _performHandshake(protocol: IMessagePassingProtocol): Promise<void> {414// 1) wait for the incoming `ready` event and send the initialization data.415// 2) wait for the incoming `initialized` event.416return new Promise<void>((resolve, reject) => {417418let timeoutHandle: Timeout;419const installTimeoutCheck = () => {420timeoutHandle = setTimeout(() => {421reject('The local extension host took longer than 60s to send its ready message.');422}, 60 * 1000);423};424const uninstallTimeoutCheck = () => {425clearTimeout(timeoutHandle);426};427428// Wait 60s for the ready message429installTimeoutCheck();430431const disposable = protocol.onMessage(msg => {432433if (isMessageOfType(msg, MessageType.Ready)) {434435// 1) Extension Host is ready to receive messages, initialize it436uninstallTimeoutCheck();437438this._createExtHostInitData().then(data => {439440// Wait 60s for the initialized message441installTimeoutCheck();442443protocol.send(VSBuffer.fromString(JSON.stringify(data)));444});445return;446}447448if (isMessageOfType(msg, MessageType.Initialized)) {449450// 2) Extension Host is initialized451uninstallTimeoutCheck();452453// stop listening for messages here454disposable.dispose();455456// release this promise457resolve();458return;459}460461console.error(`received unexpected message during handshake phase from the extension host: `, msg);462});463464});465}466467private async _createExtHostInitData(): Promise<IExtensionHostInitData> {468const initData = await this._initDataProvider.getInitData();469this.extensions = initData.extensions;470const workspace = this._contextService.getWorkspace();471return {472commit: this._productService.commit,473version: this._productService.version,474quality: this._productService.quality,475date: this._productService.date,476parentPid: 0,477environment: {478isExtensionDevelopmentDebug: this._isExtensionDevDebug,479appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,480appName: this._productService.nameLong,481appHost: this._productService.embedderIdentifier || 'desktop',482appUriScheme: this._productService.urlProtocol,483isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService),484isPortable: this._environmentService.isPortable,485appLanguage: platform.language,486extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,487extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,488globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,489workspaceStorageHome: this._environmentService.workspaceStorageHome,490extensionLogLevel: this._defaultLogLevelsService.defaultLogLevels.extensions491},492workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {493configuration: workspace.configuration ?? undefined,494id: workspace.id,495name: this._labelService.getWorkspaceLabel(workspace),496isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false,497transient: workspace.transient,498isAgentSessionsWorkspace: workspace.isAgentSessionsWorkspace499},500remote: {501authority: this._environmentService.remoteAuthority,502connectionData: null,503isRemote: false504},505consoleForward: {506includeStack: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose),507logNative: !this._isExtensionDevTestFromCli && this._isExtensionDevHost508},509extensions: this.extensions.toSnapshot(),510telemetryInfo: {511sessionId: this._telemetryService.sessionId,512machineId: this._telemetryService.machineId,513sqmId: this._telemetryService.sqmId,514devDeviceId: this._telemetryService.devDeviceId ?? this._telemetryService.machineId,515firstSessionDate: this._telemetryService.firstSessionDate,516msftInternal: this._telemetryService.msftInternal517},518remoteExtensionTips: this._productService.remoteExtensionTips,519virtualWorkspaceExtensionTips: this._productService.virtualWorkspaceExtensionTips,520logLevel: this._logService.getLevel(),521loggers: [...this._loggerService.getRegisteredLoggers()],522logsLocation: this._environmentService.extHostLogsPath,523autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart),524uiKind: UIKind.Desktop,525handle: this._environmentService.window.handle ? encodeBase64(this._environmentService.window.handle) : undefined526};527}528529private _onExtHostProcessExit(code: number, signal: string): void {530if (this._terminating) {531// Expected termination path (we asked the process to terminate)532return;533}534535this._onExit.fire([code, signal]);536}537538private _handleProcessOutputStream(stream: Event<string>) {539let last = '';540let isOmitting = false;541const event = new Emitter<string>();542stream((chunk) => {543// not a fancy approach, but this is the same approach used by the split2544// module which is well-optimized (https://github.com/mcollina/split2)545last += chunk;546const lines = last.split(/\r?\n/g);547last = lines.pop()!;548549// protected against an extension spamming and leaking memory if no new line is written.550if (last.length > 10_000) {551lines.push(last);552last = '';553}554555for (const line of lines) {556if (isOmitting) {557if (line === NativeLogMarkers.End) {558isOmitting = false;559}560} else if (line === NativeLogMarkers.Start) {561isOmitting = true;562} else if (line.length) {563event.fire(line + '\n');564}565}566}, undefined, this._store);567568return event;569}570571public async enableInspectPort(): Promise<boolean> {572if (!!this._inspectListener) {573return true;574}575576if (!this._extensionHostProcess) {577return false;578}579580const result = await this._extensionHostProcess.enableInspectPort();581if (!result) {582return false;583}584585await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]);586return !!this._inspectListener;587}588589public getInspectPort(): IExtensionInspectInfo | undefined {590return this._inspectListener ?? undefined;591}592593private _onWillShutdown(event: WillShutdownEvent): void {594// If the extension development host was started without debugger attached we need595// to communicate this back to the main side to terminate the debug session596if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {597this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId);598event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") });599}600}601}602603604