espilon-source/tools/espmon/daemon.py
Eun0us 12b851581a tools: replace flasher/provisioning with unified deploy system + espmon
Add deploy.py: multi-device build/flash/provision with JSON config.
Add espmon/: real-time ESP32 fleet monitoring daemon.
Remove tools/flasher/ and tools/provisioning/ (superseded).
2026-02-28 20:15:57 +01:00

100 lines
2.7 KiB
Python

"""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