Publish LumaOps source

This commit is contained in:
LumaOps release export
2026-09-03 01:18:36 +02:00
commit 7f1c0e5f71
2363 changed files with 501543 additions and 0 deletions
+9
View File
@@ -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 []
+10
View File
@@ -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"
+19
View File
@@ -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
+72
View File
@@ -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())
+152
View File
@@ -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())
+40
View File
@@ -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>