"""Local Linux transport: plain files plus a detached `bash run.sh` in its own session (setsid).""" import os import signal import subprocess from pathlib import Path RUNNER = """#!/usr/bin/env bash cd {cwd} || {{ echo "exit=97 at $(date -Is) (cd failed)" > {d}/exit.txt; exit 97; }} echo $$ > {d}/pid.txt date -Is > {d}/started.txt ( {command} ) < {d}/prompt.md > {d}/{stdout} 2> {d}/stderr.txt echo "exit=$? at $(date -Is)" > {d}/exit.txt """ class LocalTransport: os = "linux" def __init__(self, cfg, terminal): self.cfg, self.terminal = cfg, terminal def default_run_root(self): return str(self.cfg.state_dir / "runs") def join(self, *parts): return os.path.join(*parts) def quote(self, s): return "'" + str(s).replace("'", "'\\''") + "'" def mkdir(self, path, clean=False): p = Path(path) p.mkdir(parents=True, exist_ok=True) if clean: for f in p.iterdir(): if f.is_file(): f.unlink() def write_text(self, path, text): Path(path).write_text(text, encoding="utf-8") def read_text(self, path): p = Path(path) return p.read_text(encoding="utf-8", errors="replace") if p.exists() else None def grep_first(self, path, needle): text = self.read_text(path) or "" return next((line for line in text.splitlines() if needle in line), None) def tail(self, path, n): text = self.read_text(path) or "" return "\n".join(text.splitlines()[-n:]) def runner(self, run_dir, cwd, command, stdout_name): return "run.sh", RUNNER.format(cwd=self.quote(cwd), d=self.quote(run_dir), command=command, stdout=stdout_name) def launch(self, task, script_path, run_dir): # `setsid -f` double-forks: the runner leads its own session (pgid == pid, see kill) and is reparented # to init, so a long-running `tick --loop` never accumulates zombies. # The executor must not hold Plane credentials (design ยง8): drop every configured secret from its env. subprocess.run(["setsid", "-f", "bash", script_path], cwd=run_dir, check=True, close_fds=True, env=self.cfg.scrubbed_env(), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def kill(self, task, run_dir): pid = (self.read_text(os.path.join(run_dir, "pid.txt")) or "").strip() if pid.isdigit(): try: os.killpg(int(pid), signal.SIGTERM) # runner is a session leader: pgid == pid except ProcessLookupError: pass def forget(self, task): pass def process_count(self, name): out = subprocess.run(["pgrep", "-c", "-x", name], capture_output=True, text=True) return int(out.stdout.strip() or 0)