desloppify/engine/detectors/jscpd_adapter.py runs jscpd via:
subprocess.run(
[npx, "--yes", "jscpd", ...],
timeout=120, check=True,
)
When the 120s timeout fires, subprocess.run only kills the direct child (npx); the node jscpd grandchild gets reparented to systemd --user and keeps running at ~100% CPU forever. On my box I ended up with 4 orphaned jscpd procs running 8h+ each (~35 cpu-hours wasted) before noticing.
Fix
Spawn with start_new_session=True (or process_group=0 on 3.11+) and on TimeoutExpired send SIGKILL to the whole process group:
proc = subprocess.Popen(argv, ..., start_new_session=True)
try:
out, err = proc.communicate(timeout=120)
except subprocess.TimeoutExpired:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
proc.communicate(timeout=10)
raise
Repro
Point desloppify at any tree large enough that jscpd takes >120s. Watch pgrep -af jscpd after the scan returns — the jscpd node process is still there at 99% CPU, parented to systemd --user.
Env
- desloppify 0.9.15
- python 3.14
- jscpd via
npx --yes jscpd
- linux 6.19, systemd 260
desloppify/engine/detectors/jscpd_adapter.pyruns jscpd via:When the 120s timeout fires,
subprocess.runonly kills the direct child (npx); thenode jscpdgrandchild gets reparented tosystemd --userand keeps running at ~100% CPU forever. On my box I ended up with 4 orphaned jscpd procs running 8h+ each (~35 cpu-hours wasted) before noticing.Fix
Spawn with
start_new_session=True(orprocess_group=0on 3.11+) and onTimeoutExpiredsendSIGKILLto the whole process group:Repro
Point desloppify at any tree large enough that jscpd takes >120s. Watch
pgrep -af jscpdafter the scan returns — the jscpd node process is still there at 99% CPU, parented tosystemd --user.Env
npx --yes jscpd