Skip to content

Windows: ConoutConnection worker thread prevents Node.js from exiting after kill() #887

Open

Description

Environment

  • OS: Windows (ConPTY path)
  • node-pty version: 1.2.0-beta.10
  • Node.js: v22.22.0

Description

After calling kill() on a Windows PTY and awaiting its exit event, Node.js cannot exit because active handles remain on the event loop. This forces consumers to call process.exit() as a workaround.

The root cause is that the ConoutConnection worker thread, its internal sockets, and several cleanup timeouts are never unref()'d. There is no .unref() call anywhere in node-pty's source.

Reproduction

const pty = require('node-pty');

const term = pty.spawn('cmd.exe', [], { cols: 80, rows: 24 });

term.onExit(() => {
  console.log('exited');
  // Node.js should exit here, but it hangs indefinitely
});

setTimeout(() => term.kill(), 500);

Expected: process exits after "exited" is logged.
Actual: process hangs indefinitely.

Root cause

There are three categories of handles keeping the event loop alive after kill():

1. Worker thread (primary)

src/windowsConoutConnection.ts:47 — the Worker is created but never unref()'d:

this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });

The worker runs a net.Socket + net.createServer (src/worker/conoutSocketWorker.ts:11-17) that are also never unref()'d. Both the worker thread and its internal sockets keep the event loop alive.

2. Drain timeout

When dispose() is called, it schedules a 1-second timeout before terminating the worker (src/windowsConoutConnection.ts:76):

this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);

This timeout is not unref()'d, keeping the event loop alive for the duration.

A similar un-unref()'d timeout exists in WindowsPtyAgent._flushDataAndCleanUp() (src/windowsPtyAgent.ts:179):

this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);

3. I/O sockets

_outSocket and _inSocket in WindowsPtyAgent (src/windowsPtyAgent.ts:78-94) are never unref()'d.

Suggested fix

Call unref() on handles that shouldn't prevent process exit:

// windowsConoutConnection.ts — constructor
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });
this._worker.unref();

// windowsConoutConnection.ts — _drainDataAndClose
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
this._drainTimeout.unref();

// windowsPtyAgent.ts — constructor (sockets)
this._outSocket.unref();
this._inSocket.unref();

// windowsPtyAgent.ts — _flushDataAndCleanUp
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
this._closeTimeout.unref();

This ensures cleanup completes if other work keeps the event loop alive, but doesn't prevent the process from exiting when everything else is done.

