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).
100 lines
2.7 KiB
Python
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
|