Publish LumaOps source
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal two-process supervisor with bounded restart and graceful shutdown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
STOP = threading.Event()
|
||||
RUN_DIR = Path("/run/lumaops")
|
||||
LOG_DIR = Path(os.environ.get("LOGS_DIR", "/logs"))
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("LOG_LEVEL", "INFO"),
|
||||
format="%(asctime)s %(levelname)s supervisor %(message)s",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
|
||||
|
||||
def signal_handler(signum: int, _frame: object) -> None:
|
||||
logging.info("received signal %s; starting graceful shutdown", signum)
|
||||
STOP.set()
|
||||
|
||||
|
||||
class ManagedProcess:
|
||||
def __init__(self, name: str, command: list[str], log_name: str) -> None:
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.log_name = log_name
|
||||
self.process: subprocess.Popen[str] | None = None
|
||||
self.restarts = 0
|
||||
self.next_start = 0.0
|
||||
|
||||
def start(self) -> None:
|
||||
if STOP.is_set():
|
||||
return
|
||||
logging.info("starting %s", self.name)
|
||||
self.process = subprocess.Popen( # noqa: S603 - fixed administrator-controlled argv
|
||||
self.command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
env=os.environ.copy(),
|
||||
)
|
||||
(RUN_DIR / f"{self.name}.pid").write_text(str(self.process.pid), encoding="ascii")
|
||||
threading.Thread(target=self._pump_output, daemon=True, name=f"{self.name}-logs").start()
|
||||
|
||||
def _pump_output(self) -> None:
|
||||
assert self.process is not None and self.process.stdout is not None
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
LOG_DIR / self.log_name,
|
||||
maxBytes=int(os.environ.get("LOG_MAX_BYTES", str(10 * 1024 * 1024))),
|
||||
backupCount=int(os.environ.get("LOG_BACKUP_COUNT", "5")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||
logger = logging.getLogger(f"process.{self.name}")
|
||||
logger.handlers = [handler]
|
||||
logger.propagate = False
|
||||
logger.setLevel(logging.INFO)
|
||||
for line in self.process.stdout:
|
||||
logger.info("%s", line.rstrip())
|
||||
handler.close()
|
||||
|
||||
def poll_and_restart(self) -> None:
|
||||
if self.process is None:
|
||||
if time.monotonic() >= self.next_start:
|
||||
self.start()
|
||||
return
|
||||
code = self.process.poll()
|
||||
if code is None:
|
||||
if time.monotonic() - self.next_start > 60:
|
||||
self.restarts = 0
|
||||
return
|
||||
logging.error("%s exited with code %s", self.name, code)
|
||||
(RUN_DIR / f"{self.name}.pid").unlink(missing_ok=True)
|
||||
self.process = None
|
||||
delay = min(30.0, 0.5 * (2 ** min(self.restarts, 6)))
|
||||
self.restarts += 1
|
||||
self.next_start = time.monotonic() + delay
|
||||
|
||||
def terminate(self, timeout: float = 12.0) -> None:
|
||||
process = self.process
|
||||
if process is None or process.poll() is not None:
|
||||
return
|
||||
logging.info("stopping %s", self.name)
|
||||
process.send_signal(signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.warning("%s did not stop in %.1fs; sending SIGKILL", self.name, timeout)
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
(RUN_DIR / f"{self.name}.pid").unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
configure_logging()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
signal.signal(sig, signal_handler)
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
openrgb_config = Path(os.environ.get("OPENRGB_CONFIG_DIR", "/config/openrgb"))
|
||||
openrgb_config.mkdir(parents=True, exist_ok=True)
|
||||
port = os.environ.get("OPENRGB_PORT", "6742")
|
||||
openrgb = ManagedProcess(
|
||||
"openrgb",
|
||||
[
|
||||
"/usr/local/bin/openrgb",
|
||||
"--server",
|
||||
"--server-host",
|
||||
"127.0.0.1",
|
||||
"--server-port",
|
||||
port,
|
||||
"--config",
|
||||
str(openrgb_config),
|
||||
"--noautoconnect",
|
||||
],
|
||||
"openrgb.log",
|
||||
)
|
||||
backend = ManagedProcess(
|
||||
"backend",
|
||||
["/opt/venv/bin/python", "-m", "lumaops_backend.main"],
|
||||
"backend.log",
|
||||
)
|
||||
processes = [openrgb, backend]
|
||||
for process in processes:
|
||||
process.start()
|
||||
process.next_start = time.monotonic()
|
||||
try:
|
||||
while not STOP.wait(0.5):
|
||||
for process in processes:
|
||||
process.poll_and_restart()
|
||||
finally:
|
||||
backend.terminate()
|
||||
openrgb.terminate()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user