fix(windows): skip the ConPTY input-reader check on WinPTY panes (#36490)

node-pty falls back to WinPTY below Windows build 18309. WinPTY never sends
ConPTY's DA1 announcement, so every plain terminal was classified as having no
input reader and the healthy shell was killed and respawned. On v1.4.244 that
discard killed the WinPTY agent twice, and winpty's native kill throws
'Pty seems to have been killed already', failing every plain terminal spawn.

Fixes #36317
This commit is contained in:
Neil
2026-10-30 16:32:29 -07:00
committed by GitHub
parent 221e958689
commit 4b3ccc4158
2 changed files with 22 additions and 1 deletions
@@ -163,6 +163,14 @@ export function _resetConptyStartupTrustForTest(): void {
respawnCount = 0
}
type NodePtyWindowsPane = pty.IPty & { _agent?: { _useConpty?: boolean } }
// node-pty falls back to WinPTY below Windows build 18309, which never announces an input reader.
function runsOnWinpty(proc: pty.IPty): boolean {
const pane: NodePtyWindowsPane = proc
return pane._agent?._useConpty === false
}
function withNotice(claim: () => ConptyStartupEvents): () => ConptyStartupEvents {
return () => {
const events = claim()
@@ -186,7 +194,7 @@ export async function spawnConptyWithInputReader(
}> {
const firstSpawn = await spawn()
const { proc: first, physicalOwner: firstOwner } = firstSpawn
if (announcementUntrusted) {
if (announcementUntrusted || runsOnWinpty(first)) {
return { ...firstSpawn, claimStartupEvents: () => ({ data: [], exit: null }) }
}
const firstStartup = await decideConptyStartup(first, opts.signal, firstOwner)
@@ -367,6 +367,19 @@ describe('Windows shell fallback over node-pty ConPTY', () => {
expect(nodePty.spawn).toHaveBeenCalledTimes(3)
})
it('hands out a WinPTY pane unchecked, since only ConPTY announces an input reader', async () => {
// Windows builds below 18309 get WinPTY, whose native kill throws when repeated (#36317).
const winpty = Object.assign(deadPane(990001), { _agent: { _useConpty: false } })
nodePty.spawn.mockReturnValueOnce(winpty)
const result = await spawnNativeDaemonPty(args)
expect(nodePty.spawn).toHaveBeenCalledOnce()
expect(winpty.kill).not.toHaveBeenCalled()
expect(ptyJob.killConptyNow).not.toHaveBeenCalled()
expect(result.process).toBe(winpty)
})
it('does not respawn a shell that exited before ConPTY announced anything', async () => {
const exited = fakePane(990001, [], 3)
nodePty.spawn.mockReturnValueOnce(exited)