Kill a process and all its child processes. On Windows, subprocess.terminate() only kills the immediate process, leaving orphaned child processes (e.g., spawned browser instances, coding/testing agents). This function uses psutil to kill the entire process tree. Args: proc:
(proc: subprocess.Popen, timeout: float = 5.0)
| 38 | |
| 39 | |
| 40 | def kill_process_tree(proc: subprocess.Popen, timeout: float = 5.0) -> KillResult: |
| 41 | """Kill a process and all its child processes. |
| 42 | |
| 43 | On Windows, subprocess.terminate() only kills the immediate process, leaving |
| 44 | orphaned child processes (e.g., spawned browser instances, coding/testing agents). |
| 45 | This function uses psutil to kill the entire process tree. |
| 46 | |
| 47 | Args: |
| 48 | proc: The subprocess.Popen object to kill |
| 49 | timeout: Seconds to wait for graceful termination before force-killing |
| 50 | |
| 51 | Returns: |
| 52 | KillResult with status and statistics about the termination |
| 53 | """ |
| 54 | result = KillResult(status="success", parent_pid=proc.pid) |
| 55 | |
| 56 | try: |
| 57 | parent = psutil.Process(proc.pid) |
| 58 | # Get all children recursively before terminating |
| 59 | children = parent.children(recursive=True) |
| 60 | result.children_found = len(children) |
| 61 | |
| 62 | logger.debug( |
| 63 | "Killing process tree: PID %d with %d children", |
| 64 | proc.pid, len(children) |
| 65 | ) |
| 66 | |
| 67 | # Terminate children first (graceful) |
| 68 | for child in children: |
| 69 | try: |
| 70 | logger.debug("Terminating child PID %d (%s)", child.pid, child.name()) |
| 71 | child.terminate() |
| 72 | except (psutil.NoSuchProcess, psutil.AccessDenied) as e: |
| 73 | # NoSuchProcess: already dead |
| 74 | # AccessDenied: Windows can raise this for system processes or already-exited processes |
| 75 | logger.debug("Child PID %d already gone or inaccessible: %s", child.pid, e) |
| 76 | |
| 77 | # Wait for children to terminate |
| 78 | gone, still_alive = psutil.wait_procs(children, timeout=timeout) |
| 79 | result.children_terminated = len(gone) |
| 80 | |
| 81 | logger.debug( |
| 82 | "Children after graceful wait: %d terminated, %d still alive", |
| 83 | len(gone), len(still_alive) |
| 84 | ) |
| 85 | |
| 86 | # Force kill any remaining children |
| 87 | for child in still_alive: |
| 88 | try: |
| 89 | logger.debug("Force-killing child PID %d", child.pid) |
| 90 | child.kill() |
| 91 | result.children_killed += 1 |
| 92 | except (psutil.NoSuchProcess, psutil.AccessDenied) as e: |
| 93 | logger.debug("Child PID %d gone during force-kill: %s", child.pid, e) |
| 94 | |
| 95 | if result.children_killed > 0: |
| 96 | result.status = "partial" |
| 97 |
no test coverage detected