diff --git a/README.ja.md b/README.ja.md index bd97612f85d..627d0e3a7b9 100644 --- a/README.ja.md +++ b/README.ja.md @@ -82,6 +82,8 @@ VS Codeのフォーク。本家にはないUI/UX改善を追加した(してい | `coderm.workbench.editor.disableGroupLock` | `boolean` | `true` | エディタグループのロック機能を完全に無効化。グループは自動・手動を問わずロックできず、常にロック解除状態で動作 | | `coderm.languageHost.enabled` | `boolean` | `false` | (実験的)ネイティブ(Rust)Language Host を有効化。設定した言語で tree-sitter ベースの documentSymbol・foldingRange・hover・definition・references・documentHighlights を提供(Phase 5) | | `coderm.languageHost.languages` | `array` | `[]` | (実験的)ネイティブ Host が扱う言語 ID(例: "typescript", "tsx")。空の場合は機能無効 | +| `coderm.languageHost.isolatedEnabled` | `boolean` | `false` | (実験的)`isolatedExtensions` に列挙した拡張を、メインのローカルプロセス拡張ホストから分離した専用拡張ホストプロセスで実行(Phase 6) | +| `coderm.languageHost.isolatedExtensions` | `array` | `[]` | (実験的)`isolatedEnabled` が有効な場合に、分離拡張ホストにルーティングする拡張 ID(Phase 6) | --- diff --git a/README.md b/README.md index 4941985d857..c7105086fd7 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ Settings unique to Coderm that are not available in upstream VS Code. | `coderm.workbench.editor.disableGroupLock` | `boolean` | `true` | Completely disable the editor group lock feature — groups can never be locked (automatically or manually) and always behave as unlocked | | `coderm.languageHost.enabled` | `boolean` | `false` | _(experimental)_ Enable the native (Rust) Language Host. Provides tree-sitter-backed documentSymbol, foldingRange, hover, definition, references, and document highlights for the configured languages (Phase 5) | | `coderm.languageHost.languages` | `array` | `[]` | _(experimental)_ Language IDs handled by the native host (e.g. "typescript", "tsx"). Empty keeps the feature inert | +| `coderm.languageHost.isolatedEnabled` | `boolean` | `false` | _(experimental)_ Run the extensions listed in `isolatedExtensions` inside a dedicated extension host process, isolated from the main local process extension host (Phase 6) | +| `coderm.languageHost.isolatedExtensions` | `array` | `[]` | _(experimental)_ Extension IDs to route into the isolated extension host when `isolatedEnabled` is true (Phase 6) | --- diff --git a/src/vs/platform/extensions/common/extensionHostStarter.ts b/src/vs/platform/extensions/common/extensionHostStarter.ts index 81574ba47f9..d55718b70c9 100644 --- a/src/vs/platform/extensions/common/extensionHostStarter.ts +++ b/src/vs/platform/extensions/common/extensionHostStarter.ts @@ -19,6 +19,13 @@ export interface IExtensionHostProcessOptions { detached: boolean; execArgv: string[] | undefined; silent: boolean; + // --- Coderm start: isolated language EH kind --- + // When set to 'isolatedExtensionHost' the spawned utility process is named + // 'isolated-extension-host' (visible in Process Explorer). Optional: the main + // process falls back to the regular extension-host name when undefined, so the + // local process extension host is unaffected. + kind?: 'extensionHost' | 'isolatedExtensionHost'; + // --- Coderm end --- } export interface IExtensionHostStarter { diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index d5cddd2c8cd..e10bd7acba4 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -114,7 +114,9 @@ export class ExtensionHostStarter extends Disposable implements IDisposable, IEx extHost.start({ ...opts, type: 'extensionHost', - name: 'extension-host', + // --- Coderm start: isolated language EH kind --- + name: opts.kind === 'isolatedExtensionHost' ? 'isolated-extension-host' : 'extension-host', + // --- Coderm end --- entryPoint: 'vs/workbench/api/node/extensionHostProcess', args, execArgv: opts.execArgv, diff --git a/src/vs/workbench/contrib/coderm/browser/languageHostConfiguration.ts b/src/vs/workbench/contrib/coderm/browser/languageHostConfiguration.ts index 4f596274321..9d58f9d500c 100644 --- a/src/vs/workbench/contrib/coderm/browser/languageHostConfiguration.ts +++ b/src/vs/workbench/contrib/coderm/browser/languageHostConfiguration.ts @@ -22,6 +22,10 @@ import { registerLanguageFeatureProviders } from '../../../services/languageHost export const CodermLanguageHostEnabledSetting = 'coderm.languageHost.enabled'; +// --- Coderm start: Phase 6 isolated EH settings --- +export const CodermLanguageHostIsolatedEnabledSetting = 'coderm.languageHost.isolatedEnabled'; +// --- Coderm end --- + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'coderm.languageHost', order: 103, @@ -47,6 +51,27 @@ Registry.as(ConfigurationExtensions.Configuration).regis "Language IDs handled by the native Language Host (e.g. \"typescript\", \"tsx\"). Empty (default) keeps the feature inert."), items: { type: 'string' }, }, + // --- Coderm start: Phase 6 isolated EH settings --- + [CodermLanguageHostIsolatedEnabledSetting]: { + // Note: default:false intentionally deviates from the project's "new settings + // default to enabled" convention (project CLAUDE.md, development rules). This + // spawns an additional extension host process for the listed extensions; keep + // it inert until Phase 6 is proven end-to-end. + type: 'boolean', + default: false, + scope: ConfigurationScope.APPLICATION, + description: localize('coderm.languageHost.isolatedEnabled', + "(experimental, Phase 6) Run the extensions listed in isolatedExtensions inside a dedicated extension host process, isolated from the main local process extension host."), + }, + 'coderm.languageHost.isolatedExtensions': { + type: 'array', + default: [], + scope: ConfigurationScope.APPLICATION, + description: localize('coderm.languageHost.isolatedExtensions', + "(experimental, Phase 6) Extension IDs to route into the isolated extension host when isolatedEnabled is true (e.g. [\"vscode.typescript-language-features\"])."), + items: { type: 'string' }, + }, + // --- Coderm end --- }, }); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index b4f6cf16e7b..6ee2cf0981f 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -242,6 +242,13 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory { case ExtensionHostKind.LocalProcess: { return null; } + // --- Coderm start: isolated language EH kind --- + case ExtensionHostKind.LocalIsolatedProcess: { + // Isolated language EH is a desktop-only feature; the browser host + // never routes extensions to it. + return null; + } + // --- Coderm end --- case ExtensionHostKind.LocalWebWorker: { const startup = ( isInitialStart diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 6a83d64d718..b4e19e4091b 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -43,7 +43,9 @@ import { ExtensionHostManager } from './extensionHostManager.js'; import { IExtensionHostManager } from './extensionHostManagers.js'; import { IResolveAuthorityErrorResult } from './extensionHostProxy.js'; import { IExtensionManifestPropertiesService } from './extensionManifestPropertiesService.js'; -import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from './extensionRunningLocation.js'; +// --- Coderm start: isolated language EH kind --- +import { ExtensionRunningLocation, LocalIsolatedProcessRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from './extensionRunningLocation.js'; +// --- Coderm end --- import { ExtensionRunningLocationTracker, filterExtensionIdentifiers } from './extensionRunningLocationTracker.js'; import { ActivationKind, ActivationTimes, ExtensionActivationReason, ExtensionHostStartup, ExtensionPointContribution, IExtensionHost, IExtensionInspectInfo, IExtensionService, IExtensionsStatus, IInternalExtensionService, IMessage, IResponsiveStateChangeEvent, IWillActivateEvent, WillStopExtensionHostsEvent, toExtension, toExtensionDescription } from './extensions.js'; import { ExtensionsProposedApi } from './extensionsProposedApi.js'; @@ -825,6 +827,15 @@ export abstract class AbstractExtensionService extends Disposable implements IEx for (let affinity = 0; affinity <= this._runningLocations.maxLocalWebWorkerAffinity; affinity++) { locations.push(new LocalWebWorkerRunningLocation(affinity)); } + // --- Coderm start: isolated language EH kind --- + // Only spawn an isolated EH when at least one extension is routed to it. + // Without this guard the default (feature off) would still launch a process. + if (this._runningLocations.hasLocalIsolatedProcessExtensions()) { + for (let affinity = 0; affinity <= this._runningLocations.maxLocalIsolatedProcessAffinity; affinity++) { + locations.push(new LocalIsolatedProcessRunningLocation(affinity)); + } + } + // --- Coderm end --- locations.push(new RemoteRunningLocation()); for (const location of locations) { if (this._extensionHostManagers.getByRunningLocation(location)) { @@ -891,6 +902,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } this._extensionHostManagers.stopOne(extensionHost); } + // --- Coderm start: isolated language EH kind --- + else if (extensionHost.kind === ExtensionHostKind.LocalIsolatedProcess) { + // Isolate the blast radius: stop only the crashed host and leave the + // main/local process extension host untouched. Restart decisions are + // handled by the NativeExtensionService override below. + this._extensionHostManagers.stopOne(extensionHost); + } + // --- Coderm end --- } private _getExtensionHostExitInfoWithTimeout(reconnectionToken: string): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionHostKind.ts b/src/vs/workbench/services/extensions/common/extensionHostKind.ts index 9557a9d8ad7..1e1ef7bd083 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostKind.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostKind.ts @@ -9,7 +9,10 @@ import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform export const enum ExtensionHostKind { LocalProcess = 1, LocalWebWorker = 2, - Remote = 3 + Remote = 3, + // --- Coderm start: isolated language EH kind --- + LocalIsolatedProcess = 4 + // --- Coderm end --- } export function extensionHostKindToString(kind: ExtensionHostKind | null): string { @@ -20,6 +23,9 @@ export function extensionHostKindToString(kind: ExtensionHostKind | null): strin case ExtensionHostKind.LocalProcess: return 'LocalProcess'; case ExtensionHostKind.LocalWebWorker: return 'LocalWebWorker'; case ExtensionHostKind.Remote: return 'Remote'; + // --- Coderm start: isolated language EH kind --- + case ExtensionHostKind.LocalIsolatedProcess: return 'LocalIsolatedProcess'; + // --- Coderm end --- } } diff --git a/src/vs/workbench/services/extensions/common/extensionRunningLocation.ts b/src/vs/workbench/services/extensions/common/extensionRunningLocation.ts index 13e664c13b8..300aed8f19a 100644 --- a/src/vs/workbench/services/extensions/common/extensionRunningLocation.ts +++ b/src/vs/workbench/services/extensions/common/extensionRunningLocation.ts @@ -48,4 +48,24 @@ export class RemoteRunningLocation { } } -export type ExtensionRunningLocation = LocalProcessRunningLocation | LocalWebWorkerRunningLocation | RemoteRunningLocation; +// --- Coderm start: isolated language EH kind --- +export class LocalIsolatedProcessRunningLocation { + public readonly kind = ExtensionHostKind.LocalIsolatedProcess; + constructor( + public readonly affinity: number + ) { } + public equals(other: ExtensionRunningLocation) { + return (this.kind === other.kind && this.affinity === other.affinity); + } + public asString(): string { + if (this.affinity === 0) { + return 'LocalIsolatedProcess'; + } + return `LocalIsolatedProcess${this.affinity}`; + } +} +// --- Coderm end --- + +// --- Coderm start: isolated language EH kind --- +export type ExtensionRunningLocation = LocalProcessRunningLocation | LocalWebWorkerRunningLocation | RemoteRunningLocation | LocalIsolatedProcessRunningLocation; +// --- Coderm end --- diff --git a/src/vs/workbench/services/extensions/common/extensionRunningLocationTracker.ts b/src/vs/workbench/services/extensions/common/extensionRunningLocationTracker.ts index 7025ceb922d..25898bf5b27 100644 --- a/src/vs/workbench/services/extensions/common/extensionRunningLocationTracker.ts +++ b/src/vs/workbench/services/extensions/common/extensionRunningLocationTracker.ts @@ -13,7 +13,9 @@ import { IReadOnlyExtensionDescriptionRegistry } from './extensionDescriptionReg import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, determineExtensionHostKinds } from './extensionHostKind.js'; import { IExtensionHostManager } from './extensionHostManagers.js'; import { IExtensionManifestPropertiesService } from './extensionManifestPropertiesService.js'; -import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from './extensionRunningLocation.js'; +// --- Coderm start: isolated language EH kind --- +import { ExtensionRunningLocation, LocalIsolatedProcessRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from './extensionRunningLocation.js'; +// --- Coderm end --- import { isProposedApiEnabled } from './extensions.js'; export class ExtensionRunningLocationTracker { @@ -21,6 +23,9 @@ export class ExtensionRunningLocationTracker { private _runningLocation = new ExtensionIdentifierMap(); private _maxLocalProcessAffinity: number = 0; private _maxLocalWebWorkerAffinity: number = 0; + // --- Coderm start: isolated language EH kind --- + private _maxLocalIsolatedProcessAffinity: number = 0; + // --- Coderm end --- public get maxLocalProcessAffinity(): number { return this._maxLocalProcessAffinity; @@ -30,6 +35,25 @@ export class ExtensionRunningLocationTracker { return this._maxLocalWebWorkerAffinity; } + // --- Coderm start: isolated language EH kind --- + public get maxLocalIsolatedProcessAffinity(): number { + return this._maxLocalIsolatedProcessAffinity; + } + + // Returns true when at least one extension has been routed to the isolated + // language extension host. AbstractExtensionService uses this to avoid + // spawning an isolated EH process when there is nothing to host, so the + // default (feature off) stays indistinguishable from upstream. + public hasLocalIsolatedProcessExtensions(): boolean { + for (const runningLocation of this._runningLocation.values()) { + if (runningLocation && runningLocation.kind === ExtensionHostKind.LocalIsolatedProcess) { + return true; + } + } + return false; + } + // --- Coderm end --- + constructor( private readonly _registry: IReadOnlyExtensionDescriptionRegistry, private readonly _extensionHostKindPicker: IExtensionHostKindPicker, @@ -230,7 +254,7 @@ export class ExtensionRunningLocationTracker { return this._doComputeRunningLocation(this._runningLocation, localExtensions, remoteExtensions, isInitialAllocation).runningLocation; } - private _doComputeRunningLocation(existingRunningLocation: ExtensionIdentifierMap, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): { runningLocation: ExtensionIdentifierMap; maxLocalProcessAffinity: number; maxLocalWebWorkerAffinity: number } { + private _doComputeRunningLocation(existingRunningLocation: ExtensionIdentifierMap, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): { runningLocation: ExtensionIdentifierMap; maxLocalProcessAffinity: number; maxLocalWebWorkerAffinity: number; maxLocalIsolatedProcessAffinity: number } { // Skip extensions that have an existing running location localExtensions = localExtensions.filter(extension => !existingRunningLocation.has(extension.identifier)); remoteExtensions = remoteExtensions.filter(extension => !existingRunningLocation.has(extension.identifier)); @@ -253,6 +277,9 @@ export class ExtensionRunningLocationTracker { const result = new ExtensionIdentifierMap(); const localProcessExtensions: IExtensionDescription[] = []; const localWebWorkerExtensions: IExtensionDescription[] = []; + // --- Coderm start: isolated language EH kind --- + const localIsolatedProcessExtensions: IExtensionDescription[] = []; + // --- Coderm end --- for (const [extensionIdKey, extensionHostKind] of extensionHostKinds) { let runningLocation: ExtensionRunningLocation | null = null; if (extensionHostKind === ExtensionHostKind.LocalProcess) { @@ -267,6 +294,13 @@ export class ExtensionRunningLocationTracker { } } else if (extensionHostKind === ExtensionHostKind.Remote) { runningLocation = new RemoteRunningLocation(); + } else if (extensionHostKind === ExtensionHostKind.LocalIsolatedProcess) { + // --- Coderm start: isolated language EH kind --- + const extensionDescription = extensions.get(extensionIdKey); + if (extensionDescription) { + localIsolatedProcessExtensions.push(extensionDescription); + } + // --- Coderm end --- } result.set(extensionIdKey, runningLocation); } @@ -281,6 +315,13 @@ export class ExtensionRunningLocationTracker { const affinity = localWebWorkerAffinities.get(extension.identifier) || 0; result.set(extension.identifier, new LocalWebWorkerRunningLocation(affinity)); } + // --- Coderm start: isolated language EH kind --- + const { affinities: localIsolatedProcessAffinities, maxAffinity: maxLocalIsolatedProcessAffinity } = this._computeAffinity(localIsolatedProcessExtensions, ExtensionHostKind.LocalIsolatedProcess, isInitialAllocation); + for (const extension of localIsolatedProcessExtensions) { + const affinity = localIsolatedProcessAffinities.get(extension.identifier) || 0; + result.set(extension.identifier, new LocalIsolatedProcessRunningLocation(affinity)); + } + // --- Coderm end --- // Add extensions that already have an existing running location for (const [extensionIdKey, runningLocation] of existingRunningLocation) { @@ -289,14 +330,17 @@ export class ExtensionRunningLocationTracker { } } - return { runningLocation: result, maxLocalProcessAffinity: maxAffinity, maxLocalWebWorkerAffinity: maxLocalWebWorkerAffinity }; + return { runningLocation: result, maxLocalProcessAffinity: maxAffinity, maxLocalWebWorkerAffinity: maxLocalWebWorkerAffinity, maxLocalIsolatedProcessAffinity: maxLocalIsolatedProcessAffinity }; } public initializeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[]): void { - const { runningLocation, maxLocalProcessAffinity, maxLocalWebWorkerAffinity } = this._doComputeRunningLocation(this._runningLocation, localExtensions, remoteExtensions, true); + const { runningLocation, maxLocalProcessAffinity, maxLocalWebWorkerAffinity, maxLocalIsolatedProcessAffinity } = this._doComputeRunningLocation(this._runningLocation, localExtensions, remoteExtensions, true); this._runningLocation = runningLocation; this._maxLocalProcessAffinity = maxLocalProcessAffinity; this._maxLocalWebWorkerAffinity = maxLocalWebWorkerAffinity; + // --- Coderm start: isolated language EH kind --- + this._maxLocalIsolatedProcessAffinity = maxLocalIsolatedProcessAffinity; + // --- Coderm end --- } /** @@ -324,6 +368,9 @@ export class ExtensionRunningLocationTracker { // Determine new running location const localProcessExtensions: IExtensionDescription[] = []; const localWebWorkerExtensions: IExtensionDescription[] = []; + // --- Coderm start: isolated language EH kind --- + const localIsolatedProcessExtensions: IExtensionDescription[] = []; + // --- Coderm end --- for (const extension of toAdd) { const extensionKind = this.readExtensionKinds(extension); const isRemote = extension.extensionLocation.scheme === Schemas.vscodeRemote; @@ -335,6 +382,10 @@ export class ExtensionRunningLocationTracker { localWebWorkerExtensions.push(extension); } else if (extensionHostKind === ExtensionHostKind.Remote) { runningLocation = new RemoteRunningLocation(); + } else if (extensionHostKind === ExtensionHostKind.LocalIsolatedProcess) { + // --- Coderm start: isolated language EH kind --- + localIsolatedProcessExtensions.push(extension); + // --- Coderm end --- } this._runningLocation.set(extension.identifier, runningLocation); } @@ -350,6 +401,14 @@ export class ExtensionRunningLocationTracker { const affinity = webWorkerExtensionsAffinities.get(extension.identifier) || 0; this._runningLocation.set(extension.identifier, new LocalWebWorkerRunningLocation(affinity)); } + + // --- Coderm start: isolated language EH kind --- + const { affinities: isolatedProcessExtensionsAffinities } = this._computeAffinity(localIsolatedProcessExtensions, ExtensionHostKind.LocalIsolatedProcess, false); + for (const extension of localIsolatedProcessExtensions) { + const affinity = isolatedProcessExtensionsAffinities.get(extension.identifier) || 0; + this._runningLocation.set(extension.identifier, new LocalIsolatedProcessRunningLocation(affinity)); + } + // --- Coderm end --- } } diff --git a/src/vs/workbench/services/extensions/electron-browser/localIsolatedProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localIsolatedProcessExtensionHost.ts new file mode 100644 index 00000000000..ee6ccc1c1a5 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/localIsolatedProcessExtensionHost.ts @@ -0,0 +1,649 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// --- Coderm file: Phase 6 isolated language extension host. Cloned from +// localProcessExtensionHost.ts; routes a configurable set of language extensions +// into a separate utility process so their crashes cannot take down the main EH. --- + +import { timeout } from '../../../../base/common/async.js'; +import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import * as objects from '../../../../base/common/objects.js'; +import * as platform from '../../../../base/common/platform.js'; +import { removeDangerousEnvVariables } from '../../../../base/common/processes.js'; +import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js'; +import { BufferedEmitter } from '../../../../base/parts/ipc/common/ipc.net.js'; +import { acquirePort } from '../../../../base/parts/ipc/electron-browser/ipc.mp.js'; +import * as nls from '../../../../nls.js'; +import { IExtensionHostDebugService } from '../../../../platform/debug/common/extensionHostDebug.js'; +import { extensionHostGraceTimeMs, IExtensionHostProcessOptions, IExtensionHostStarter } from '../../../../platform/extensions/common/extensionHostStarter.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { ILogService, ILoggerService } from '../../../../platform/log/common/log.js'; +import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { isLoggingOnly } from '../../../../platform/telemetry/common/telemetryUtils.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IWorkspaceContextService, WorkbenchState, isUntitledWorkspace } from '../../../../platform/workspace/common/workspace.js'; +import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js'; +import { IShellEnvironmentService } from '../../environment/electron-browser/shellEnvironmentService.js'; +import { MessagePortExtHostConnection, writeExtHostConnection } from '../common/extensionHostEnv.js'; +import { createMessageOfType, IExtensionHostInitData, MessageType, NativeLogMarkers, UIKind, isMessageOfType } from '../common/extensionHostProtocol.js'; +import { LocalIsolatedProcessRunningLocation } from '../common/extensionRunningLocation.js'; +import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionInspectInfo } from '../common/extensions.js'; +import { IHostService } from '../../host/browser/host.js'; +import { ILifecycleService, WillShutdownEvent } from '../../lifecycle/common/lifecycle.js'; +import { parseExtensionDevOptions } from '../common/extensionDevOptions.js'; +import { IDefaultLogLevelsService } from '../../log/common/defaultLogLevels.js'; + +export interface ILocalIsolatedProcessExtensionHostInitData { + readonly extensions: ExtensionHostExtensions; +} + +export interface ILocalIsolatedProcessExtensionHostDataProvider { + getInitData(): Promise; +} + +export class ExtensionHostProcess { + + private readonly _id: string; + + public get onStdout(): Event { + return this._extensionHostStarter.onDynamicStdout(this._id); + } + + public get onStderr(): Event { + return this._extensionHostStarter.onDynamicStderr(this._id); + } + + public get onMessage(): Event { + return this._extensionHostStarter.onDynamicMessage(this._id); + } + + public get onExit(): Event<{ code: number; signal: string }> { + return this._extensionHostStarter.onDynamicExit(this._id); + } + + constructor( + id: string, + private readonly _extensionHostStarter: IExtensionHostStarter, + ) { + this._id = id; + } + + public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number | undefined }> { + return this._extensionHostStarter.start(this._id, opts); + } + + public enableInspectPort(): Promise { + return this._extensionHostStarter.enableInspectPort(this._id); + } + + public waitForExit(maxWaitTimeMs: number): Promise { + return this._extensionHostStarter.waitForExit(this._id, maxWaitTimeMs); + } + + public kill(): Promise { + return this._extensionHostStarter.kill(this._id); + } +} + +export class NativeLocalIsolatedProcessExtensionHost extends Disposable implements IExtensionHost { + + public pid: number | null = null; + public readonly remoteAuthority = null; + public extensions: ExtensionHostExtensions | null = null; + + private readonly _onExit: Emitter<[number, string]> = this._register(new Emitter<[number, string]>()); + public readonly onExit: Event<[number, string]> = this._onExit.event; + + private readonly _onDidSetInspectPort = this._register(new Emitter()); + + + private readonly _isExtensionDevHost: boolean; + private readonly _isExtensionDevDebug: boolean; + private readonly _isExtensionDevDebugBrk: boolean; + private readonly _isExtensionDevTestFromCli: boolean; + + // State + private _terminating: boolean; + private _mainProcessHandlesExtHostShutdown: boolean; + + // Resources, in order they get acquired/created when .start() is called: + private _inspectListener: IExtensionInspectInfo | null; + private _extensionHostProcess: ExtensionHostProcess | null; + private _messageProtocol: Promise | null; + + constructor( + public readonly runningLocation: LocalIsolatedProcessRunningLocation, + public readonly startup: ExtensionHostStartup.EagerAutoStart | ExtensionHostStartup.EagerManualStart, + private readonly _initDataProvider: ILocalIsolatedProcessExtensionHostDataProvider, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @INotificationService private readonly _notificationService: INotificationService, + @INativeHostService private readonly _nativeHostService: INativeHostService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, + @IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILogService private readonly _logService: ILogService, + @ILoggerService private readonly _loggerService: ILoggerService, + @ILabelService private readonly _labelService: ILabelService, + @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, + @IHostService private readonly _hostService: IHostService, + @IProductService private readonly _productService: IProductService, + @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, + @IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter, + @IDefaultLogLevelsService private readonly _defaultLogLevelsService: IDefaultLogLevelsService, + ) { + super(); + const devOpts = parseExtensionDevOptions(this._environmentService); + this._isExtensionDevHost = devOpts.isExtensionDevHost; + this._isExtensionDevDebug = devOpts.isExtensionDevDebug; + this._isExtensionDevDebugBrk = devOpts.isExtensionDevDebugBrk; + this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; + + this._terminating = false; + this._mainProcessHandlesExtHostShutdown = false; + + this._inspectListener = null; + this._extensionHostProcess = null; + this._messageProtocol = null; + + this._register(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); + this._register(this._extensionHostDebugService.onClose(event => { + if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { + this._nativeHostService.closeWindow(); + } + })); + this._register(this._extensionHostDebugService.onReload(event => { + if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { + this._hostService.reload(); + } + })); + } + + public override dispose(): void { + if (!this._terminating) { + this._terminating = true; + } + super.dispose(); + this._messageProtocol = null; + } + + public async disconnect(): Promise { + this._terminating = true; + + // Send the Terminate message so the extension host can run + // deactivation handlers and exit gracefully. + if (this._messageProtocol) { + try { + const protocol = await Promise.race([ + this._messageProtocol.then(protocol => protocol, () => undefined), + timeout(1000).then(() => undefined) + ]); + protocol?.send(createMessageOfType(MessageType.Terminate)); + } catch { + // ignore - extension host may have already exited + } + } + + // For the restart case where the main process does not handle the + // extension host shutdown, signal the main process to start the grace + // timer (fire-and-forget). After the timeout the extension host will + // be forcefully killed if it hasn't exited on its own. For all + // window-lifecycle shutdown reasons (close/quit/reload/load), the + // main process already handles this via + // WindowUtilityProcess.registerWindowListeners. + if (this._extensionHostProcess && !this._mainProcessHandlesExtHostShutdown) { + this._extensionHostProcess.waitForExit(extensionHostGraceTimeMs).catch(() => { /* best-effort */ }); + } + + this._messageProtocol = null; + } + + public start(): Promise { + if (this._terminating) { + // .terminate() was called + throw new CancellationError(); + } + + if (!this._messageProtocol) { + this._messageProtocol = this._start(); + } + + return this._messageProtocol; + } + + private async _start(): Promise { + const [extensionHostCreationResult, portNumber, processEnv] = await Promise.all([ + this._extensionHostStarter.createExtensionHost(), + this._tryFindDebugPort(), + this._shellEnvironmentService.getShellEnv(), + ]); + + this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); + + const env = objects.mixin(processEnv, { + VSCODE_ESM_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', + VSCODE_HANDLES_UNCAUGHT_ERRORS: true + }); + + if (this._environmentService.debugExtensionHost.env) { + objects.mixin(env, this._environmentService.debugExtensionHost.env); + } + + removeDangerousEnvVariables(env); + + if (this._isExtensionDevHost) { + // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might + // be that dependencies, that otherwise would be cached, get modified. + delete env['VSCODE_CODE_CACHE_PATH']; + } + + const opts: IExtensionHostProcessOptions = { + // --- Coderm start: isolated language EH kind --- + // Distinguished so the main-side ExtensionHostStarter names the spawned + // utility process 'isolated-extension-host' (visible in Process Explorer). + kind: 'isolatedExtensionHost', + // --- Coderm end --- + responseWindowId: this._nativeHostService.windowId, + responseChannel: 'coderm:startIsolatedExtensionHostMessagePortResult', + responseNonce: generateUuid(), + env, + // We only detach the extension host on windows. Linux and Mac orphan by default + // and detach under Linux and Mac create another process group. + // We detach because we have noticed that when the renderer exits, its child processes + // (i.e. extension host) are taken down in a brutal fashion by the OS + detached: !!platform.isWindows, + execArgv: undefined as string[] | undefined, + silent: true + }; + + const inspectHost = '127.0.0.1'; + if (portNumber !== 0) { + opts.execArgv = [ + '--nolazy', + (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + `${inspectHost}:${portNumber}` + ]; + } else { + opts.execArgv = ['--inspect-port=0']; + } + + if (this._environmentService.extensionTestsLocationURI) { + opts.execArgv.unshift('--expose-gc'); + } + + if (this._environmentService.args['prof-v8-extensions']) { + opts.execArgv.unshift('--prof'); + } + + // Refs https://github.com/microsoft/vscode/issues/189805 + // + // Enable experimental network inspection + // inspector agent is always setup hence add this flag + // unconditionally. + opts.execArgv.unshift('--dns-result-order=ipv4first', '--experimental-network-inspection'); + + // Catch all output coming from the extension host process + type Output = { data: string; format: string[] }; + const onStdout = this._register(this._handleProcessOutputStream(this._extensionHostProcess.onStdout)); + const onStderr = this._register(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'] })) + ); + + // Debounce all output, so we can render it in the Chrome console as a group + const onDebouncedOutput = Event.debounce(onOutput, (r, o) => { + return r + ? { data: r.data + o.data, format: [...r.format, ...o.format] } + : { data: o.data, format: o.format }; + }, 100); + + // Print out extension host output + this._register(onDebouncedOutput(output => { + const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+):(\d+)\/([^\s]+)/); + if (inspectorUrlMatch) { + const [, host, port, auth] = inspectorUrlMatch; + const devtoolsUrl = `devtools://devtools/bundled/js_app.html?v8only=true&ws=${host}:${port}/${auth}`; + if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { + console.debug(`%c[Extension Host] %cdebugger inspector at ${devtoolsUrl}`, 'color: blue', 'color:'); + } + if (!this._inspectListener || !this._inspectListener.devtoolsUrl) { + this._inspectListener = { host, port: Number(port), devtoolsUrl }; + this._onDidSetInspectPort.fire(); + } + } else { + if (!this._isExtensionDevTestFromCli) { + console.group('Extension Host'); + console.log(output.data, ...output.format); + console.groupEnd(); + } + } + })); + + // Lifecycle + + this._register(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) { + if (this._isExtensionDevHost && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber); + } + this._inspectListener = { port: portNumber, host: inspectHost }; + this._onDidSetInspectPort.fire(); + } + + // Help in case we fail to start it + let startupTimeoutHandle: Timeout | undefined; + if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) { + startupTimeoutHandle = setTimeout(() => { + this._logService.error(`[IsolatedExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`); + + const msg = this._isExtensionDevDebugBrk + ? 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.") + : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); + + this._notificationService.prompt(Severity.Warning, msg, + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => this._hostService.reload() + }], + { + sticky: true, + priority: NotificationPriority.URGENT + } + ); + }, 10000); + } + + // Initialize extension host process with hand shakes + const protocol = await this._establishProtocol(this._extensionHostProcess, opts); + await this._performHandshake(protocol); + clearTimeout(startupTimeoutHandle); + return protocol; + } + + /** + * Find a free port if extension host debugging is enabled. + */ + private async _tryFindDebugPort(): Promise { + + if (typeof this._environmentService.debugExtensionHost.port !== 'number') { + return 0; + } + + const expected = this._environmentService.debugExtensionHost.port; + const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); + + if (!this._isExtensionDevTestFromCli) { + if (!port) { + console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:'); + } else { + if (port !== expected) { + console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:'); + } + if (this._isExtensionDevDebugBrk) { + console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:'); + } else { + console.debug(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:'); + } + } + } + + return port || 0; + } + + private _establishProtocol(extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise { + + writeExtHostConnection(new MessagePortExtHostConnection(), opts.env); + + // Get ready to acquire the message port from the shared process worker + const portPromise = acquirePort(undefined /* we trigger the request via service call! */, opts.responseChannel, opts.responseNonce); + + return new Promise((resolve, reject) => { + + const handle = setTimeout(() => { + reject('The isolated extension host took longer than 60s to connect.'); + }, 60 * 1000); + + portPromise.then((port) => { + this._register(toDisposable(() => { + // Close the message port when the extension host is disposed + port.close(); + port.onmessage = null; + })); + clearTimeout(handle); + + const onMessage = new BufferedEmitter(); + port.onmessage = ((e) => { + if (e.data) { + onMessage.fire(VSBuffer.wrap(e.data)); + } + }); + port.start(); + + resolve({ + onMessage: onMessage.event, + send: message => port.postMessage(message.buffer), + }); + }); + + // Now that the message port listener is installed, start the ext host process + const sw = StopWatch.create(false); + extensionHostProcess.start(opts).then(({ pid }) => { + if (pid) { + this.pid = pid; + } + this._logService.info(`Started isolated extension host with pid ${pid}.`); + const duration = sw.elapsed(); + if (platform.isCI) { + this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); + } + }, (err) => { + // Starting the ext host process resulted in an error + reject(err); + }); + }); + } + + private _performHandshake(protocol: IMessagePassingProtocol): Promise { + // 1) wait for the incoming `ready` event and send the initialization data. + // 2) wait for the incoming `initialized` event. + return new Promise((resolve, reject) => { + + let timeoutHandle: Timeout; + const installTimeoutCheck = () => { + timeoutHandle = setTimeout(() => { + reject('The isolated extension host took longer than 60s to send its ready message.'); + }, 60 * 1000); + }; + const uninstallTimeoutCheck = () => { + clearTimeout(timeoutHandle); + }; + + // Wait 60s for the ready message + installTimeoutCheck(); + + const disposable = protocol.onMessage(msg => { + + if (isMessageOfType(msg, MessageType.Ready)) { + + // 1) Extension Host is ready to receive messages, initialize it + uninstallTimeoutCheck(); + + this._createExtHostInitData().then(data => { + + // Wait 60s for the initialized message + installTimeoutCheck(); + + protocol.send(VSBuffer.fromString(JSON.stringify(data))); + }); + return; + } + + if (isMessageOfType(msg, MessageType.Initialized)) { + + // 2) Extension Host is initialized + uninstallTimeoutCheck(); + + // stop listening for messages here + disposable.dispose(); + + // release this promise + resolve(); + return; + } + + console.error(`received unexpected message during handshake phase from the extension host: `, msg); + }); + + }); + } + + private async _createExtHostInitData(): Promise { + const initData = await this._initDataProvider.getInitData(); + this.extensions = initData.extensions; + const workspace = this._contextService.getWorkspace(); + return { + commit: this._productService.commit, + version: this._productService.version, + quality: this._productService.quality, + date: this._productService.date, + parentPid: 0, + environment: { + isExtensionDevelopmentDebug: this._isExtensionDevDebug, + appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined, + appName: this._productService.extensionAppName ?? this._productService.nameLong, + appHost: (this._environmentService.isSessionsWindow ? this._productService.agentsTelemetryAppName : undefined) || this._productService.embedderIdentifier || 'desktop', + appUriScheme: this._productService.urlProtocol, + isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService), + isPortable: this._environmentService.isPortable, + appLanguage: platform.language, + extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI, + extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI, + globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome, + workspaceStorageHome: this._environmentService.workspaceStorageHome, + extensionLogLevel: this._defaultLogLevelsService.defaultLogLevels.extensions, + isSessionsWindow: this._environmentService.isSessionsWindow + }, + workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : { + configuration: workspace.configuration ?? undefined, + id: workspace.id, + name: this._labelService.getWorkspaceLabel(workspace), + isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false, + transient: workspace.transient + }, + remote: { + authority: this._environmentService.remoteAuthority, + connectionData: null, + isRemote: false + }, + consoleForward: { + includeStack: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose), + logNative: !this._isExtensionDevTestFromCli && this._isExtensionDevHost + }, + extensions: this.extensions.toSnapshot(), + telemetryInfo: { + sessionId: this._telemetryService.sessionId, + machineId: this._telemetryService.machineId, + sqmId: this._telemetryService.sqmId, + devDeviceId: this._telemetryService.devDeviceId ?? this._telemetryService.machineId, + firstSessionDate: this._telemetryService.firstSessionDate, + msftInternal: this._telemetryService.msftInternal + }, + remoteExtensionTips: this._productService.remoteExtensionTips, + virtualWorkspaceExtensionTips: this._productService.virtualWorkspaceExtensionTips, + logLevel: this._logService.getLevel(), + loggers: [...this._loggerService.getRegisteredLoggers()], + logsLocation: this._environmentService.extHostLogsPath, + autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart), + uiKind: UIKind.Desktop, + handle: this._environmentService.window.handle ? encodeBase64(this._environmentService.window.handle) : undefined + }; + } + + private _onExtHostProcessExit(code: number, signal: string): void { + if (this._terminating) { + // Expected termination path (we asked the process to terminate) + return; + } + + this._onExit.fire([code, signal]); + } + + private _handleProcessOutputStream(stream: Event) { + let last = ''; + let isOmitting = false; + const event = new Emitter(); + 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; + const 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'); + } + } + }, undefined, this._store); + + return event; + } + + public async enableInspectPort(): Promise { + if (!!this._inspectListener) { + return true; + } + + if (!this._extensionHostProcess) { + return false; + } + + const result = await this._extensionHostProcess.enableInspectPort(); + if (!result) { + return false; + } + + await Promise.race([Event.toPromise(this._onDidSetInspectPort.event), timeout(1000)]); + return !!this._inspectListener; + } + + public getInspectPort(): IExtensionInspectInfo | undefined { + return this._inspectListener ?? undefined; + } + + private _onWillShutdown(event: WillShutdownEvent): void { + this._mainProcessHandlesExtHostShutdown = true; + + // If the extension development host was started without debugger attached we need + // to communicate this back to the main side to terminate the debug session + if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); + } + } +} diff --git a/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts index 2b6104a77d9..7c49f6e5533 100644 --- a/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts @@ -46,13 +46,18 @@ import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker import { IExtensionHostManager } from '../common/extensionHostManagers.js'; import { ExtensionHostExitCode } from '../common/extensionHostProtocol.js'; import { IExtensionManifestPropertiesService } from '../common/extensionManifestPropertiesService.js'; -import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation } from '../common/extensionRunningLocation.js'; +// --- Coderm start: isolated language EH kind --- +import { ExtensionRunningLocation, LocalIsolatedProcessRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation } from '../common/extensionRunningLocation.js'; +// --- Coderm end --- import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from '../common/extensionRunningLocationTracker.js'; import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost, IExtensionService, WebWorkerExtHostConfigValue, toExtension, webWorkerExtHostConfig } from '../common/extensions.js'; import { ExtensionsProposedApi } from '../common/extensionsProposedApi.js'; import { IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData, RemoteExtensionHost } from '../common/remoteExtensionHost.js'; import { CachedExtensionScanner } from './cachedExtensionScanner.js'; import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, NativeLocalProcessExtensionHost } from './localProcessExtensionHost.js'; +// --- Coderm start: isolated language EH kind --- +import { ILocalIsolatedProcessExtensionHostDataProvider, ILocalIsolatedProcessExtensionHostInitData, NativeLocalIsolatedProcessExtensionHost } from './localIsolatedProcessExtensionHost.js'; +// --- Coderm end --- import { IHostService } from '../../host/browser/host.js'; import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; @@ -63,6 +68,9 @@ export class NativeExtensionService extends AbstractExtensionService implements private readonly _extensionScanner: CachedExtensionScanner; private readonly _localCrashTracker = new ExtensionHostCrashTracker(); + // --- Coderm start: isolated language EH kind --- + private readonly _isolatedCrashTracker = new ExtensionHostCrashTracker(); + // --- Coderm end --- constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -225,6 +233,27 @@ export class NativeExtensionService extends AbstractExtensionService implements this._notificationService.prompt(Severity.Error, nls.localize('extensionService.crash', "Extension host terminated unexpectedly 3 times within the last 5 minutes."), choices); } } + // --- Coderm start: isolated language EH kind --- + else if (extensionHost.kind === ExtensionHostKind.LocalIsolatedProcess) { + // Restart the isolated host independently of the main/local EH: a language + // extension crash must not take the editor down. startExtensionHosts() skips + // already-running hosts, so only the stopped isolated host is respawned. + this._logExtensionHostCrash(extensionHost); + this._sendExtensionHostCrashTelemetry(code, signal, activatedExtensions); + + this._isolatedCrashTracker.registerCrash(); + if (this._isolatedCrashTracker.shouldAutomaticallyRestart()) { + this._logService.info(`Automatically restarting the isolated extension host.`); + this._notificationService.status(nls.localize('extensionService.autoRestart', "The extension host terminated unexpectedly. Restarting..."), { hideAfter: 5000 }); + this.startExtensionHosts(); + } else { + this._notificationService.prompt(Severity.Error, nls.localize('extensionService.crash', "Extension host terminated unexpectedly 3 times within the last 5 minutes."), [{ + label: nls.localize('restart', "Restart Extension Host"), + run: () => this.startExtensionHosts() + }]); + } + } + // --- Coderm end --- } private _sendExtensionHostCrashTelemetry(code: number, signal: string | null, activatedExtensions: ExtensionIdentifier[]): void { @@ -559,6 +588,16 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { } return null; } + // --- Coderm start: isolated language EH kind --- + case ExtensionHostKind.LocalIsolatedProcess: { + const startup = ( + isInitialStart + ? ExtensionHostStartup.EagerManualStart + : ExtensionHostStartup.EagerAutoStart + ); + return this._instantiationService.createInstance(NativeLocalIsolatedProcessExtensionHost, runningLocation, startup, this._createLocalIsolatedProcessExtensionHostDataProvider(runningLocations, isInitialStart, runningLocation)); + } + // --- Coderm end --- } } @@ -594,6 +633,40 @@ class NativeExtensionHostFactory implements IExtensionHostFactory { } }; } + // --- Coderm start: isolated language EH kind --- + private _createLocalIsolatedProcessExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, isInitialStart: boolean, desiredRunningLocation: LocalIsolatedProcessRunningLocation): ILocalIsolatedProcessExtensionHostDataProvider { + return { + getInitData: async (): Promise => { + if (isInitialStart) { + // Here we load even extensions that would be disabled by workspace trust + const scannedExtensions = await this._extensionScanner.scannedExtensions; + if (isCI) { + this._logService.info(`NativeExtensionHostFactory._createLocalIsolatedProcessExtensionHostDataProvider.scannedExtensions: ${scannedExtensions.map(ext => ext.identifier.value).join(',')}`); + } + + const localExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, scannedExtensions, /* ignore workspace trust */true); + if (isCI) { + this._logService.info(`NativeExtensionHostFactory._createLocalIsolatedProcessExtensionHostDataProvider.localExtensions: ${localExtensions.map(ext => ext.identifier.value).join(',')}`); + } + + const runningLocation = runningLocations.computeRunningLocation(localExtensions, [], false); + const myExtensions = filterExtensionDescriptions(localExtensions, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation)); + const extensions = new ExtensionHostExtensions(0, localExtensions, myExtensions.map(extension => extension.identifier)); + if (isCI) { + this._logService.info(`NativeExtensionHostFactory._createLocalIsolatedProcessExtensionHostDataProvider.myExtensions: ${myExtensions.map(ext => ext.identifier.value).join(',')}`); + } + return { extensions }; + } else { + // restart case + const snapshot = await this._getExtensionRegistrySnapshotWhenReady(); + const myExtensions = runningLocations.filterByRunningLocation(snapshot.extensions, desiredRunningLocation); + const extensions = new ExtensionHostExtensions(snapshot.versionId, snapshot.extensions, myExtensions.map(extension => extension.identifier)); + return { extensions }; + } + } + }; + } + // --- Coderm end --- private _createWebWorkerExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: LocalWebWorkerRunningLocation): IWebWorkerExtensionHostDataProvider { return { @@ -659,18 +732,37 @@ export class NativeExtensionHostKindPicker implements IExtensionHostKindPicker { private readonly _hasRemoteExtHost: boolean; private readonly _hasWebWorkerExtHost: boolean; + // --- Coderm start: isolated language EH kind --- + private readonly _hasLocalIsolatedExtHost: boolean; + // --- Coderm end --- constructor( @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IConfigurationService configurationService: IConfigurationService, + // --- Coderm start: isolated language EH kind --- + @IConfigurationService private readonly _configurationService: IConfigurationService, + // --- Coderm end --- @ILogService private readonly _logService: ILogService, ) { this._hasRemoteExtHost = Boolean(environmentService.remoteAuthority); - const webWorkerExtHostEnablement = determineLocalWebWorkerExtHostEnablement(environmentService, configurationService); + const webWorkerExtHostEnablement = determineLocalWebWorkerExtHostEnablement(environmentService, this._configurationService); this._hasWebWorkerExtHost = (webWorkerExtHostEnablement !== LocalWebWorkerExtHostEnablement.Disabled); + // --- Coderm start: isolated language EH kind --- + this._hasLocalIsolatedExtHost = this._configurationService.getValue('coderm.languageHost.isolatedEnabled'); + // --- Coderm end --- } public pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { + // --- Coderm start: isolated language EH kind --- + // Route explicitly-listed language extensions into the isolated EH when the + // feature is enabled. This runs before the upstream kind logic, so the main + // local process EH never claims these extensions. + if (this._hasLocalIsolatedExtHost) { + const isolatedExtensions = this._configurationService.getValue('coderm.languageHost.isolatedExtensions') || []; + if (isolatedExtensions.includes(extensionId.value)) { + return ExtensionHostKind.LocalIsolatedProcess; + } + } + // --- Coderm end --- const result = NativeExtensionHostKindPicker.pickExtensionHostKind(extensionKinds, isInstalledLocally, isInstalledRemotely, preference, this._hasRemoteExtHost, this._hasWebWorkerExtHost); this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionHostKindToString(result)}`); return result;