From ffe583d2431078c2495e20ae69d2049803479d44 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 25 Nov 2025 12:55:47 +0100 Subject: [PATCH 1/3] fix: memory leak in local process extension host --- .../localProcessExtensionHost.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 516169c5aa984..96331bd51cde7 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -7,7 +7,7 @@ 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 { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.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'; @@ -87,18 +87,17 @@ export class ExtensionHostProcess { } } -export class NativeLocalProcessExtensionHost implements IExtensionHost { +export class NativeLocalProcessExtensionHost extends Disposable implements IExtensionHost { public pid: number | null = null; public readonly remoteAuthority = null; public extensions: ExtensionHostExtensions | null = null; - private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); + private readonly _onExit: Emitter<[number, string]> = this._register(new Emitter<[number, string]>()); public readonly onExit: Event<[number, string]> = this._onExit.event; - private readonly _onDidSetInspectPort = new Emitter(); + private readonly _onDidSetInspectPort = this._register(new Emitter()); - private readonly _toDispose = new DisposableStore(); private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevDebug: boolean; @@ -133,6 +132,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, @IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter, ) { + super(); const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; this._isExtensionDevDebug = devOpts.isExtensionDevDebug; @@ -145,27 +145,26 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { this._extensionHostProcess = null; this._messageProtocol = null; - this._toDispose.add(this._onExit); - this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._extensionHostDebugService.onClose(event => { + this._register(this._onExit); + 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._toDispose.add(this._extensionHostDebugService.onReload(event => { + this._register(this._extensionHostDebugService.onReload(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._hostService.reload(); } })); } - public dispose(): void { + public override dispose(): void { if (this._terminating) { return; } this._terminating = true; - - this._toDispose.dispose(); + super.dispose(); } public start(): Promise { @@ -248,8 +247,8 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { // Catch all output coming from the extension host process type Output = { data: string; format: string[] }; - const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout, this._toDispose); - const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr, this._toDispose); + const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); + const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); const onOutput = Event.any( Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) @@ -263,7 +262,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { }, 100); // Print out extension host output - this._toDispose.add(onDebouncedOutput(output => { + this._register(onDebouncedOutput(output => { const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+):(\d+)\/([^\s]+)/); if (inspectorUrlMatch) { const [, host, port, auth] = inspectorUrlMatch; @@ -286,7 +285,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { // Lifecycle - this._toDispose.add(this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal))); + 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) { @@ -371,9 +370,10 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { }, 60 * 1000); portPromise.then((port) => { - this._toDispose.add(toDisposable(() => { + this._register(toDisposable(() => { // Close the message port when the extension host is disposed port.close(); + port.onmessage = null; })); clearTimeout(handle); @@ -530,7 +530,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { this._onExit.fire([code, signal]); } - private _handleProcessOutputStream(stream: Event, store: DisposableStore) { + private _handleProcessOutputStream(stream: Event) { let last = ''; let isOmitting = false; const event = new Emitter(); @@ -558,7 +558,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { event.fire(line + '\n'); } } - }, undefined, store); + }, undefined, this._store); return event; } From 8943fb1673eef7c502f9269396b03bc3193dee4a Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 25 Nov 2025 13:00:13 +0100 Subject: [PATCH 2/3] clean --- .../extensions/electron-browser/localProcessExtensionHost.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 96331bd51cde7..d0399bdc39073 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -145,7 +145,6 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte this._extensionHostProcess = null; this._messageProtocol = null; - this._register(this._onExit); this._register(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); this._register(this._extensionHostDebugService.onClose(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { From 8ba3bbe3d4492224267a425b3e4c28f019c44206 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 25 Nov 2025 13:12:57 +0100 Subject: [PATCH 3/3] clear promise --- .../extensions/electron-browser/localProcessExtensionHost.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index d0399bdc39073..4ee5230281e8b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -164,6 +164,7 @@ export class NativeLocalProcessExtensionHost extends Disposable implements IExte } this._terminating = true; super.dispose(); + this._messageProtocol = null; } public start(): Promise {