"""Background daemon management — double-fork, PID file, stop.""" import os import signal import time from espmon.colors import ok, info, err, warn from espmon.config import PID_FILE, LOGS_DIR from espmon.logs import LogManager from espmon.monitor import SerialMonitor def daemonize(port: str, baud: int) -> int: """Start the serial monitor as a background daemon. Uses the Unix double-fork pattern to fully detach from the terminal. Returns 0 to the parent process, never returns in the daemon. """ log_path = LogManager.session_path(port) pid = os.fork() if pid > 0: # Parent: wait for first child os.waitpid(pid, 0) ok(f"Monitor started in background") ok(f"Logging to {log_path}") info(f"PID file: {PID_FILE}") info(f"Stop with: python -m espmon stop") return 0 # First child: new session os.setsid() pid2 = os.fork() if pid2 > 0: os._exit(0) # Second child (daemon process) devnull = os.open(os.devnull, os.O_RDWR) os.dup2(devnull, 0) os.dup2(devnull, 1) os.dup2(devnull, 2) os.close(devnull) # Write PID file LOGS_DIR.mkdir(parents=True, exist_ok=True) PID_FILE.write_text(str(os.getpid())) # Run monitor with signal handler for clean shutdown monitor = SerialMonitor(port, baud, log_path) signal.signal(signal.SIGTERM, lambda s, f: monitor.stop()) signal.signal(signal.SIGINT, lambda s, f: monitor.stop()) try: monitor.run() finally: PID_FILE.unlink(missing_ok=True) os._exit(0) def stop_daemon() -> int: """Stop a running background monitor daemon.""" if not PID_FILE.exists(): err("No background monitor running (no PID file)") return 1 try: pid = int(PID_FILE.read_text().strip()) except (ValueError, OSError): err(f"Invalid PID file: {PID_FILE}") PID_FILE.unlink(missing_ok=True) return 1 try: os.kill(pid, signal.SIGTERM) info(f"Sent SIGTERM to PID {pid}") # Wait for process to exit (max 5 seconds) for _ in range(50): try: os.kill(pid, 0) time.sleep(0.1) except OSError: ok("Background monitor stopped") PID_FILE.unlink(missing_ok=True) return 0 warn(f"Process {pid} did not exit, sending SIGKILL") os.kill(pid, signal.SIGKILL) PID_FILE.unlink(missing_ok=True) return 0 except ProcessLookupError: info(f"Process {pid} already gone") PID_FILE.unlink(missing_ok=True) return 0 except PermissionError: err(f"Permission denied to kill PID {pid}") return 1