feat(l4d2): add status and journald log read APIs

This commit is contained in:
mwiegand 2026-04-23 01:00:02 +02:00
parent 270f31f6e7
commit a6c4a6c50f
No known key found for this signature in database
4 changed files with 140 additions and 0 deletions

View file

@ -0,0 +1,34 @@
import subprocess
from typing import Iterator
def stream_instance_logs(name: str, *, lines: int = 200, follow: bool = True) -> Iterator[str]:
command = [
"journalctl",
"--user",
"-u",
f"l4d2@{name}.service",
"-n",
str(lines),
"-o",
"cat",
]
if follow:
command.append("-f")
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
try:
if proc.stdout is None:
return
for raw in iter(proc.stdout.readline, ""):
yield raw.rstrip("\n")
finally:
if proc.poll() is None:
proc.terminate()
proc.wait(timeout=2)

View file

@ -0,0 +1,56 @@
from dataclasses import dataclass
import subprocess
from l4d2host.process import run_command
@dataclass(slots=True)
class InstanceStatus:
state: str
raw_active_state: str
raw_sub_state: str
def map_active_state(active_state: str) -> str:
if active_state == "active":
return "running"
if active_state in {"inactive", "failed"}:
return "stopped"
return "unknown"
def get_instance_status(name: str) -> InstanceStatus:
try:
result = run_command(
[
"systemctl",
"--user",
"show",
f"l4d2@{name}.service",
"--property=ActiveState",
"--property=SubState",
"--no-pager",
]
)
except (subprocess.CalledProcessError, FileNotFoundError):
return InstanceStatus(
state="unknown",
raw_active_state="unknown",
raw_sub_state="unknown",
)
values: dict[str, str] = {}
for line in result.stdout.splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
values[key] = value
active_state = values.get("ActiveState", "unknown")
sub_state = values.get("SubState", "unknown")
return InstanceStatus(
state=map_active_state(active_state),
raw_active_state=active_state,
raw_sub_state=sub_state,
)

View file

@ -0,0 +1,42 @@
from types import SimpleNamespace
import pytest
from l4d2host.logs import stream_instance_logs
class DummyProcess:
def __init__(self, lines: list[str]) -> None:
self.stdout = SimpleNamespace(readline=self._readline)
self.stderr = SimpleNamespace(readline=lambda: "")
self._lines = iter(lines)
self.terminated = False
self.waited = False
def _readline(self) -> str:
return next(self._lines, "")
def poll(self):
return None if not self.waited else 0
def terminate(self) -> None:
self.terminated = True
def wait(self, timeout: int) -> None:
del timeout
self.waited = True
def test_stream_instance_logs_yields_lines(monkeypatch: pytest.MonkeyPatch) -> None:
proc = DummyProcess(["line1\n", "line2\n", ""])
def fake_popen(cmd, **kwargs):
del cmd
del kwargs
return proc
monkeypatch.setattr("l4d2host.logs.subprocess.Popen", fake_popen)
lines = list(stream_instance_logs("alpha", lines=10, follow=False))
assert lines == ["line1", "line2"]
assert proc.terminated is True

View file

@ -0,0 +1,8 @@
from l4d2host.status import map_active_state
def test_status_mapping() -> None:
assert map_active_state("active") == "running"
assert map_active_state("inactive") == "stopped"
assert map_active_state("failed") == "stopped"
assert map_active_state("weird") == "unknown"