From a4ea8bfab06eb1578956791cdefc22d1fb922d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 1 Oct 2025 17:32:39 +0200 Subject: [PATCH 1/3] Include the last input in the exit event --- .../ptyhost/v1/pty_host_service_pb.ts | 14 ++++++++++- .../ptyhost/v1/pty_host_service.proto | 1 + .../pty/ptyHost/ptyEventsStreamHandler.ts | 6 ++++- .../src/services/pty/ptyHost/ptyProcess.ts | 8 +++++- .../ptyHost/ptyEventsStreamHandler.ts | 5 ++-- .../sharedProcess/ptyHost/ptyProcess.test.ts | 25 +++++++++++++++++++ .../src/sharedProcess/ptyHost/ptyProcess.ts | 8 ++++-- .../src/sharedProcess/ptyHost/types.ts | 2 +- 8 files changed, 60 insertions(+), 9 deletions(-) diff --git a/gen/proto/ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb.ts b/gen/proto/ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb.ts index 1e6cb4cb3ed06..2be7d35d1c951 100644 --- a/gen/proto/ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb.ts +++ b/gen/proto/ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb.ts @@ -206,6 +206,10 @@ export interface PtyEventExit { * @generated from protobuf field: optional uint32 signal = 2; */ signal?: number; + /** + * @generated from protobuf field: string last_input = 3; + */ + lastInput: string; } /** * PtyEventStartError is sent by the PTY process when the shared process fails to start it. @@ -710,12 +714,14 @@ class PtyEventExit$Type extends MessageType { constructor() { super("teleport.web.teleterm.ptyhost.v1.PtyEventExit", [ { no: 1, name: "exit_code", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, - { no: 2, name: "signal", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ } + { no: 2, name: "signal", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "last_input", kind: "scalar", T: 9 /*ScalarType.STRING*/ } ]); } create(value?: PartialMessage): PtyEventExit { const message = globalThis.Object.create((this.messagePrototype!)); message.exitCode = 0; + message.lastInput = ""; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -731,6 +737,9 @@ class PtyEventExit$Type extends MessageType { case /* optional uint32 signal */ 2: message.signal = reader.uint32(); break; + case /* string last_input */ 3: + message.lastInput = reader.string(); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -749,6 +758,9 @@ class PtyEventExit$Type extends MessageType { /* optional uint32 signal = 2; */ if (message.signal !== undefined) writer.tag(2, WireType.Varint).uint32(message.signal); + /* string last_input = 3; */ + if (message.lastInput !== "") + writer.tag(3, WireType.LengthDelimited).string(message.lastInput); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/proto/teleport/web/teleterm/ptyhost/v1/pty_host_service.proto b/proto/teleport/web/teleterm/ptyhost/v1/pty_host_service.proto index fd37c0ad45598..5963539060ebe 100644 --- a/proto/teleport/web/teleterm/ptyhost/v1/pty_host_service.proto +++ b/proto/teleport/web/teleterm/ptyhost/v1/pty_host_service.proto @@ -93,6 +93,7 @@ message PtyEventOpen {} message PtyEventExit { uint32 exit_code = 1; optional uint32 signal = 2; + string last_input = 3; } // PtyEventStartError is sent by the PTY process when the shared process fails to start it. diff --git a/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts b/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts index fd12c6556204e..8d939cd5e498d 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts @@ -110,7 +110,11 @@ export class PtyEventsStreamHandler { } onExit( - callback: (reason: { exitCode: number; signal?: number }) => void + callback: (reason: { + exitCode: number; + signal?: number; + lastInput: string; + }) => void ): RemoveListenerFunction { return this.addDataListenerAndReturnRemovalFunction( (event: ManagePtyProcessResponse) => { diff --git a/web/packages/teleterm/src/services/pty/ptyHost/ptyProcess.ts b/web/packages/teleterm/src/services/pty/ptyHost/ptyProcess.ts index 10e3ede4b6074..24df852b6a0de 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/ptyProcess.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/ptyProcess.ts @@ -61,7 +61,13 @@ export function createPtyProcess( return stream.onOpen(callback); }, - onExit(callback: (reason: { exitCode: number; signal?: number }) => void) { + onExit( + callback: (reason: { + exitCode: number; + signal?: number; + lastInput: string; + }) => void + ) { return stream.onExit(callback); }, diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts index 9d9124c223703..9ff15de5b6fee 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts @@ -22,7 +22,6 @@ import { ManagePtyProcessRequest, ManagePtyProcessResponse, PtyEventData, - PtyEventExit, PtyEventOpen, PtyEventResize, PtyEventStart, @@ -94,12 +93,12 @@ export class PtyEventsStreamHandler { }) ) ); - this.ptyProcess.onExit(({ exitCode, signal }) => + this.ptyProcess.onExit(payload => this.stream.write( ManagePtyProcessResponse.create({ event: { oneofKind: 'exit', - exit: PtyEventExit.create({ exitCode, signal }), + exit: payload, }, }) ) diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.test.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.test.ts index 77d8a7dddad68..43c2065fc26d7 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.test.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.test.ts @@ -52,4 +52,29 @@ describe('PtyProcess', () => { ); }); }); + + if (process.platform !== 'win32') { + test('including the last input in the exit event', async () => { + const pty = new PtyProcess({ + path: 'sh', + env: {}, + args: [], + useConpty: true, + ptyId: '1234', + }); + await pty.start(80, 24); + const listener = jest.fn(); + pty.onExit(listener); + + pty.write('\x04'); + + await expect(() => listener.mock.calls.length > 0).toEventuallyBeTrue({ + waitFor: 2000, + tick: 10, + }); + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ lastInput: '\x04' }) + ); + }); + } }); diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts index 771065ded6a67..5f2835aa5ef8c 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts @@ -42,6 +42,7 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { private _logger: Logger; private _status: Status = 'not_initialized'; private _disposed = false; + private _lastInput = ''; constructor(private options: PtyProcessOptions & { ptyId: string }) { super(); @@ -115,6 +116,7 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { return; } + this._lastInput = data; this._process.write(data); } @@ -184,7 +186,9 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { return this.addListenerAndReturnRemovalFunction(TermEventEnum.Open, cb); } - onExit(cb: (ev: { exitCode: number; signal?: number }) => void) { + onExit( + cb: (ev: { exitCode: number; signal?: number; lastInput: string }) => void + ) { return this.addListenerAndReturnRemovalFunction(TermEventEnum.Exit, cb); } @@ -229,7 +233,7 @@ export class PtyProcess extends EventEmitter implements IPtyProcess { } private _handleExit(e: { exitCode: number; signal?: number }) { - this.emit(TermEventEnum.Exit, e); + this.emit(TermEventEnum.Exit, { ...e, lastInput: this._lastInput }); this._logger.info(`pty has been terminated with exit code: ${e.exitCode}`); this._setStatus('terminated'); } diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts index 792cf7c67ff69..9a898f95395d1 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/types.ts @@ -45,7 +45,7 @@ export type IPtyProcess = { onOpen(cb: () => void): RemoveListenerFunction; onStartError(cb: (message: string) => void): RemoveListenerFunction; onExit( - cb: (ev: { exitCode: number; signal?: number }) => void + cb: (ev: { exitCode: number; signal?: number; lastInput: string }) => void ): RemoveListenerFunction; }; From 06f2e0233741e724bc29ac52307db26d808b5f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 1 Oct 2025 17:32:39 +0200 Subject: [PATCH 2/3] Close terminal tab if last input was Ctrl+D --- .../src/ui/DocumentTerminal/useDocumentTerminal.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 4c28dfc0045fe..35444150996f4 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -209,15 +209,12 @@ async function setUpPtyProcess( ptyProcess.onExit(event => { // Not closing the tab on non-zero exit code lets us show the error to the user if, for example, - // tsh ssh cannot connect to the given node. + // tsh ssh couldn't connect to the given node. // - // The downside of this is that if you open a local shell, then execute a command that fails - // (for example, `cd` to a nonexistent directory), and then try to execute `exit` or press - // Ctrl + D, the tab won't automatically close, because the last exit code is not zero. - // - // We can look up how the terminal in vscode handles this problem, since in the scenario - // described above they do close the tab correctly. - if (event.exitCode === 0) { + // We also have to account for Ctrl+D, as executing it makes the shell exit with the last + // reported exit code. If we depended on the exit code alone, it'd mean that the terminal tab + // wouldn't close if Ctrl+D followed a command that failed, say cd to a nonexistent directory. + if (event.exitCode === 0 || event.lastInput === /* Ctrl+D */ '\x04') { documentsService.close(doc.uri); } }); From 49fc93baaebe263634576f716ae0d7528b823928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Tue, 14 Oct 2025 12:11:26 +0200 Subject: [PATCH 3/3] Remove use of create functions --- .../pty/ptyHost/ptyEventsStreamHandler.ts | 9 +-- .../ptyHost/ptyEventsStreamHandler.ts | 58 ++++++++----------- .../sharedProcess/ptyHost/ptyHostService.ts | 9 +-- 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts b/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts index 8d939cd5e498d..4b56a71777f07 100644 --- a/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts +++ b/web/packages/teleterm/src/services/pty/ptyHost/ptyEventsStreamHandler.ts @@ -21,9 +21,6 @@ import { DuplexStreamingCall } from '@protobuf-ts/runtime-rpc'; import { ManagePtyProcessRequest, ManagePtyProcessResponse, - PtyEventData, - PtyEventResize, - PtyEventStart, } from 'gen-proto-ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb'; import { @@ -56,7 +53,7 @@ export class PtyEventsStreamHandler { await this.send({ event: { oneofKind: 'start', - start: PtyEventStart.create({ columns, rows }), + start: { columns, rows }, }, }); } @@ -65,7 +62,7 @@ export class PtyEventsStreamHandler { await this.send({ event: { oneofKind: 'data', - data: PtyEventData.create({ message: data }), + data: { message: data }, }, }); } @@ -74,7 +71,7 @@ export class PtyEventsStreamHandler { await this.send({ event: { oneofKind: 'resize', - resize: PtyEventResize.create({ columns, rows }), + resize: { columns, rows }, }, }); } diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts index 9ff15de5b6fee..5726da4dcd5c6 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyEventsStreamHandler.ts @@ -22,10 +22,8 @@ import { ManagePtyProcessRequest, ManagePtyProcessResponse, PtyEventData, - PtyEventOpen, PtyEventResize, PtyEventStart, - PtyEventStartError, } from 'gen-proto-ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb'; import { @@ -74,44 +72,36 @@ export class PtyEventsStreamHandler { private handleStartEvent(event: PtyEventStart): void { this.ptyProcess.onData(data => - this.stream.write( - ManagePtyProcessResponse.create({ - event: { - oneofKind: 'data', - data: PtyEventData.create({ message: data }), - }, - }) - ) + this.stream.write({ + event: { + oneofKind: 'data', + data: { message: data }, + }, + }) ); this.ptyProcess.onOpen(() => - this.stream.write( - ManagePtyProcessResponse.create({ - event: { - oneofKind: 'open', - open: PtyEventOpen.create(), - }, - }) - ) + this.stream.write({ + event: { + oneofKind: 'open', + open: {}, + }, + }) ); this.ptyProcess.onExit(payload => - this.stream.write( - ManagePtyProcessResponse.create({ - event: { - oneofKind: 'exit', - exit: payload, - }, - }) - ) + this.stream.write({ + event: { + oneofKind: 'exit', + exit: payload, + }, + }) ); this.ptyProcess.onStartError(message => { - this.stream.write( - ManagePtyProcessResponse.create({ - event: { - oneofKind: 'startError', - startError: PtyEventStartError.create({ message }), - }, - }) - ); + this.stream.write({ + event: { + oneofKind: 'startError', + startError: { message }, + }, + }); }); // PtyProcess.prototype.start always returns a fulfilled promise. If an error is caught during // start, it's reported through PtyProcess.prototype.onStartError. Similarly, the information diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts index a5bd845c193f3..a0c40b8286b49 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyHostService.ts @@ -17,10 +17,6 @@ */ import { Struct } from 'gen-proto-ts/google/protobuf/struct_pb'; -import { - CreatePtyProcessResponse, - GetCwdResponse, -} from 'gen-proto-ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb'; import { IPtyHostService } from 'gen-proto-ts/teleport/web/teleterm/ptyhost/v1/pty_host_service_pb.grpc-server'; import Logger from 'teleterm/logger'; @@ -55,7 +51,7 @@ export function createPtyHostService(): IPtyHostService & { callback(error); return; } - callback(null, CreatePtyProcessResponse.create({ id: ptyId })); + callback(null, { id: ptyId }); logger.info(`created PTY process for id ${ptyId}`); }, getCwd: (call, callback) => { @@ -69,8 +65,7 @@ export function createPtyHostService(): IPtyHostService & { ptyProcess .getCwd() .then(cwd => { - const response = GetCwdResponse.create({ cwd }); - callback(null, response); + callback(null, { cwd }); }) .catch(error => { logger.error(`could not read CWD for id: ${id}`, error);