feat(host): add step logging to steam_install
This commit is contained in:
parent
005d2d8458
commit
1604859f41
6 changed files with 75 additions and 39 deletions
|
|
@ -8,12 +8,7 @@ from l4d2host.service_control import start_service, stop_service
|
||||||
from l4d2host.spec import load_spec
|
from l4d2host.spec import load_spec
|
||||||
|
|
||||||
|
|
||||||
def _emit_step(msg: str, on_stdout: Callable[[str], None] | None, passthrough: bool) -> None:
|
from l4d2host.logging import emit_step
|
||||||
formatted = f"Step: {msg}"
|
|
||||||
if on_stdout is not None:
|
|
||||||
on_stdout(formatted)
|
|
||||||
elif passthrough:
|
|
||||||
print(formatted, flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT
|
DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT
|
||||||
|
|
@ -32,7 +27,7 @@ def initialize_instance(
|
||||||
root = get_left4me_root() if root is None else Path(root)
|
root = get_left4me_root() if root is None else Path(root)
|
||||||
spec = load_spec(spec_path)
|
spec = load_spec(spec_path)
|
||||||
|
|
||||||
_emit_step("creating instance directories...", on_stdout, passthrough)
|
emit_step("creating instance directories...", on_stdout, passthrough)
|
||||||
instance_dir = root / "instances" / name
|
instance_dir = root / "instances" / name
|
||||||
runtime_dir = root / "runtime" / name
|
runtime_dir = root / "runtime" / name
|
||||||
(runtime_dir / "upper").mkdir(parents=True, exist_ok=True)
|
(runtime_dir / "upper").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -43,7 +38,7 @@ def initialize_instance(
|
||||||
lowerdirs = [str(overlay_path(overlay, root=root)) for overlay in spec.overlays]
|
lowerdirs = [str(overlay_path(overlay, root=root)) for overlay in spec.overlays]
|
||||||
lowerdirs.append(str(root / "installation"))
|
lowerdirs.append(str(root / "installation"))
|
||||||
|
|
||||||
_emit_step("writing instance.env...", on_stdout, passthrough)
|
emit_step("writing instance.env...", on_stdout, passthrough)
|
||||||
instance_env = "\n".join(
|
instance_env = "\n".join(
|
||||||
[
|
[
|
||||||
f"L4D2_PORT={spec.port}",
|
f"L4D2_PORT={spec.port}",
|
||||||
|
|
@ -53,10 +48,10 @@ def initialize_instance(
|
||||||
) + "\n"
|
) + "\n"
|
||||||
(instance_dir / "instance.env").write_text(instance_env)
|
(instance_dir / "instance.env").write_text(instance_env)
|
||||||
|
|
||||||
_emit_step("writing server.cfg...", on_stdout, passthrough)
|
emit_step("writing server.cfg...", on_stdout, passthrough)
|
||||||
server_cfg = "\n".join(spec.config) if spec.config else ""
|
server_cfg = "\n".join(spec.config) if spec.config else ""
|
||||||
(instance_dir / "server.cfg").write_text(server_cfg)
|
(instance_dir / "server.cfg").write_text(server_cfg)
|
||||||
_emit_step("initialization complete.", on_stdout, passthrough)
|
emit_step("initialization complete.", on_stdout, passthrough)
|
||||||
|
|
||||||
|
|
||||||
def _load_instance_env(path: Path) -> dict[str, str]:
|
def _load_instance_env(path: Path) -> dict[str, str]:
|
||||||
|
|
@ -84,7 +79,7 @@ def start_instance(
|
||||||
|
|
||||||
env = _load_instance_env(instance_dir / "instance.env")
|
env = _load_instance_env(instance_dir / "instance.env")
|
||||||
|
|
||||||
_emit_step("mounting runtime overlay...", on_stdout, passthrough)
|
emit_step("mounting runtime overlay...", on_stdout, passthrough)
|
||||||
run_command(
|
run_command(
|
||||||
[
|
[
|
||||||
"fuse-overlayfs",
|
"fuse-overlayfs",
|
||||||
|
|
@ -102,12 +97,12 @@ def start_instance(
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
|
|
||||||
_emit_step("copying server.cfg to runtime...", on_stdout, passthrough)
|
emit_step("copying server.cfg to runtime...", on_stdout, passthrough)
|
||||||
target_cfg = runtime_dir / "merged" / "left4dead2" / "cfg" / "server.cfg"
|
target_cfg = runtime_dir / "merged" / "left4dead2" / "cfg" / "server.cfg"
|
||||||
target_cfg.parent.mkdir(parents=True, exist_ok=True)
|
target_cfg.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(instance_dir / "server.cfg", target_cfg)
|
shutil.copy2(instance_dir / "server.cfg", target_cfg)
|
||||||
|
|
||||||
_emit_step("starting systemd service...", on_stdout, passthrough)
|
emit_step("starting systemd service...", on_stdout, passthrough)
|
||||||
start_service(
|
start_service(
|
||||||
name,
|
name,
|
||||||
on_stdout=on_stdout,
|
on_stdout=on_stdout,
|
||||||
|
|
@ -115,7 +110,7 @@ def start_instance(
|
||||||
passthrough=passthrough,
|
passthrough=passthrough,
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
_emit_step("start complete.", on_stdout, passthrough)
|
emit_step("start complete.", on_stdout, passthrough)
|
||||||
|
|
||||||
|
|
||||||
def stop_instance(
|
def stop_instance(
|
||||||
|
|
@ -128,7 +123,7 @@ def stop_instance(
|
||||||
should_cancel: Callable[[], bool] | None = None,
|
should_cancel: Callable[[], bool] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
root = get_left4me_root() if root is None else Path(root)
|
root = get_left4me_root() if root is None else Path(root)
|
||||||
_emit_step("stopping systemd service...", on_stdout, passthrough)
|
emit_step("stopping systemd service...", on_stdout, passthrough)
|
||||||
stop_service(
|
stop_service(
|
||||||
name,
|
name,
|
||||||
on_stdout=on_stdout,
|
on_stdout=on_stdout,
|
||||||
|
|
@ -136,7 +131,7 @@ def stop_instance(
|
||||||
passthrough=passthrough,
|
passthrough=passthrough,
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
_emit_step("unmounting runtime overlay...", on_stdout, passthrough)
|
emit_step("unmounting runtime overlay...", on_stdout, passthrough)
|
||||||
run_command(
|
run_command(
|
||||||
["fusermount3", "-u", str(root / "runtime" / name / "merged")],
|
["fusermount3", "-u", str(root / "runtime" / name / "merged")],
|
||||||
on_stdout=on_stdout,
|
on_stdout=on_stdout,
|
||||||
|
|
@ -144,7 +139,7 @@ def stop_instance(
|
||||||
passthrough=passthrough,
|
passthrough=passthrough,
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
_emit_step("stop complete.", on_stdout, passthrough)
|
emit_step("stop complete.", on_stdout, passthrough)
|
||||||
|
|
||||||
|
|
||||||
def delete_instance(
|
def delete_instance(
|
||||||
|
|
@ -163,7 +158,7 @@ def delete_instance(
|
||||||
if not instance_dir.exists() and not runtime_dir.exists():
|
if not instance_dir.exists() and not runtime_dir.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
_emit_step("stopping systemd service (if running)...", on_stdout, passthrough)
|
emit_step("stopping systemd service (if running)...", on_stdout, passthrough)
|
||||||
stop_service(
|
stop_service(
|
||||||
name,
|
name,
|
||||||
on_stdout=on_stdout,
|
on_stdout=on_stdout,
|
||||||
|
|
@ -174,7 +169,7 @@ def delete_instance(
|
||||||
|
|
||||||
merged = runtime_dir / "merged"
|
merged = runtime_dir / "merged"
|
||||||
if merged.is_mount():
|
if merged.is_mount():
|
||||||
_emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
|
emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough)
|
||||||
run_command(
|
run_command(
|
||||||
["fusermount3", "-u", str(merged)],
|
["fusermount3", "-u", str(merged)],
|
||||||
on_stdout=on_stdout,
|
on_stdout=on_stdout,
|
||||||
|
|
@ -183,9 +178,9 @@ def delete_instance(
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
|
|
||||||
_emit_step("removing instance files...", on_stdout, passthrough)
|
emit_step("removing instance files...", on_stdout, passthrough)
|
||||||
if instance_dir.exists():
|
if instance_dir.exists():
|
||||||
shutil.rmtree(instance_dir)
|
shutil.rmtree(instance_dir)
|
||||||
if runtime_dir.exists():
|
if runtime_dir.exists():
|
||||||
shutil.rmtree(runtime_dir)
|
shutil.rmtree(runtime_dir)
|
||||||
_emit_step("delete complete.", on_stdout, passthrough)
|
emit_step("delete complete.", on_stdout, passthrough)
|
||||||
|
|
|
||||||
8
l4d2host/logging.py
Normal file
8
l4d2host/logging.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
def emit_step(msg: str, on_stdout: Callable[[str], None] | None, passthrough: bool) -> None:
|
||||||
|
formatted = f"Step: {msg}"
|
||||||
|
if on_stdout is not None:
|
||||||
|
on_stdout(formatted)
|
||||||
|
if passthrough:
|
||||||
|
print(formatted, flush=True)
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Callable
|
||||||
|
|
||||||
from l4d2host.paths import get_left4me_root
|
from l4d2host.paths import get_left4me_root
|
||||||
from l4d2host.process import run_command
|
from l4d2host.process import run_command
|
||||||
|
from l4d2host.logging import emit_step
|
||||||
|
|
||||||
|
|
||||||
class SteamInstaller:
|
class SteamInstaller:
|
||||||
|
|
@ -19,6 +20,7 @@ class SteamInstaller:
|
||||||
should_cancel: Callable[[], bool] | None = None,
|
should_cancel: Callable[[], bool] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
for platform in ("windows", "linux"):
|
for platform in ("windows", "linux"):
|
||||||
|
emit_step(f"downloading {platform} platform payload...", on_stdout, passthrough)
|
||||||
run_command(
|
run_command(
|
||||||
[
|
[
|
||||||
self.steamcmd,
|
self.steamcmd,
|
||||||
|
|
@ -38,3 +40,4 @@ class SteamInstaller:
|
||||||
passthrough=passthrough,
|
passthrough=passthrough,
|
||||||
should_cancel=should_cancel,
|
should_cancel=should_cancel,
|
||||||
)
|
)
|
||||||
|
emit_step("installation complete.", on_stdout, passthrough)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import sys
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from l4d2host.instances import initialize_instance, _emit_step
|
from l4d2host.instances import initialize_instance
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_writes_files(tmp_path: Path) -> None:
|
def test_initialize_writes_files(tmp_path: Path) -> None:
|
||||||
|
|
@ -35,23 +35,6 @@ def test_initialize_uses_configured_left4me_root(tmp_path: Path, monkeypatch) ->
|
||||||
assert f"L4D2_LOWERDIRS={tmp_path}/overlays/standard:{tmp_path}/installation" in env
|
assert f"L4D2_LOWERDIRS={tmp_path}/overlays/standard:{tmp_path}/installation" in env
|
||||||
|
|
||||||
|
|
||||||
def test_emit_step_uses_callback() -> None:
|
|
||||||
calls: list[str] = []
|
|
||||||
_emit_step("test step", on_stdout=calls.append, passthrough=False)
|
|
||||||
assert calls == ["Step: test step"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_emit_step_uses_passthrough_stdout(monkeypatch) -> None:
|
|
||||||
fake_out = StringIO()
|
|
||||||
monkeypatch.setattr(sys, "stdout", fake_out)
|
|
||||||
_emit_step("passthrough step", on_stdout=None, passthrough=True)
|
|
||||||
assert fake_out.getvalue() == "Step: passthrough step\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_emit_step_does_nothing_if_no_target() -> None:
|
|
||||||
_emit_step("silent step", on_stdout=None, passthrough=False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_instance_emits_steps(tmp_path: Path) -> None:
|
def test_initialize_instance_emits_steps(tmp_path: Path) -> None:
|
||||||
spec = tmp_path / "spec.yaml"
|
spec = tmp_path / "spec.yaml"
|
||||||
spec.write_text("port: 27015\noverlays: [standard]\n")
|
spec.write_text("port: 27015\noverlays: [standard]\n")
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,20 @@ def test_default_install_dir_uses_left4me_root(tmp_path, monkeypatch):
|
||||||
SteamInstaller().install_or_update()
|
SteamInstaller().install_or_update()
|
||||||
|
|
||||||
assert str(tmp_path / "installation") in calls[0]
|
assert str(tmp_path / "installation") in calls[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_steam_installer_emits_steps(tmp_path, monkeypatch) -> None:
|
||||||
|
from pathlib import Path
|
||||||
|
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
|
||||||
|
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: None)
|
||||||
|
|
||||||
|
steps: list[str] = []
|
||||||
|
|
||||||
|
from l4d2host.steam_install import SteamInstaller
|
||||||
|
SteamInstaller().install_or_update(on_stdout=steps.append)
|
||||||
|
|
||||||
|
assert steps == [
|
||||||
|
"Step: downloading windows platform payload...",
|
||||||
|
"Step: downloading linux platform payload...",
|
||||||
|
"Step: installation complete."
|
||||||
|
]
|
||||||
|
|
|
||||||
30
l4d2host/tests/test_logging.py
Normal file
30
l4d2host/tests/test_logging.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from l4d2host.logging import emit_step
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_step_uses_callback() -> None:
|
||||||
|
calls: list[str] = []
|
||||||
|
emit_step("test step", on_stdout=calls.append, passthrough=False)
|
||||||
|
assert calls == ["Step: test step"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_step_uses_passthrough_stdout(monkeypatch) -> None:
|
||||||
|
fake_out = StringIO()
|
||||||
|
monkeypatch.setattr(sys, "stdout", fake_out)
|
||||||
|
emit_step("passthrough step", on_stdout=None, passthrough=True)
|
||||||
|
assert fake_out.getvalue() == "Step: passthrough step\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_step_does_nothing_if_no_target() -> None:
|
||||||
|
emit_step("silent step", on_stdout=None, passthrough=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_emit_step_does_both(monkeypatch) -> None:
|
||||||
|
calls: list[str] = []
|
||||||
|
fake_out = StringIO()
|
||||||
|
monkeypatch.setattr(sys, "stdout", fake_out)
|
||||||
|
emit_step("both step", on_stdout=calls.append, passthrough=True)
|
||||||
|
assert calls == ["Step: both step"]
|
||||||
|
assert fake_out.getvalue() == "Step: both step\n"
|
||||||
Loading…
Reference in a new issue