From 9a5828f752a55c53b1a4b019616b923cb7fcb257 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 25 Jun 2026 14:12:44 +0200 Subject: [PATCH] fix: surface CreateProcessW failures as 'exit' instead of uncaughtException on Windows --- src/windowsPtyAgent.ts | 21 +++++++++++++++++++-- src/windowsTerminal.test.ts | 14 ++++++++++++++ src/windowsTerminal.ts | 6 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index cdc804150..06450864e 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -10,6 +10,7 @@ import { fork } from 'child_process'; import { Socket } from 'net'; import { ArgvOrCommandLine } from './types'; import { ConoutConnection } from './windowsConoutConnection'; +import { EventEmitter2, IEvent } from './eventEmitter2'; import { loadNativeModule } from './utils'; let conptyNative: IConptyNative; @@ -32,6 +33,9 @@ export class WindowsPtyAgent { private _exitCode: number | undefined; private _conoutSocketWorker: ConoutConnection; + private _onError = new EventEmitter2(); + public get onError(): IEvent { return this._onError.event; } + private _fd: any; private _pty: number; private _ptyNative: IConptyNative; @@ -124,8 +128,21 @@ export class WindowsPtyAgent { const { pty, commandLine, cwd, env } = this._pendingPtyInfo; this._pendingPtyInfo = undefined; - const connect = conptyNative.connect(pty, commandLine, cwd, env, this._useConptyDll, c => this._$onProcessExit(c)); - this._innerPid = connect.pid; + try { + const connect = conptyNative.connect(pty, commandLine, cwd, env, this._useConptyDll, c => this._$onProcessExit(c)); + this._innerPid = connect.pid; + } catch (err) { + // connect() runs from the conout worker's onReady callback (or its + // timeout fallback), so a throw here would otherwise surface as an + // uncaughtException with no way for the consumer to observe it. + const code = /error code: (\d+)/.exec((err as Error).message)?.[1]; + this._exitCode = code ? parseInt(code, 10) : -1; + try { this._ptyNative.kill(this._pty, this._useConptyDll); } catch { /* already gone */ } + this._conoutSocketWorker.dispose(); + this._inSocket.destroy(); + this._outSocket.destroy(); + this._onError.fire(err as Error); + } } public resize(cols: number, rows: number): void { diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index 303086ae4..c76196512 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -238,6 +238,20 @@ if (process.platform === 'win32') { }); }); + describe('connect failure', () => { + it('should emit exit instead of an uncaught exception when CreateProcessW fails', function (done) { + this.timeout(10000); + // Must exist (startProcess validates that) but not be a valid executable. + const notAnExe = path.join(__dirname, '..', 'package.json'); + const term = new WindowsTerminal(notAnExe, [], { useConptyDll }); + term.on('exit', (code) => { + assert.notStrictEqual(code, 0); + assert.strictEqual(term.pid, 0); + done(); + }); + }); + }); + describe('On close', () => { it('should return process zero exit codes', function (done) { this.timeout(10000); diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index 55f7dc7f2..3f88f4dee 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -50,6 +50,12 @@ export class WindowsTerminal extends Terminal { // Create new termal. this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor); this._socket = this._agent.outSocket; + // The socket 'close' handler that drives 'exit' is only attached after + // ready_datapipe, which never fires when connect() fails. + this._agent.onError(() => { + this._close(); + this.emit('exit', this._agent.exitCode); + }); // Not available until `ready` event emitted. this._pid = this._agent.innerPid;