mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 18:07:07 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""The enforce loop's handle on the control socket.
|
|
|
|
``start_control`` is the only thing the loop calls. It never raises and never
|
|
blocks the enforcer: if the socket cannot be served (not root, demo run, no
|
|
desktop user, bind failure) the loop gets an inert handle and carries on
|
|
exactly as before.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
import logging
|
|
import os
|
|
import pwd
|
|
import threading
|
|
from typing import TYPE_CHECKING
|
|
|
|
from steam_backlog_enforcer._ctl_context import CtlContext
|
|
from steam_backlog_enforcer._ctl_gap import record_exit
|
|
from steam_backlog_enforcer._ctl_server import CtlServer
|
|
from steam_backlog_enforcer._desktop_env import resolve_desktop_user
|
|
from steam_backlog_enforcer._playtime import playtime_tick
|
|
|
|
if TYPE_CHECKING:
|
|
from steam_backlog_enforcer._playtime_session import PlaytimeSession
|
|
from steam_backlog_enforcer.config import Config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DaemonControl:
|
|
"""Lock + restart flag shared with the control socket (inert if none)."""
|
|
|
|
def __init__(self, ctx: CtlContext, server: CtlServer | None) -> None:
|
|
"""Wrap the shared context and the (optional) running server.
|
|
|
|
Args:
|
|
ctx: Shared op context; owns the tick lock and restart event.
|
|
server: The running socket server, or ``None`` when inert.
|
|
"""
|
|
self.ctx = ctx
|
|
self._server = server
|
|
|
|
@property
|
|
def tick_lock(self) -> threading.Lock:
|
|
"""Held by the loop for each iteration so ops never interleave with one."""
|
|
return self.ctx.tick_lock
|
|
|
|
@property
|
|
def restart_requested(self) -> bool:
|
|
"""Whether the ``restart`` op has been accepted."""
|
|
return self.ctx.restart_event.is_set()
|
|
|
|
def flush_for_restart(
|
|
self, config: Config, session: PlaytimeSession, *, interval: float, demo: bool
|
|
) -> bool:
|
|
"""Bill the time up to now before the process exits for a restart.
|
|
|
|
Everything else the enforcer relies on is already on disk after each
|
|
tick (the budget state, the cutoff time, fired warnings, the store
|
|
window deadline); the only thing a restart can lose is the seconds
|
|
since the last tick, so one more accounting tick closes that gap. Call
|
|
with the tick lock held.
|
|
|
|
Returns:
|
|
Whether the loop should exit now.
|
|
"""
|
|
if not self.restart_requested:
|
|
return False
|
|
playtime_tick(config, interval=interval, session=session, demo=demo)
|
|
record_exit()
|
|
logger.warning("State flushed; exiting so systemd restarts the enforcer.")
|
|
return True
|
|
|
|
def close(self) -> None:
|
|
"""Stop serving and remove the socket file."""
|
|
if self._server is not None:
|
|
self._server.stop()
|
|
|
|
|
|
def _is_supervised() -> bool:
|
|
"""Whether systemd started this process (it sets ``INVOCATION_ID``)."""
|
|
return bool(os.environ.get("INVOCATION_ID"))
|
|
|
|
|
|
def start_control(config: Config, *, demo: bool) -> DaemonControl:
|
|
"""Start the control socket if this process is the real root daemon.
|
|
|
|
Args:
|
|
config: The daemon's configuration.
|
|
demo: Whether this is a ``--demo`` run (never serves: it could take the
|
|
socket from the real daemon).
|
|
|
|
Returns:
|
|
A handle; inert when the socket is not being served.
|
|
"""
|
|
ctx = CtlContext(
|
|
config=config,
|
|
tick_lock=threading.Lock(),
|
|
restart_event=threading.Event(),
|
|
started_at=datetime.now(UTC),
|
|
supervised=_is_supervised(),
|
|
)
|
|
if demo or os.geteuid() != 0:
|
|
return DaemonControl(ctx, None)
|
|
user = resolve_desktop_user()
|
|
try:
|
|
entry = pwd.getpwnam(user) if user else None
|
|
except KeyError:
|
|
entry = None
|
|
if entry is None or entry.pw_uid == 0:
|
|
# Fail closed: without a known unprivileged owner there is nobody the
|
|
# socket could safely be for.
|
|
logger.error("Control socket disabled: no desktop user (%r)", user)
|
|
return DaemonControl(ctx, None)
|
|
server = CtlServer(ctx, desktop_uid=entry.pw_uid, desktop_gid=entry.pw_gid)
|
|
return DaemonControl(ctx, server if server.start() else None)
|