"""The server's background compiles (the Logbook's statistics, the whole-program check, the emulator run) give way to the lander: its gates (a full readable check of every file) are what land work on main, and a second compile of main beside them roughly quadruples their time. Two parts: lander_busy(project) while [logbook] lander_lock names a file that exists and was written in the last STALE_MINUTES, the lander is running its gates: background jobs do not start. The lander creates the file before its gates and removes it after the push; an older file is a lander that died, and is ignored. run_low(...) subprocess.run at BELOW_NORMAL priority (children such as cl.exe inherit it) that, on a timeout, ends the whole process tree instead of leaving the compiler running orphaned. Configured per project in the [logbook] table: lander_lock = 'R:/Workspace/Data/lander-busy.lock' unset or "": never wait""" import os import subprocess import time from pathlib import Path STALE_MINUTES = 60 BELOW_NORMAL = getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0) IDLE = getattr(subprocess, "IDLE_PRIORITY_CLASS", 0) NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) def lock_path(project): path = ((getattr(project, "logbook", None) or {}).get("lander_lock") or "").strip() return Path(path) if path else None def lander_busy(project, now=None): """Why background work should wait (the lander's lock is fresh), or None.""" path = lock_path(project) if path is None: return None try: written = path.stat().st_mtime except OSError: return None age = (time.time() if now is None else now) - written if age >= STALE_MINUTES * 60: return None return f"the lander is running its gates ({path.name}, {max(0, int(age // 60))} min old)" def kill_tree(pid): if os.name == "nt": subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True, creationflags=NO_WINDOW) else: try: os.kill(pid, 9) except OSError: pass def run_low(argv, timeout, priority=BELOW_NORMAL, **kwargs): """subprocess.run(argv, capture_output=True, text=True, ...) at a lower priority class; on a timeout the whole tree (cmd.exe, cl.exe) is ended before TimeoutExpired is raised.""" flags = kwargs.pop("creationflags", 0) | (priority | NO_WINDOW if os.name == "nt" else 0) with subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, creationflags=flags, **kwargs) as proc: try: stdout, stderr = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired: kill_tree(proc.pid) proc.kill() proc.communicate() raise subprocess.TimeoutExpired(argv, timeout) from None return subprocess.CompletedProcess(argv, proc.returncode, stdout, stderr)