Skip to content

Windows: consider per-pane Job Objects for tree teardown (vs taskkill exec-out) #128

Description

@c1-squire-dev

Context

PR #124 ships descendant-tree teardown on Windows via taskkill /F /T /PID (kill_tree_windows in crates/agent-tui-daemon/src/pty.rs). That was a deliberate pragmatic call, recorded here so it is a decision rather than an accident.

The purist alternative: one Job Object per paneCreateJobObject + AssignProcessToJobObject at spawn, TerminateJobObject on teardown. No exec, no external binary dependency, atomic tree kill.

Why it is not a drop-in

  1. Assignment race — a job captures future children of assigned processes, so correct usage is spawn-suspended → assign → resume. portable-pty owns the ConPTY CreateProcessW; the daemon can only assign post-spawn via Child::as_raw_handle(), and a fast child can spawn a grandchild in that window, escaping the job.
  2. conhost is outside the child's tree — the ConPTY host is spawned by the console subsystem, not the pane child, so no tree-kill mechanism reaches it by parentage. ClosePseudoConsole (see PtyChild::close_master) is required regardless; a job would not replace the whole teardown.
  3. Nested-job compatAssignProcessToJobObject fails when the child is already in a non-nestable job (CI agents, Windows Terminal, EDR sandboxes run processes under jobs). Windows 8+ nests, but the matrix needs a documented stance and test coverage.
  4. The in-process alternative is literally taskkill — a Toolhelp32 snapshot walk + per-PID TerminateProcess is taskkill's own implementation, except in-crate we would own the PID-reuse TOCTOU races. (The root PID is stable either way: the daemon retains the child handle, and Windows does not reuse a PID while a handle to the process object is open.)

Also verified during review

  • portable-pty 0.9's Windows kill path is unusable: win/mod.rs do_kill inverts the TerminateProcess success BOOL (returns Err on success), and WinChild::kill swallows it into an unconditional Ok(()). It also never assigns children to a job and does not pass CREATE_NEW_PROCESS_GROUP.

Possible shape

  • Per-pane job created at pane spawn; child assigned post-spawn via as_raw_handle() (accepting the small assignment race, or closing it later by taking over the ConPTY spawn the way spawn_headless_windows already owns the headless spawn).
  • TerminateJobObject in PtyChild::kill / reap_all_panes; conhost still via ClosePseudoConsole.
  • Fallback to the current taskkill path when assignment fails (already-in-a-job).

Non-urgent: the taskkill path is hardened (absolute %SystemRoot% path, CREATE_NO_WINDOW, exit-128 = already-gone).

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