Activity

  1. DrDexter6000 commented on Apr 6, 2026

    @DrDexter6000

    This issue's Windows ConOutConnection worker thread preventing Node.js from exiting after kill() may be related to a Node.js Windows runtime bug:

    **nodejs/node#62125 ??Node.js v24.13.x on Windows silently terminates child processes spawned with child_process.spawn({ detached: true }) via TerminateProcess() with no JS exit handlers, no diagnostic report, and no Event Log entry. The process simply vanishes after ~4-6 seconds.

    We independently verified this on Windows ( Chrome Native Messaging host chain):

    • Bare setInterval(() => {}, 1000) in detached spawn dies in ~4-6s on v24.13.1
    • All exit handlers fail to fire ??TerminateProcess() bypasses all JS cleanup
    • Python equivalent survives indefinitely under identical conditions ??Node.js-specific
    • Node v22.22.2 LTS works: 137+ seconds uptime verified
    • Digital signature verified: valid OpenJS Foundation signature, ruling out AV/trust policy

    The Note: node-pty already has a Windows process lifecycle issues. If your ConoutConnection worker thread bug (#887) is related to Node.js v24+'s Windows process management regressions. The upstream bug in nodejs/node#62125 may help explain the silent termination behavior on Windows:

    • Bare setInterval(() => {}, 1000) in detached spawn dies in ~4-6s on v24.13.1 (zero I/O, zero business logic)
    • Node v22.22.2 LTS: 137+ seconds uptime verified
    • Workaround: nvm install 22 && nvm use 22
  2. DrDexter6000 commented on Apr 6, 2026

    @DrDexter6000

    This issue documents a Windows process lifecycle behavior that may be related to a Node.js v24/v25 Windows regression:

    nodejs/node#62125 ??Node.js v24.13.x on Windows silently terminates child processes spawned with child_process.spawn({ detached: true }) via TerminateProcess() with no JS exit handlers, no diagnostic report, and no Event Log entry. The process simply vanishes after ~4-6 seconds.

    We independently verified this on Windows (Chrome Native Messaging host chain):

    • Bare setInterval(() => {}, 1000) in detached spawn dies in ~4-6s on v24.13.1 (zero I/O, zero business logic)
    • All exit handlers fail to fire ??TerminateProcess() bypasses all JS cleanup
    • Python equivalent survives indefinitely under identical conditions
    • Node v22.22.2 LTS works: 137+ seconds uptime verified
    • Digital signature verified: valid OpenJS Foundation signature, ruling out AV/trust policy

    The node-pty ConoutConnection worker thread issue may be another symptom of Node.js v24+'s Windows process management regressions. Worth cross-referencing.

    Workaround: nvm install 22 && nvm use 22

  3. DrDexter6000 commented on Apr 6, 2026

    @DrDexter6000

    Important Update: Windows Security Behavioral Detection Confirmed

    After further investigation, we now have direct evidence that Windows Security (Defender) behavioral detection is killing detached child processes on Windows. This may be a contributing factor alongside any Node.js v24 runtime behavior.

    Evidence

    On 2026-04-06 at 08:23 local time, while running minimal detached spawn reproduction tests with Node.js v24.13.1 on Windows 11, Windows Security flagged and terminated the test script:

    • Detection: Trojan:Win32/SuspExec.SE (Suspicious Execution)
    • Status: Removed (process killed)
    • Action: The process was terminated immediately with no notification to the Node.js runtime

    The Windows Security Protection History shows the exact command line that was flagged. The test script was a minimal reproduction: spawn a detached node -e 'setInterval(()=>{},1000)' child, then poll it with process.kill(pid, 0).

    Why This Pattern Triggers Detection

    Windows Security behavioral analysis flags this pattern because it resembles C2 beacon/persistence behavior:

    1. PowerShell writes a script to disk
    2. Script spawns a detached, never-exiting child process
    3. Parent polls child liveness at regular intervals
    4. Child has no visible I/O or user-facing activity

    This is indistinguishable from malware persistence techniques at the behavioral level.

    Key Observation: Other Tools Work Fine

    On the same machine, other browser automation tools work without issues:

    • Claude Code (desktop): browser automation works perfectly
    • Manus AI (web): browser automation works perfectly

    This suggests the issue is not purely a Node.js v24 runtime bug. If it were, all Node.js-based tools would be equally affected.

    Revised Hypothesis

    The root cause is likely a combination of factors:

    1. Windows Security behavioral detection (confirmed): kills processes that match the SuspExec pattern, regardless of digital signature validity. The detection is behavior-based, not signature-based.
    2. Node.js v24 compatibility: Windows Security may not have whitelisted/recognized the behavioral profile of Node.js v24 yet (newer runtime = fewer established behavioral patterns = higher false positive rate).
    3. Spawn pattern similarity to malware: detached: true + stdio: 'ignore' + child.unref() + polling is exactly how many persistence implants work.

    Why Node v22 LTS Works

    Node v22 LTS has been around longer. Windows Security has likely built behavioral whitelisting for common Node.js v22 spawn patterns. Node v24 is newer and its behavioral profile may not be fully recognized yet.

    Updated Workaround

    In addition to using Node v22 LTS, also consider:

    1. Add your project directory to Windows Security exclusions (Settings > Privacy & Security > Windows Security > Virus & Threat Protection > Exclusions)
    2. Use Node v22 LTS for spawning long-lived detached processes: nvm install 22 && nvm use 22
    3. If developing/testing detached spawn patterns on Windows, be aware that Windows Security real-time protection may silently kill your processes

    This finding also explains the "invisible hand" effect we observed: processes dying after ~4-6 seconds with zero diagnostic output, zero JS exit handlers firing, and zero Event Log entries. Windows Security TerminateProcess() produces exactly this symptom profile.

  4. DrDexter6000 commented on Apr 6, 2026

    @DrDexter6000

    ⚠️ Correction: Root Cause Was Windows Defender, Not Node v24 Bug

    TL;DR: Our original diagnosis was wrong. The actual root cause was Windows Defender behavior detection (Trojan:Win32/SuspExec.SE), not Node.js v24 silently terminating detached processes.

    What Changed

    After adding Windows Defender exclusions for OpenCode.exe, opencode-cli.exe, node.exe, and their install directories:

    • Node v24.13.1 detached spawn: ✅ Stable 8+ seconds (previously died at 4-6s)
    • Chrome headless launch: ✅ Normal (previously killed by Defender)
    • CDP browser automation (full chain): ✅ Tab create → navigate → JS eval → DOM modify → close — all pass

    The same Node v24.13.1 binary, same code, same machine — the only change was Defender exclusions.

    Corrected Diagnosis

    Before (Wrong) After (Correct)
    Node v24 silently terminates detached children via TerminateProcess() Windows Defender behavior detection flags AI coding agent processes as Trojan:Win32/SuspExec.SE and kills them
    Workaround: downgrade to Node v22 Workaround: add Defender exclusions for AI coding tools
    nodejs/node#62125 is the root cause nodejs/node#62125 may still be a real issue under other conditions, but our specific reproduction case was Defender

    Why the Misdiagnosis

    1. TerminateProcess() was called — but by Windows Defender, not Node.js internals
    2. No exit handlers fired — because Defender's TerminateProcess() is a forced kill, identical behavior to what we attributed to Node
    3. Python survived — not because of language differences, but because Defender's heuristic didn't flag Python processes the same way
    4. Digital signature was valid — Defender uses behavioral heuristics, not just signature checks

    Evidence Log

    • Windows Security popup: Trojan:Win32/SuspExec.SE detected during debugging session
    • Claude Code and Manus AI browser automation worked fine on same machine (different process signatures)
    • After Defender exclusions: all tests pass, including bare setInterval detached spawn on v24.13.1

    Apologies for the initial misattribution. The nodejs/node#62125 issue may still be valid for other reporters — we can only correct our own reproduction case.


    Root cause analysis documented at: [Gist link TBD]

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions