Skip to content

Commit 481d87f

Browse files
committed
dispose extension pseudoterminals when terminals close
1 parent cd42951 commit 481d87f

2 files changed

Lines changed: 90 additions & 18 deletions

File tree

src/vs/workbench/api/common/extHostTerminalService.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export class ExtHostTerminal extends Disposable {
309309
}
310310
}
311311

312-
class ExtHostPseudoterminal implements ITerminalChildProcess {
312+
class ExtHostPseudoterminal extends Disposable implements ITerminalChildProcess {
313313
readonly id = 0;
314314
readonly shouldPersist = false;
315315

@@ -322,7 +322,9 @@ class ExtHostPseudoterminal implements ITerminalChildProcess {
322322
private readonly _onProcessExit = new Emitter<number | undefined>();
323323
public readonly onProcessExit: Event<number | undefined> = this._onProcessExit.event;
324324

325-
constructor(private readonly _pty: vscode.Pseudoterminal) { }
325+
constructor(private readonly _pty: vscode.Pseudoterminal) {
326+
super();
327+
}
326328

327329
refreshProperty<T extends ProcessPropertyType>(property: ProcessPropertyType): Promise<IProcessPropertyMap[T]> {
328330
throw new Error(`refreshProperty is not suppported in extension owned terminals. property: ${property}`);
@@ -380,18 +382,18 @@ class ExtHostPseudoterminal implements ITerminalChildProcess {
380382

381383
startSendingEvents(initialDimensions: ITerminalDimensionsDto | undefined): void {
382384
// Attach the listeners
383-
this._pty.onDidWrite(e => this._onProcessData.fire(e));
384-
this._pty.onDidClose?.((e: number | void = undefined) => {
385+
this._register(this._pty.onDidWrite(e => this._onProcessData.fire(e)));
386+
this._register(this._pty.onDidClose?.((e: number | void = undefined) => {
385387
this._onProcessExit.fire(e === void 0 ? undefined : e);
386-
});
387-
this._pty.onDidOverrideDimensions?.(e => {
388+
}) ?? Disposable.None);
389+
this._register(this._pty.onDidOverrideDimensions?.(e => {
388390
if (e) {
389391
this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: { cols: e.columns, rows: e.rows } });
390392
}
391-
});
392-
this._pty.onDidChangeName?.(title => {
393+
}) ?? Disposable.None);
394+
this._register(this._pty.onDidChangeName?.(title => {
393395
this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: title });
394-
});
396+
}) ?? Disposable.None);
395397

396398
this._pty.open(initialDimensions ? initialDimensions : undefined);
397399

@@ -520,8 +522,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
520522
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name);
521523
const p = new ExtHostPseudoterminal(options.pty);
522524
terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.titleTemplate).then(id => {
523-
const disposable = this._setupExtHostProcessListeners(id, p);
524-
this._terminalProcessDisposables[id] = disposable;
525+
if (!this.getTerminalById(id)) {
526+
return;
527+
}
528+
this._setupExtHostProcessListeners(id, p);
525529
});
526530
this._terminals.push(terminal);
527531
return terminal.value;
@@ -551,8 +555,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
551555
throw new Error(`Cannot resolve terminal with id ${id} for virtual process`);
552556
}
553557
const p = new ExtHostPseudoterminal(pty);
554-
const disposable = this._setupExtHostProcessListeners(id, p);
555-
this._terminalProcessDisposables[id] = disposable;
558+
this._setupExtHostProcessListeners(id, p);
556559
}
557560

558561
public async $acceptActiveTerminalChanged(id: number | null): Promise<void> {
@@ -613,6 +616,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
613616
}
614617

615618
public async $acceptTerminalClosed(id: number, exitCode: number | undefined, exitReason: TerminalExitReason): Promise<void> {
619+
this._cleanUpTerminalProcess(id);
620+
616621
// Release any cached terminal links and cancel in-flight link providers for this terminal
617622
this._terminalLinkCache.delete(id);
618623
const cancellationSource = this._terminalLinkCancellationSource.get(id);
@@ -694,23 +699,26 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
694699
return undefined;
695700
}
696701

697-
protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): IDisposable {
702+
protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): void {
698703
const disposables = new DisposableStore();
704+
if (p instanceof ExtHostPseudoterminal) {
705+
disposables.add(p);
706+
}
699707
disposables.add(p.onProcessReady(e => this._proxy.$sendProcessReady(id, e.pid, e.cwd, e.windowsPty)));
700708
disposables.add(p.onDidChangeProperty(property => this._proxy.$sendProcessProperty(id, property)));
701709

702710
// Buffer data events to reduce the amount of messages going to the renderer
703-
this._bufferer.startBuffering(id, p.onProcessData);
711+
disposables.add(this._bufferer.startBuffering(id, p.onProcessData));
704712
disposables.add(p.onProcessExit(exitCode => this._onProcessExit(id, exitCode)));
705713
this._terminalProcesses.set(id, p);
714+
this._terminalProcessDisposables[id] = disposables;
706715

707716
const awaitingStart = this._extensionTerminalAwaitingStart[id];
708717
if (awaitingStart && p instanceof ExtHostPseudoterminal) {
709718
p.startSendingEvents(awaitingStart.initialDimensions);
710719
delete this._extensionTerminalAwaitingStart[id];
711720
}
712721

713-
return disposables;
714722
}
715723

