Path: blob/main/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts
3296 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 { DisposableStore, 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';4041export interface ILocalProcessExtensionHostInitData {42readonly extensions: ExtensionHostExtensions;43}4445export interface ILocalProcessExtensionHostDataProvider {46getInitData(): Promise<ILocalProcessExtensionHostInitData>;47}4849export class ExtensionHostProcess {5051private readonly _id: string;5253public get onStdout(): Event<string> {54return this._extensionHostStarter.onDynamicStdout(this._id);55}5657public get onStderr(): Event<string> {58return this._extensionHostStarter.onDynamicStderr(this._id);59}6061public get onMessage(): Event<any> {62return this._extensionHostStarter.onDynamicMessage(this._id);63}6465public get onExit(): Event<{ code: number; signal: string }> {66return this._extensionHostStarter.onDynamicExit(this._id);67}6869constructor(70id: string,71private readonly _extensionHostStarter: IExtensionHostStarter,72) {73this._id = id;74}7576public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number | undefined }> {77return this._extensionHostStarter.start(this._id, opts);78}7980public enableInspectPort(): Promise<boolean> {81return this._extensionHostStarter.enableInspectPort(this._id);82}8384public kill(): Promise<void> {85return this._extensionHostStarter.kill(this._id);86}87}8889export class NativeLocalProcessExtensionHost implements IExtensionHost {9091public pid: number | null = null;92public readonly remoteAuthority = null;93public extensions: ExtensionHostExtensions | null = null;9495private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>();96public readonly onExit: Event<[number, string]> = this._onExit.event;9798private readonly _onDidSetInspectPort = new Emitter<void>();99100private readonly _toDispose = new DisposableStore();101102private 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) {135const devOpts = parseExtensionDevOptions(this._environmentService);136this._isExtensionDevHost = devOpts.isExtensionDevHost;137this._isExtensionDevDebug = devOpts.isExtensionDevDebug;138this._isExtensionDevDebugBrk = devOpts.isExtensionDevDebugBrk;139this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;140141this._terminating = false;142143this._inspectListener = null;144this._extensionHostProcess = null;145this._messageProtocol = null;146147this._toDispose.add(this._onExit);148this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));149this._toDispose.add(this._extensionHostDebugService.onClose(event => {150if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {151this._nativeHostService.closeWindow();152}153}));154this._toDispose.add(this._extensionHostDebugService.onReload(event => {155if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) {156this._hostService.reload();157}158}));159}160161public dispose(): void {162if (this._terminating) {163return;164}165this._terminating = true;166167this._toDispose.dispose();168}169170public start(): Promise<IMessagePassingProtocol> {171if (this._terminating) {172// .terminate() was called173throw new CancellationError();174}175176if (!this._messageProtocol) {177this._messageProtocol = this._start();178}179180return this._messageProtocol;181}182183private async _start(): Promise<IMessagePassingProtocol> {184const [extensionHostCreationResult, portNumber, processEnv] = await Promise.all([185this._extensionHostStarter.createExtensionHost(),186this._tryFindDebugPort(),187this._shellEnvironmentService.getShellEnv(),188]);189190this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter);191192const env = objects.mixin(processEnv, {193VSCODE_ESM_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess',194VSCODE_HANDLES_UNCAUGHT_ERRORS: true195});196197if (this._environmentService.debugExtensionHost.env) {198objects.mixin(env, this._environmentService.debugExtensionHost.env);199}200201removeDangerousEnvVariables(env);202203if (this._isExtensionDevHost) {204// Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might205// be that dependencies, that otherwise would be cached, get modified.206delete env['VSCODE_CODE_CACHE_PATH'];207}208209const opts: IExtensionHostProcessOptions = {210responseWindowId: this._nativeHostService.windowId,211responseChannel: 'vscode:startExtensionHostMessagePortResult',212responseNonce: generateUuid(),213env,214// We only detach the extension host on windows. Linux and Mac orphan by default215// and detach under Linux and Mac create another process group.216// We detach because we have noticed that when the renderer exits, its child processes217// (i.e. extension host) are taken down in a brutal fashion by the OS218detached: !!platform.isWindows,219execArgv: undefined as string[] | undefined,220silent: true221};222223const inspectHost = '127.0.0.1';224if (portNumber !== 0) {225opts.execArgv = [226'--nolazy',227(this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + `${inspectHost}:${portNumber}`228];229} else {230opts.execArgv = ['--inspect-port=0'];231}232233if (this._environmentService.extensionTestsLocationURI) {234opts.execArgv.unshift('--expose-gc');235}236237if (this._environmentService.args['prof-v8-extensions']) {238opts.execArgv.unshift('--prof');239}240241// Refs https://github.com/microsoft/vscode/issues/189805242//243// Enable experimental network inspection244// inspector agent is always setup hence add this flag245// unconditionally.246opts.execArgv.unshift('--dns-result-order=ipv4first', '--experimental-network-inspection');247248// Catch all output coming from the extension host process249type Output = { data: string; format: string[] };250const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout, this._toDispose);251const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr, this._toDispose);252const onOutput = Event.any(253Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })),254Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] }))255);256257// Debounce all output, so we can render it in the Chrome console as a group258const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {259return r260? { data: r.data + o.data, format: [...r.format, ...o.format] }261: { data: o.data, format: o.format };262}, 100);263264// Print out extension host output265this._toDispose.add(onDebouncedOutput(output => {266const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+):(\d+)\/([^\s]+)/);267if (inspectorUrlMatch) {268const [, host, port, auth] = inspectorUrlMatch;269const devtoolsUrl = `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${host}:${port}/${auth}`;270if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) {271console.log(`%c[Extension Host] %cdebugger inspector at ${devtoolsUrl}`, 'color: blue', 'color:');272}273if (!this._inspectListener || !this._inspectListener.devtoolsUrl) {274this._inspectListener = { host, port: Number(port), devtoolsUrl };275this._onDidSetInspectPort.fire();276}277} else {278if (!this._isExtensionDevTestFromCli) {279console.group('Extension Host');280console.log(output.data, ...output.format);281console.groupEnd();282}283}284}));285286// Lifecycle287288this._toDispose.add(this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)));289290// Notify debugger that we are ready to attach to the process if we run a development extension291if (portNumber) {292if (this._isExtensionDevHost && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {293this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber);294}295this._inspectListener = { port: portNumber, host: inspectHost };296this._onDidSetInspectPort.fire();297}298299// Help in case we fail to start it300let startupTimeoutHandle: Timeout | undefined;301if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) {302startupTimeoutHandle = setTimeout(() => {303this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`);304305const msg = this._isExtensionDevDebugBrk306? 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.")307: nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem.");308309this._notificationService.prompt(Severity.Warning, msg,310[{311label: nls.localize('reloadWindow', "Reload Window"),312run: () => this._hostService.reload()313}],314{315sticky: true,316priority: NotificationPriority.URGENT317}318);319}, 10000);320}321322// Initialize extension host process with hand shakes323const protocol = await this._establishProtocol(this._extensionHostProcess, opts);324await this._performHandshake(protocol);325clearTimeout(startupTimeoutHandle);326return protocol;327}328329/**330* Find a free port if extension host debugging is enabled.331*/332private async _tryFindDebugPort(): Promise<number> {333334if (typeof this._environmentService.debugExtensionHost.port !== 'number') {335return 0;336}337338const expected = this._environmentService.debugExtensionHost.port;339const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */);340341if (!this._isExtensionDevTestFromCli) {342if (!port) {343console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:');344} else {345if (port !== expected) {346console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:');347}348if (this._isExtensionDevDebugBrk) {349console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:');350} else {351console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:');352}353}354}355356return port || 0;357}358359private _establishProtocol(extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise<IMessagePassingProtocol> {360361writeExtHostConnection(new MessagePortExtHostConnection(), opts.env);362363// Get ready to acquire the message port from the shared process worker364const portPromise = acquirePort(undefined /* we trigger the request via service call! */, opts.responseChannel, opts.responseNonce);365366return new Promise<IMessagePassingProtocol>((resolve, reject) => {367368const handle = setTimeout(() => {369reject('The local extension host took longer than 60s to connect.');370}, 60 * 1000);371372portPromise.then((port) => {373this._toDispose.add(toDisposable(() => {374// Close the message port when the extension host is disposed375port.close();376}));377clearTimeout(handle);378379const onMessage = new BufferedEmitter<VSBuffer>();380port.onmessage = ((e) => {381if (e.data) {382onMessage.fire(VSBuffer.wrap(e.data));383}384});385port.start();386387resolve({388onMessage: onMessage.event,389send: message => port.postMessage(message.buffer),390});391});392393// Now that the message port listener is installed, start the ext host process394const sw = StopWatch.create(false);395extensionHostProcess.start(opts).then(({ pid }) => {396if (pid) {397this.pid = pid;398}399this._logService.info(`Started local extension host with pid ${pid}.`);400const duration = sw.elapsed();401if (platform.isCI) {402this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`);403}404}, (err) => {405// Starting the ext host process resulted in an error406reject(err);407});408});409}410411private _performHandshake(protocol: IMessagePassingProtocol): Promise<void> {412// 1) wait for the incoming `ready` event and send the initialization data.413// 2) wait for the incoming `initialized` event.414return new Promise<void>((resolve, reject) => {415416let timeoutHandle: Timeout;417const installTimeoutCheck = () => {418timeoutHandle = setTimeout(() => {419reject('The local extension host took longer than 60s to send its ready message.');420}, 60 * 1000);421};422const uninstallTimeoutCheck = () => {423clearTimeout(timeoutHandle);424};425426// Wait 60s for the ready message427installTimeoutCheck();428429const disposable = protocol.onMessage(msg => {430431if (isMessageOfType(msg, MessageType.Ready)) {432433// 1) Extension Host is ready to receive messages, initialize it434uninstallTimeoutCheck();435436this._createExtHostInitData().then(data => {437438// Wait 60s for the initialized message439installTimeoutCheck();440441protocol.send(VSBuffer.fromString(JSON.stringify(data)));442});443return;444}445446if (isMessageOfType(msg, MessageType.Initialized)) {447448// 2) Extension Host is initialized449uninstallTimeoutCheck();450451// stop listening for messages here452disposable.dispose();453454// release this promise455resolve();456return;457}458459console.error(`received unexpected message during handshake phase from the extension host: `, msg);460});461462});463}464465private async _createExtHostInitData(): Promise<IExtensionHostInitData> {466const initData = await this._initDataProvider.getInitData();467this.extensions = initData.extensions;468const workspace = this._contextService.getWorkspace();469return {470commit: this._productService.commit,471version: this._productService.version,472quality: this._productService.quality,473date: this._productService.date,474parentPid: 0,475environment: {476isExtensionDevelopmentDebug: this._isExtensionDevDebug,477appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,478appName: this._productService.nameLong,479appHost: this._productService.embedderIdentifier || 'desktop',480appUriScheme: this._productService.urlProtocol,481isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService),482appLanguage: platform.language,483extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,484extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,485globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,486workspaceStorageHome: this._environmentService.workspaceStorageHome,487extensionLogLevel: this._environmentService.extensionLogLevel488},489workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {490configuration: workspace.configuration ?? undefined,491id: workspace.id,492name: this._labelService.getWorkspaceLabel(workspace),493isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false,494transient: workspace.transient495},496remote: {497authority: this._environmentService.remoteAuthority,498connectionData: null,499isRemote: false500},501consoleForward: {502includeStack: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose),503logNative: !this._isExtensionDevTestFromCli && this._isExtensionDevHost504},505extensions: this.extensions.toSnapshot(),506telemetryInfo: {507sessionId: this._telemetryService.sessionId,508machineId: this._telemetryService.machineId,509sqmId: this._telemetryService.sqmId,510devDeviceId: this._telemetryService.devDeviceId,511firstSessionDate: this._telemetryService.firstSessionDate,512msftInternal: this._telemetryService.msftInternal513},514logLevel: this._logService.getLevel(),515loggers: [...this._loggerService.getRegisteredLoggers()],516logsLocation: this._environmentService.extHostLogsPath,517autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart),518uiKind: UIKind.Desktop,519handle: this._environmentService.window.handle ? encodeBase64(this._environmentService.window.handle) : undefined520};521}522523private _onExtHostProcessExit(code: number, signal: string): void {524if (this._terminating) {525// Expected termination path (we asked the process to terminate)526return;527}528529this._onExit.fire([code, signal]);530}531532private _handleProcessOutputStream(stream: Event<string>, store: DisposableStore) {533let last = '';534let isOmitting = false;535const event = new Emitter<string>();536stream((chunk) => {537// not a fancy approach, but this is the same approach used by the split2538// module which is well-optimized (https://github.com/mcollina/split2)539last += chunk;540const lines = last.split(/\r?\n/g);541last = lines.pop()!;542543// protected against an extension spamming and leaking memory if no new line is written.544if (last.length > 10_000) {545lines.push(last);546last = '';547}548549for (const line of lines) {550if (isOmitting) {551if (line === NativeLogMarkers.End) {552isOmitting = false;553}554} else if (line === NativeLogMarkers.Start) {555isOmitting = true;556} else if (line.length) {557event.fire(line + '\n');558}559}560}, undefined, store);561562return event;563}564565public async enableInspectPort(): Promise<boolean> {566if (!!this._inspectListener) {567return true;568}569570if (!this._extensionHostProcess) {571return false;572}573574const result = await this._extensionHostProcess.enableInspectPort();575if (!result) {576return false;577}578579await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]);580return !!this._inspectListener;581}582583public getInspectPort(): IExtensionInspectInfo | undefined {584return this._inspectListener ?? undefined;585}586587private _onWillShutdown(event: WillShutdownEvent): void {588// If the extension development host was started without debugger attached we need589// to communicate this back to the main side to terminate the debug session590if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {591this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId);592event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") });593}594}595}596597598