diff --git a/components/l4d2-host-lib/src/l4d2host/instances.py b/components/l4d2-host-lib/src/l4d2host/instances.py new file mode 100644 index 0000000..e3f1462 --- /dev/null +++ b/components/l4d2-host-lib/src/l4d2host/instances.py @@ -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) diff --git a/components/l4d2-host-lib/src/l4d2host/systemd_user.py b/components/l4d2-host-lib/src/l4d2host/systemd_user.py new file mode 100644 index 0000000..5eeec2d --- /dev/null +++ b/components/l4d2-host-lib/src/l4d2host/systemd_user.py @@ -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, + ) diff --git a/components/l4d2-host-lib/src/l4d2host/templates/__init__.py b/components/l4d2-host-lib/src/l4d2host/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/l4d2-host-lib/src/l4d2host/templates/l4d2@.service b/components/l4d2-host-lib/src/l4d2host/templates/l4d2@.service new file mode 100644 index 0000000..2ea66cf --- /dev/null +++ b/components/l4d2-host-lib/src/l4d2host/templates/l4d2@.service @@ -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 diff --git a/components/l4d2-host-lib/tests/test_initialize.py b/components/l4d2-host-lib/tests/test_initialize.py new file mode 100644 index 0000000..144edae --- /dev/null +++ b/components/l4d2-host-lib/tests/test_initialize.py @@ -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() == ""