716724
public $acceptProcessAckDataEvent(id: number, charCount: number): void {
@@ -980,6 +988,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
980988
}
981989

982990
private _onProcessExit(id: number, exitCode: number | undefined): void {
991+
this._cleanUpTerminalProcess(id);
992+
993+
// Send exit event to main side
994+
this._proxy.$sendProcessExit(id, exitCode);
995+
}
996+
997+
private _cleanUpTerminalProcess(id: number): void {
983998
this._bufferer.stopBuffering(id);
984999

9851000
// Remove process reference
@@ -992,8 +1007,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
9921007
processDiposable.dispose();
9931008
delete this._terminalProcessDisposables[id];
9941009
}
995-
// Send exit event to main side
996-
this._proxy.$sendProcessExit(id, exitCode);
9971010
}
9981011

9991012
public getTerminalById(id: number): ExtHostTerminal | null {

src/vs/workbench/api/test/common/extHostTerminalService.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as assert from 'assert';
77
import type * as vscode from 'vscode';
8+
import { Emitter } from '../../../../base/common/event.js';
89
import { mock } from '../../../../base/test/common/mock.js';
910
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
1011
import { IShellLaunchConfigDto } from '../../../../platform/terminal/common/terminal.js';
@@ -78,4 +79,62 @@ suite('ExtHostTerminalService', () => {
7879
{ providerTokenCancelled: true, firstLinks: [], linksAfterClose: [], handledAfterClose: false }
7980
);
8081
});
82+
83+
test('extension terminal processes are released when terminals close', async () => {
84+
const rpcProtocol = new TestRPCProtocol();
85+
let processExitCalls = 0;
86+
rpcProtocol.set(MainContext.MainThreadTerminalService, new class extends mock<MainThreadTerminalServiceShape>() {
87+
override async $registerProcessSupport(): Promise<void> { }
88+
override async $sendProcessExit(): Promise<void> { processExitCalls++; }
89+
override async $sendProcessReady(): Promise<void> { }
90+
});
91+
92+
const commands = new class extends mock<ExtHostCommands>() {
93+
override registerArgumentProcessor(_processor: ArgumentProcessor): void { }
94+
};
95+
const initData = new class extends mock<IExtHostInitDataService>() {
96+
override readonly remote = { authority: 'test+remote', isRemote: true, connectionData: null };
97+
};
98+
const service = store.add(new WorkerExtHostTerminalService(commands, rpcProtocol, initData));
99+
100+
const terminalId = 42;
101+
service.$acceptTerminalOpened(terminalId, undefined, 'test', {} as IShellLaunchConfigDto);
102+
const terminal = service.getTerminalById(terminalId)!;
103+
store.add(terminal);
104+
105+
const writeEmitter = store.add(new Emitter<string>());
106+
const closeEmitter = store.add(new Emitter<number | void>());
107+
const inputs: string[] = [];
108+
let closeOnOpen = false;
109+
const pty: vscode.Pseudoterminal = {
110+
onDidWrite: writeEmitter.event,
111+
onDidClose: closeEmitter.event,
112+
open(): void { if (closeOnOpen) { closeEmitter.fire(); } },
113+
close(): void { },
114+
handleInput(data: string): void { inputs.push(data); }
115+
};
116+
service.attachPtyToTerminal(terminalId, pty);
117+
await service.$startExtensionTerminal(terminalId, undefined);
118+
await rpcProtocol.sync();
119+
120+
service.$acceptProcessInput(terminalId, 'before');
121+
await service.$acceptTerminalClosed(terminalId, undefined, TerminalExitReason.Unknown);
122+
service.$acceptProcessInput(terminalId, 'after');
123+
closeEmitter.fire();
124+
await rpcProtocol.sync();
125+
126+
assert.deepStrictEqual(inputs, ['before']);
127+
assert.strictEqual(processExitCalls, 0);
128+
129+
const synchronousTerminalId = 43;
130+
service.$acceptTerminalOpened(synchronousTerminalId, undefined, 'test', {} as IShellLaunchConfigDto);
131+
store.add(service.getTerminalById(synchronousTerminalId)!);
132+
closeOnOpen = true;
133+
service.attachPtyToTerminal(synchronousTerminalId, pty);
134+
await service.$startExtensionTerminal(synchronousTerminalId, undefined);
135+
await rpcProtocol.sync();
136+
137+
assert.strictEqual(closeEmitter.hasListeners(), false);
138+
assert.strictEqual(processExitCalls, 1);
139+
});
81140
});

0 commit comments

Comments
 (0)