feat(l4d2): implement initialize flow and systemd user template management

This commit is contained in:
mwiegand 2026-04-23 00:57:00 +02:00
parent 3c92721973
commit 60bb709916
No known key found for this signature in database
5 changed files with 111 additions and 0 deletions

View file

@ -0,0 +1,46 @@
from pathlib import Path
from typing import Callable
from l4d2host.spec import load_spec
from l4d2host.systemd_user import daemon_reload, ensure_template_unit
DEFAULT_ROOT = Path("/opt/l4d2")
def initialize_instance(
name: str,
spec_path: Path,
*,
root: Path = DEFAULT_ROOT,
on_stdout: Callable[[str], None] | None = None,
on_stderr: Callable[[str], None] | None = None,
passthrough: bool = False,
) -> None:
spec = load_spec(spec_path)
instance_dir = root / "instances" / name
runtime_dir = root / "runtime" / name
(runtime_dir / "upper").mkdir(parents=True, exist_ok=True)
(runtime_dir / "work").mkdir(parents=True, exist_ok=True)
(runtime_dir / "merged").mkdir(parents=True, exist_ok=True)
instance_dir.mkdir(parents=True, exist_ok=True)
lowerdirs = [str(root / "overlays" / overlay) for overlay in spec.overlays]
lowerdirs.append(str(root / "installation"))
instance_env = "\n".join(
[
f"L4D2_PORT={spec.port}",
f"L4D2_ARGS={' '.join(spec.arguments)}",
f"L4D2_LOWERDIRS={':'.join(lowerdirs)}",
]
) + "\n"
(instance_dir / "instance.env").write_text(instance_env)
server_cfg = "\n".join(spec.config) if spec.config else ""
(instance_dir / "server.cfg").write_text(server_cfg)
if root.resolve() == DEFAULT_ROOT:
ensure_template_unit()
daemon_reload(on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)

View file

@ -0,0 +1,29 @@
from importlib import resources
from pathlib import Path
from typing import Callable
from l4d2host.process import run_command
def ensure_template_unit(target_dir: Path | None = None) -> Path:
if target_dir is None:
target_dir = Path.home() / ".config/systemd/user"
target_dir.mkdir(parents=True, exist_ok=True)
target_file = target_dir / "l4d2@.service"
body = resources.files("l4d2host.templates").joinpath("l4d2@.service").read_text()
target_file.write_text(body)
return target_file
def daemon_reload(
*,
on_stdout: Callable[[str], None] | None = None,
on_stderr: Callable[[str], None] | None = None,
passthrough: bool = False,
) -> None:
run_command(
["systemctl", "--user", "daemon-reload"],
on_stdout=on_stdout,
on_stderr=on_stderr,
passthrough=passthrough,
)

View file

@ -0,0 +1,14 @@
[Unit]
Description=L4D2 dedicated server instance %i
After=network.target
[Service]
Type=simple
EnvironmentFile=/opt/l4d2/instances/%i/instance.env
WorkingDirectory=/opt/l4d2/runtime/%i/merged/left4dead2
ExecStart=/opt/l4d2/installation/srcds_run -game left4dead2 +hostport ${L4D2_PORT} ${L4D2_ARGS}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target

View file

@ -0,0 +1,22 @@
from pathlib import Path
from l4d2host.instances import initialize_instance
def test_initialize_writes_files(tmp_path: Path) -> None:
spec = tmp_path / "spec.yaml"
spec.write_text("port: 27015\noverlays: [a,b]\nconfig: ['sv_consistency 1']\n")
initialize_instance("alpha", spec, root=tmp_path)
assert (tmp_path / "instances/alpha/instance.env").exists()
assert (tmp_path / "instances/alpha/server.cfg").exists()
def test_empty_config_writes_empty_server_cfg(tmp_path: Path) -> None:
spec = tmp_path / "spec.yaml"
spec.write_text("port: 27015\n")
initialize_instance("alpha", spec, root=tmp_path)
assert (tmp_path / "instances/alpha/server.cfg").read_text() == ""