#!/usr/bin/env python3 """Run one command in its own session and process group, with a timeout. usage: run_group.py TIMEOUT_S LOGFILE CWD -- CMD [ARGS...] Child stdout and stderr go to LOGFILE. On timeout the whole process group gets SIGTERM, then SIGKILL after 5 s. After the child exits, anything still left in its group is killed too, so nothing from one step outlives it. Exit status: the child's, or 124 on timeout. """ import os import signal import subprocess import sys def group_kill(pgid, sig): try: os.killpg(pgid, sig) except ProcessLookupError: pass def main(): timeout = float(sys.argv[1]) log = sys.argv[2] cwd = sys.argv[3] cmd = sys.argv[sys.argv.index('--') + 1:] with open(log, 'w') as out: p = subprocess.Popen( cmd, cwd=cwd, stdout=out, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True, ) try: rc = p.wait(timeout=timeout) except subprocess.TimeoutExpired: group_kill(p.pid, signal.SIGTERM) try: p.wait(timeout=5) except subprocess.TimeoutExpired: pass group_kill(p.pid, signal.SIGKILL) p.wait() out.write(f"\nTIMEOUT after {timeout:.0f}s: process group {p.pid} killed\n") rc = 124 group_kill(p.pid, signal.SIGKILL) sys.exit(rc) if __name__ == '__main__': main()