From c96930f3ff6fd2a7f71573934ac66d12876ff2a5 Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 18:11:37 +0200 Subject: [PATCH] Kill entire process group when stopping a job Start CLI subprocesses in their own session so SIGTERM via os.killpg reaches child processes (rsync, etc.) not just the shell. Co-Authored-By: Claude Opus 4.6 --- tui/backend.py | 1 + tui/jobs.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tui/backend.py b/tui/backend.py index 7467909..2cf41ef 100644 --- a/tui/backend.py +++ b/tui/backend.py @@ -30,6 +30,7 @@ async def start_cli_process(*args: str) -> asyncio.subprocess.Process: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, + start_new_session=True, ) diff --git a/tui/jobs.py b/tui/jobs.py index 5280e5e..558a4a4 100644 --- a/tui/jobs.py +++ b/tui/jobs.py @@ -1,4 +1,6 @@ import asyncio +import os +import signal import uuid from dataclasses import dataclass, field from datetime import datetime @@ -77,23 +79,24 @@ class JobManager: app.post_message(JobFinished(job.id, rc)) return job.return_code if job.return_code is not None else 1 + @staticmethod + def _kill_process_group(proc: asyncio.subprocess.Process) -> None: + try: + os.killpg(proc.pid, signal.SIGTERM) + except (ProcessLookupError, PermissionError): + pass + def kill_job(self, job_id: str) -> bool: job = self._jobs.get(job_id) if not job or job._proc is None: return False - try: - job._proc.terminate() - return True - except ProcessLookupError: - return False + self._kill_process_group(job._proc) + return True def kill_running(self) -> None: for job in self._jobs.values(): if job._proc is not None: - try: - job._proc.terminate() - except ProcessLookupError: - pass + self._kill_process_group(job._proc) job_manager = JobManager()