Skip to content

fix(shell): guard Windows pty teardown against blocked process creation - #3918

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/fix-windows-pty-teardown-unhandled-rejection
Draft

fix(shell): guard Windows pty teardown against blocked process creation#3918
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/fix-windows-pty-teardown-unhandled-rejection

Conversation

@posthog

@posthog posthog Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Closing a terminal on Windows could produce a spawn UNKNOWN error report from the Electron main process, affecting a small number of Windows users.

node-pty's WindowsPtyAgent.kill() forks a conpty_console_list_agent helper to enumerate the ConPTY console process list so it can force-kill leftover console processes. fork() throws synchronously when CreateProcess fails outright — libuv's generic UNKNOWN, typically antivirus or EDR blocking process creation.

That fork() sits inside a new Promise(...) executor, and the consuming .then() has no rejection handler. So the failure became an unhandled rejection on every affected terminal close, and the console-process reap was skipped entirely.

Worth correcting one detail from the originating report: this is not a synchronous throw escaping pty.destroy(), so a try/catch at our call sites would not have caught it, and the reported side effects (leaked session map entry, aborted shutdown sweep) did not actually occur for this error — the rejection is asynchronous, so sessions.delete() still ran and the loop still completed. The captured event confirms it: mechanism.type = onunhandledrejection, and our own handler tagged it unhandledRejection, not uncaughtException. The defect is still real, just one layer down — and still unguarded in the latest upstream beta, so patching is the route.

Changes

The actual fix — extend patches/node-pty.patch (this repo already patches node-pty):

  • Guard the fork() in _getConsoleProcessList() and fall back to the shell PID, mirroring the existing 5s-timeout fallback. This also restores the force-kill for the shell process, which was being skipped.
  • Attach an error listener to the forked helper (an unhandled error event on a ChildProcess throws).
  • Add .catch() on the reap chain so teardown can never surface as an unhandled rejection.

Defence-in-depth, not what fixes the aboveShellService teardown was duplicated across three call sites. They now share one teardownSession() that drops the session from the map before releasing the pty and guards pty.destroy(). This closes a genuine latent hazard of the shape the report described (the native _ptyNative.kill() inside destroy() can throw synchronously), which would otherwise leak a session entry and abort the destroyAll() shutdown sweep, leaving shells running after quit.

How did you test this?

  • Reproduced the failure against the real node-pty code, stubbing child_process.fork to throw spawn UNKNOWN and driving WindowsPtyAgent.prototype._getConsoleProcessList through the exact shape kill() uses (.then() with no rejection handler):
    • pristine 1.1.0 → unhandledRejection: spawn UNKNOWN, reaped PIDs null (reap skipped) — matching the captured event
    • patched → no unhandled rejection, reap falls back to the shell PID
  • Verified the patch applies cleanly to pristine node-pty 1.1.0 and confirmed it landed in the installed node_modules/node-pty after install; re-ran the repro against the installed package.
  • pnpm install --frozen-lockfile passes, confirming the updated patch_hash in pnpm-lock.yaml is consistent.
  • Two new regression tests in shell.test.ts; full file passes (9/9). Confirmed non-vacuous by reverting shell.ts to main — both new tests fail with spawn UNKNOWN and the other 7 still pass.
  • biome check clean on the touched paths. tsc --noEmit reports zero errors in shell files (the 54 remaining are pre-existing, from unbuilt @posthog/agent types).
  • Not verified on a real Windows box with EDR blocking CreateProcess — no such host available here; the fault was injected at the fork() boundary instead.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Desktop from this inbox report.

Closing a terminal on Windows could produce a `spawn UNKNOWN` error report from
the Electron main process.

node-pty's `WindowsPtyAgent.kill()` forks a `conpty_console_list_agent` helper to
enumerate the ConPTY console process list. That `fork()` throws synchronously
when CreateProcess fails outright (antivirus/EDR blocking process creation), and
it sits inside a Promise executor whose `.then()` has no rejection handler — so
it surfaced as an unhandled rejection during routine shell teardown, and the
console-process reap was skipped entirely.

Extend patches/node-pty.patch to guard the fork and fall back to the shell PID
(matching the existing timeout path), attach an `error` listener to the forked
helper, and add a `.catch()` on the reap chain so teardown can never surface as
an unhandled rejection.

Separately, harden ShellService teardown: the three duplicated teardown blocks
now share one `teardownSession()` that drops the session from the map before
releasing the pty and guards `pty.destroy()`, so a synchronous failure can't
leak a session entry or abort the `destroyAll()` shutdown sweep.

Generated-By: PostHog Code
Task-Id: aa8c2e1f-6579-4308-bc35-da21fbfca990
@trunk-io

trunk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit acf1f07.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants