Commit 5f5a74a
fix(detection): kill leaked subprocess on task timeout, remove dead scan (#20)
* fix(detection): kill leaked subprocess on task timeout, remove dead scan
_execute_command ran the claude CLI via asyncio.create_subprocess_exec +
asyncio.wait_for(process.communicate(), timeout=...). wait_for only
cancels the await, not the child process, so on timeout the claude CLI
(and any MCP servers it spawned) kept running unkilled. The synchronous
equivalent in guardrail/adr_agent/adr_baseline.py already gets this
right via subprocess.run(..., timeout=...), which kills the child on
TimeoutExpired - this mirrors that same kill-and-reap behavior for the
async path.
Also removes a dead computation in execute_ads_task: existing_sessions
was built via a full recursive rglob("*.jsonl") over the host-wide,
ever-growing ~/.claude/projects/ directory on every single task, then
passed into _process_results, which never referenced it. Confirmed via
grep - pure wasted I/O, safe to delete along with the now-unused
parameter.
Added TestTaskExecutorExecuteCommand covering the timeout-kill behavior
(verified by temporarily reverting the fix and confirming the new test
fails against the original code - the leaked process's returncode was
still None after the call), the success path, and non-zero exit
handling.
Fixes #18
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(detection): kill whole process group on timeout, not just the direct child
Addresses review feedback on PR #20 from @pengyuzhang, who ran a
differential harness (child spawning a grandchild subprocess, probing
both PIDs after _execute_command returns) and found two real gaps in
the previous kill-on-timeout fix:
1. process.kill() only signals the claude CLI itself - any descendant
it spawned (e.g. an MCP server) survives.
2. await process.wait() can block far past the configured timeout -
potentially indefinitely - if a descendant inherited the CLI's
stdout/stderr pipes and holds them open, since asyncio's subprocess
transport only reports completion once those pipes close. Measured
at 120s in the review's harness for a 2s timeout; an unbounded
orphan would wedge one of the max_concurrent execution slots
forever.
Fix, per the review's suggested approach: start_new_session=True puts
the CLI and everything it spawns in one process group. On timeout,
signal the whole group - SIGTERM first for a clean shutdown, bounded
wait, SIGKILL escalation if it hasn't exited within the grace period.
Falls back to killing just the direct child on platforms without
process-group support (os.killpg), since start_new_session and
os.killpg are POSIX-only - this benchmark harness has no other
Windows-specific handling, but the fallback keeps the code from
raising if it's ever imported there.
Added test_kills_grandchild_process_holding_inherited_stdio,
reproducing the review's exact scenario: a child that spawns its own
child without redirecting stdout/stderr, so the grandchild inherits
the same pipes asyncio set up for the direct child. Asserts the call
returns promptly (not hanging toward the grandchild's full sleep
duration) and that both processes are actually gone afterward, polling
via os.kill(pid, 0) rather than a single check to avoid a false
negative against a not-yet-reaped zombie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Pengyu Zhang <zhangelsu@gmail.com>1 parent 8f83e9a commit 5f5a74a
2 files changed
Lines changed: 183 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| 16 | + | |
15 | 17 | | |
16 | 18 | | |
17 | 19 | | |
| |||
675 | 677 | | |
676 | 678 | | |
677 | 679 | | |
678 | | - | |
679 | | - | |
680 | | - | |
681 | | - | |
682 | | - | |
683 | 680 | | |
684 | 681 | | |
685 | 682 | | |
| |||
689 | 686 | | |
690 | 687 | | |
691 | 688 | | |
692 | | - | |
693 | | - | |
694 | | - | |
| 689 | + | |
695 | 690 | | |
696 | 691 | | |
697 | 692 | | |
| |||
781 | 776 | | |
782 | 777 | | |
783 | 778 | | |
| 779 | + | |
784 | 780 | | |
785 | 781 | | |
786 | 782 | | |
787 | 783 | | |
788 | 784 | | |
789 | | - | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
790 | 790 | | |
791 | 791 | | |
792 | 792 | | |
| |||
802 | 802 | | |
803 | 803 | | |
804 | 804 | | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
805 | 817 | | |
806 | 818 | | |
807 | 819 | | |
808 | 820 | | |
809 | | - | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
810 | 847 | | |
811 | 848 | | |
812 | 849 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
3 | 7 | | |
4 | 8 | | |
5 | 9 | | |
6 | 10 | | |
7 | 11 | | |
8 | | - | |
| 12 | + | |
9 | 13 | | |
10 | 14 | | |
11 | 15 | | |
| |||
101 | 105 | | |
102 | 106 | | |
103 | 107 | | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
0 commit comments