5e1867bb50
After test runs that use live_gui, dozens of sloppy.py --enable-test-hooks processes can leak (the watchdoge1c8730fbounds the hang but doesn't kill the spawned GUI subprocesses). This script: - Enumerates all python.exe / uv.exe processes via CIM - Categorizes each by command-line content: - sloppy.py --enable-test-hooks -> KILL (orphans) - scripts/mcp_server.py -> PRESERVE (manual_slop's MCP server, used by opencode) - minimax-coding-plan-mcp -> PRESERVE (opencode's MCP server, used by opencode) - pytest runner / stuck App() test -> PRESERVE by default, kill with --kill-tests - Defaults to DRY-RUN; pass --kill to terminate - --kill-tests: also kill stuck test subprocesses - --kill-mcp: also kill MCP servers (off by default; usually DON'T want this) - --json: machine-readable output for CI/scripting Verified after a 10-batch test run: 28 sloppy.py orphans identified, 21 MCP servers (9 manual_slop + 12 minimax) preserved correctly. The watchdog fix (e1c8730f) bounds the test hang; this script cleans up the leaked GUI subprocesses afterward. Usage: uv run python scripts/cleanup_orphaned_processes.py # dry-run uv run python scripts/cleanup_orphaned_processes.py --kill # kill sloppy.py orphans uv run python scripts/cleanup_orphaned_processes.py --kill --kill-tests
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
"""Identify and clean up orphaned manual_slop processes left over from prior test runs.
|
|
|
|
Categorizes running python/uv processes into:
|
|
- sloppy.py: manual_slop GUI app spawned by live_gui tests; KILL (these are the orphans)
|
|
- mcp_server.py: manual_slop's MCP server; preserve (legitimate, used by opencode)
|
|
- minimax-coding-plan-mcp: opencode's MCP server; preserve (legitimate, used by opencode)
|
|
- test subprocess: long-running pytest/test helpers; flag, kill if --kill-tests
|
|
|
|
Default mode is DRY-RUN (lists orphans, prints what would be killed, kills nothing).
|
|
Pass --kill to actually kill sloppy.py orphans.
|
|
Pass --kill-tests to also kill the "test subprocess" category.
|
|
Pass --kill-mcp to kill the MCP servers (off by default; usually you don't want to).
|
|
Pass --json for machine-readable output.
|
|
|
|
Usage:
|
|
uv run python scripts/cleanup_orphaned_processes.py # dry-run
|
|
uv run python scripts/cleanup_orphaned_processes.py --kill # kill sloppy.py orphans
|
|
uv run python scripts/cleanup_orphaned_processes.py --kill --kill-tests
|
|
uv run python scripts/cleanup_orphaned_processes.py --json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProcInfo:
|
|
pid: int
|
|
name: str
|
|
command_line: str
|
|
age_seconds: float
|
|
cpu_seconds: float
|
|
category: str
|
|
|
|
def render(self) -> str:
|
|
cmd_short = self.command_line[:120].replace("\n", " ").replace("\r", " ")
|
|
return (f" PID {self.pid:>6} {self.name:<10} age={self.age_seconds:>7.1f}s "
|
|
f"cpu={self.cpu_seconds:>7.1f}s {self.category}\n"
|
|
f" cmd: {cmd_short}")
|
|
|
|
|
|
def _enum_processes() -> list[ProcInfo]:
|
|
r = subprocess.run(
|
|
["powershell.exe", "-NoProfile", "-Command",
|
|
"Get-CimInstance Win32_Process -Filter \"Name='python.exe' OR Name='uv.exe'\" "
|
|
"| Where-Object { $_.CommandLine } "
|
|
"| Select-Object ProcessId, Name, CommandLine, CreationDate, KernelModeTime, UserModeTime "
|
|
"| ConvertTo-Json -Depth 2 -Compress"],
|
|
capture_output=True, text=True, timeout=15,
|
|
)
|
|
if r.returncode != 0:
|
|
print(f"ERROR: powershell enumeration failed: {r.stderr}", file=sys.stderr)
|
|
return []
|
|
out = r.stdout.strip()
|
|
if not out or out == "null":
|
|
return []
|
|
raw = json.loads(out)
|
|
if isinstance(raw, dict):
|
|
raw = [raw]
|
|
procs: list[ProcInfo] = []
|
|
for item in raw:
|
|
pid = int(item.get("ProcessId", 0))
|
|
name = str(item.get("Name", ""))
|
|
cl = str(item.get("CommandLine", "") or "")
|
|
if not cl:
|
|
continue
|
|
creation = item.get("CreationDate", "")
|
|
try:
|
|
from datetime import datetime, timezone
|
|
dt = datetime.fromisoformat(creation.replace("Z", "+00:00"))
|
|
age = (datetime.now(timezone.utc) - dt).total_seconds()
|
|
except Exception:
|
|
age = 0.0
|
|
kernel = float(item.get("KernelModeTime", 0) or 0) / 1e7
|
|
user = float(item.get("UserModeTime", 0) or 0) / 1e7
|
|
cpu = kernel + user
|
|
cat = _categorize(cl)
|
|
if cat is None:
|
|
continue
|
|
procs.append(ProcInfo(pid=pid, name=name, command_line=cl, age_seconds=age, cpu_seconds=cpu, category=cat))
|
|
return procs
|
|
|
|
|
|
def _categorize(cl: str) -> str | None:
|
|
if "sloppy.py" in cl and "--enable-test-hooks" in cl:
|
|
return "sloppy.py (test leftover)"
|
|
if "scripts/mcp_server.py" in cl or "manual_slop" in cl and "mcp" in cl.lower():
|
|
return "manual_slop MCP server (preserve)"
|
|
if "minimax-coding-plan-mcp" in cl:
|
|
return "minimax MCP server (preserve)"
|
|
if "pytest" in cl and "conftest" not in cl:
|
|
return "pytest runner"
|
|
if "src.gui_2" in cl and "App()" in cl:
|
|
return "stuck test subprocess (src.gui_2 + App)"
|
|
return None
|
|
|
|
|
|
def _kill(pid: int) -> bool:
|
|
r = subprocess.run(["powershell.exe", "-NoProfile", "-Command", f"Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue; if (Get-Process -Id {pid} -ErrorAction SilentlyContinue) {{ 'STILL_ALIVE' }} else {{ 'KILLED' }}"],
|
|
capture_output=True, text=True, timeout=10)
|
|
return "KILLED" in r.stdout
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
ap.add_argument("--kill", action="store_true", help="Actually kill sloppy.py orphans (default: dry-run)")
|
|
ap.add_argument("--kill-tests", action="store_true", help="Also kill stuck test subprocesses")
|
|
ap.add_argument("--kill-mcp", action="store_true", help="Also kill MCP servers (usually DON'T want this)")
|
|
ap.add_argument("--json", action="store_true", help="Machine-readable JSON output")
|
|
args = ap.parse_args()
|
|
|
|
procs = _enum_processes()
|
|
by_cat: dict[str, list[ProcInfo]] = {}
|
|
for p in procs:
|
|
by_cat.setdefault(p.category, []).append(p)
|
|
|
|
if args.json:
|
|
out = {
|
|
"dry_run": not args.kill,
|
|
"categories": {cat: [asdict(p) for p in items] for cat, items in by_cat.items()},
|
|
}
|
|
print(json.dumps(out, indent=2))
|
|
return 0
|
|
|
|
print("=" * 80)
|
|
print(f"manual_slop process cleanup ({'DRY-RUN' if not args.kill else 'KILL MODE'})")
|
|
print("=" * 80)
|
|
|
|
kill_targets: list[ProcInfo] = []
|
|
preserve_targets: list[ProcInfo] = []
|
|
|
|
for cat, items in by_cat.items():
|
|
is_orphan = cat.startswith("sloppy.py")
|
|
is_test = cat.startswith("pytest") or cat.startswith("stuck test")
|
|
is_mcp = "MCP server" in cat
|
|
if is_orphan:
|
|
kill_targets.extend(items)
|
|
header = f"[KILL: sloppy.py orphans] ({len(items)} found)"
|
|
elif is_test and args.kill_tests:
|
|
kill_targets.extend(items)
|
|
header = f"[KILL: test subprocesses] ({len(items)} found)"
|
|
elif is_mcp and args.kill_mcp:
|
|
kill_targets.extend(items)
|
|
header = f"[KILL: MCP servers] ({len(items)} found)"
|
|
else:
|
|
preserve_targets.extend(items)
|
|
header = f"[PRESERVE] ({len(items)} found) {cat}"
|
|
print()
|
|
print(header)
|
|
for p in items:
|
|
print(p.render())
|
|
|
|
print()
|
|
print("=" * 80)
|
|
print(f"Totals: {len(kill_targets)} kill candidates, {len(preserve_targets)} preserved")
|
|
print("=" * 80)
|
|
|
|
if not kill_targets:
|
|
print("Nothing to kill.")
|
|
return 0
|
|
|
|
if not args.kill:
|
|
print()
|
|
print("DRY-RUN: pass --kill to actually terminate the above kill candidates.")
|
|
return 0
|
|
|
|
print()
|
|
print(f"Killing {len(kill_targets)} process(es)...")
|
|
killed = 0
|
|
failed = []
|
|
for p in kill_targets:
|
|
if _kill(p.pid):
|
|
killed += 1
|
|
print(f" killed PID {p.pid:>6} ({p.category})")
|
|
else:
|
|
failed.append(p.pid)
|
|
print(f" FAILED PID {p.pid:>6} ({p.category})")
|
|
print()
|
|
print(f"Result: {killed} killed, {len(failed)} failed")
|
|
return 0 if not failed else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|