Publish LumaOps source
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# UNSAFE: stop the normal container first and use this override only long enough
|
||||
# to identify missing device permissions. Remove it after selecting explicit nodes.
|
||||
services:
|
||||
lumaops:
|
||||
privileged: true
|
||||
read_only: false
|
||||
security_opt: !reset []
|
||||
cap_drop: !reset []
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
lumaops:
|
||||
network_mode: host
|
||||
ports: !reset []
|
||||
environment:
|
||||
# Host networking heeft geen port mapping: luister rechtstreeks op de
|
||||
# gekozen publieke webpoort.
|
||||
APP_PORT: "${WEB_PORT:-1223}"
|
||||
OPENRGB_HOST: "127.0.0.1"
|
||||
ENABLE_NETWORK_DISCOVERY: "true"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PUID="${PUID:-99}"
|
||||
PGID="${PGID:-100}"
|
||||
|
||||
case "$PUID:$PGID" in
|
||||
*[!0-9:]*|:|*:) echo "PUID en PGID moeten numeriek zijn" >&2; exit 64 ;;
|
||||
esac
|
||||
|
||||
groupmod --gid "$PGID" lumaops 2>/dev/null || true
|
||||
usermod --uid "$PUID" --gid "$PGID" lumaops 2>/dev/null || true
|
||||
|
||||
mkdir -p /config/openrgb /config/lumaops /data /logs /run/lumaops
|
||||
chown -R "$PUID:$PGID" /config/lumaops /data /logs /run/lumaops
|
||||
chown -R "$PUID:$PGID" /config/openrgb
|
||||
gosu "$PUID:$PGID" mkdir -p /data/backups
|
||||
|
||||
exec gosu "$PUID:$PGID" /usr/bin/tini -- /opt/venv/bin/python /opt/lumaops/supervisor.py
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Container health probe for web, database, OpenRGB process, and SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = int(os.environ.get("APP_PORT", "8080"))
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health/live", timeout=2) as response:
|
||||
if response.status != 200:
|
||||
return fail("web liveness failed")
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health/ready", timeout=2) as response:
|
||||
if response.status != 200:
|
||||
return fail("backend readiness failed")
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health/container", timeout=3) as response:
|
||||
report = json.load(response)
|
||||
except (urllib.error.URLError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
|
||||
return fail(f"health API failed: {exc}")
|
||||
|
||||
pid_file = Path("/run/lumaops/openrgb.pid")
|
||||
if not pid_file.is_file():
|
||||
return fail("OpenRGB PID file missing")
|
||||
try:
|
||||
pid = int(pid_file.read_text(encoding="ascii"))
|
||||
process_status = Path(f"/proc/{pid}/status").read_text(encoding="ascii")
|
||||
process_name = next(
|
||||
line.split(":", 1)[1].strip()
|
||||
for line in process_status.splitlines()
|
||||
if line.startswith("Name:")
|
||||
)
|
||||
process_state = next(
|
||||
line.split(":", 1)[1].strip()
|
||||
for line in process_status.splitlines()
|
||||
if line.startswith("State:")
|
||||
)
|
||||
if process_name != "openrgb" or process_state.startswith("Z"):
|
||||
raise ValueError("stale OpenRGB PID")
|
||||
except (OSError, StopIteration, ValueError):
|
||||
return fail("OpenRGB process is not running")
|
||||
|
||||
try:
|
||||
with socket.create_connection(
|
||||
("127.0.0.1", int(os.environ.get("OPENRGB_PORT", "6742"))), timeout=2
|
||||
):
|
||||
pass
|
||||
except OSError as exc:
|
||||
return fail(f"OpenRGB loopback socket failed: {exc}")
|
||||
|
||||
connector = report.get("components", {}).get("openrgb-local", {})
|
||||
if os.environ.get("HEALTHCHECK_REQUIRE_OPENRGB", "true").lower() == "true":
|
||||
if not connector.get("connected"):
|
||||
return fail("backend SDK connection is degraded")
|
||||
print(json.dumps({"status": report.get("status"), "openrgb": "connected"}))
|
||||
return 0
|
||||
|
||||
|
||||
def fail(message: str) -> int:
|
||||
print(message, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -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())
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0"?>
|
||||
<Container version="2">
|
||||
<Name>LumaOps</Name>
|
||||
<Repository>lumaops:0.1.0</Repository>
|
||||
<Registry/>
|
||||
<Network>bridge</Network>
|
||||
<MyIP/>
|
||||
<Shell>sh</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support/>
|
||||
<Project/>
|
||||
<Overview>Lokale OpenRGB 1.0rc3-hardware-engine met LumaOps webbeheer. Publiceer SDK-poort 6742 nooit.</Overview>
|
||||
<Category>Tools:System</Category>
|
||||
<WebUI>http://[IP]:[PORT:1223]/</WebUI>
|
||||
<TemplateURL/>
|
||||
<Icon>https://gitlab.com/CalcProgrammer1/OpenRGB/-/raw/release_candidate_1.0rc3/qt/org.openrgb.OpenRGB.png</Icon>
|
||||
<ExtraParams>--read-only --tmpfs=/tmp:size=128m,mode=1777 --tmpfs=/run:size=16m,mode=0755 --security-opt=no-new-privileges --cap-drop=ALL --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID</ExtraParams>
|
||||
<PostArgs/>
|
||||
<CPUset/>
|
||||
<DateInstalled>0</DateInstalled>
|
||||
<DonateText/>
|
||||
<DonateLink/>
|
||||
<Requires>Run scripts/unraid-hardware-setup.sh once on the host. Map /dev/bus/usb, the ASUS Aura /dev/hidraw0 node and Intel SMBus /dev/i2c-0. Host networking is optional for broadcast/multicast discovery.</Requires>
|
||||
<Config Name="Web UI" Target="8080" Default="1223" Mode="tcp" Description="Publieke LumaOps-webpoort; intern blijft de container op 8080 luisteren" Type="Port" Display="always" Required="true" Mask="false">1223</Config>
|
||||
<Config Name="OpenRGB appdata" Target="/config/openrgb" Default="/mnt/user/appdata/lumaops/openrgb" Mode="rw" Description="OpenRGB settings, profiles and plugins" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/lumaops/openrgb</Config>
|
||||
<Config Name="LumaOps config" Target="/config/lumaops" Default="/mnt/user/appdata/lumaops/config" Mode="rw" Description="Encryptiesleutel en appconfiguratie" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/lumaops/config</Config>
|
||||
<Config Name="LumaOps data" Target="/data" Default="/mnt/user/appdata/lumaops/data" Mode="rw" Description="SQLite-database en back-ups" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/lumaops/data</Config>
|
||||
<Config Name="Logs" Target="/logs" Default="/mnt/user/appdata/lumaops/logs" Mode="rw" Description="Geroteerde backend- en OpenRGB-logs" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/lumaops/logs</Config>
|
||||
<Config Name="USB bus" Target="/dev/bus/usb" Default="/dev/bus/usb" Mode="rw" Description="USB-apparaten; geen volledige /dev mount" Type="Device" Display="always" Required="false" Mask="false">/dev/bus/usb</Config>
|
||||
<Config Name="ASUS Aura HID" Target="/dev/hidraw0" Default="/dev/hidraw0" Mode="rw" Description="ASUS Aura LED Controller 0b05:18f3" Type="Device" Display="always" Required="true" Mask="false">/dev/hidraw0</Config>
|
||||
<Config Name="Intel SMBus" Target="/dev/i2c-0" Default="/dev/i2c-0" Mode="rw" Description="Intel I801 SMBus voor Corsair RGB-geheugen" Type="Device" Display="always" Required="true" Mask="false">/dev/i2c-0</Config>
|
||||
<Config Name="Timezone" Target="TZ" Default="Europe/Brussels" Mode="" Description="IANA-tijdzone" Type="Variable" Display="always" Required="true" Mask="false">Europe/Brussels</Config>
|
||||
<Config Name="Loglevel" Target="LOG_LEVEL" Default="INFO" Mode="" Description="DEBUG, INFO, WARNING of ERROR" Type="Variable" Display="advanced" Required="true" Mask="false">INFO</Config>
|
||||
<Config Name="Network discovery" Target="ENABLE_NETWORK_DISCOVERY" Default="true" Mode="" Description="Schakel LAN-discovery in" Type="Variable" Display="advanced" Required="true" Mask="false">true</Config>
|
||||
<Config Name="Authentication" Target="AUTH_ENABLED" Default="true" Mode="" Description="Verplicht voor de standaard LAN-bind" Type="Variable" Display="always" Required="true" Mask="false">true</Config>
|
||||
<Config Name="Admin token" Target="LUMAOPS_ADMIN_TOKEN" Default="" Mode="" Description="Unieke willekeurige beheertoken van minstens 32 tekens" Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
||||
<Config Name="Secure cookies" Target="SECURE_COOKIES" Default="false" Mode="" Description="Alleen inschakelen wanneer de webinterface via HTTPS wordt aangeboden" Type="Variable" Display="advanced" Required="true" Mask="false">false</Config>
|
||||
<Config Name="PUID" Target="PUID" Default="99" Mode="" Description="Unraid nobody UID" Type="Variable" Display="advanced" Required="true" Mask="false">99</Config>
|
||||
<Config Name="PGID" Target="PGID" Default="100" Mode="" Description="Unraid users GID" Type="Variable" Display="advanced" Required="true" Mask="false">100</Config>
|
||||
</Container>
|
||||
Reference in New Issue
Block a user