aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_platform.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_platform.py')
-rw-r--r--packages/meshbay-node/tests/test_platform.py252
1 files changed, 164 insertions, 88 deletions
diff --git a/packages/meshbay-node/tests/test_platform.py b/packages/meshbay-node/tests/test_platform.py
index bebe8d9..8856962 100644
--- a/packages/meshbay-node/tests/test_platform.py
+++ b/packages/meshbay-node/tests/test_platform.py
@@ -8,6 +8,7 @@ suite happens to run on.
import asyncio
import os
import sys
+import time
from pathlib import Path
from unittest.mock import Mock
@@ -168,10 +169,10 @@ def test_autostart_install_refuses_off_windows(monkeypatch):
plat.autostart_install(exe="/usr/bin/meshbay-node")
-def test_autostart_run_launches_the_resolved_exe_windowless_and_records_its_pid(
- win_startup, monkeypatch, tmp_path):
+def test_autostart_run_launches_windowless_and_inherits_nothing(win_startup, monkeypatch):
+ """close_fds: the desktop app starts its node through here because a child
+ of Electron inherited Electron's sockets and held them after the app quit."""
monkeypatch.setattr(plat, "_node_exe", lambda: r"C:\x\meshbay-node.exe")
- monkeypatch.setenv("LOCALAPPDATA", str(tmp_path)) # state_dir() -> pidfile location
calls = {}
def fake_popen(argv, **kw):
@@ -181,13 +182,23 @@ def test_autostart_run_launches_the_resolved_exe_windowless_and_records_its_pid(
monkeypatch.setattr(plat.subprocess, "Popen", fake_popen)
plat.autostart_run()
assert calls["argv"] == [r"C:\x\meshbay-node.exe"]
- flags = calls["kw"]["creationflags"]
- assert flags & 0x08000000 # CREATE_NO_WINDOW
- assert flags & 0x00000200 # CREATE_NEW_PROCESS_GROUP
- assert not flags & 0x00000008 # not DETACHED_PROCESS -- that has no
- # console at all, so CTRL_BREAK_EVENT
- # would have nothing to signal
- assert plat._pid_file().read_text(encoding="utf-8") == "4242"
+ assert calls["kw"]["creationflags"] & 0x08000000 # CREATE_NO_WINDOW
+ assert not calls["kw"]["creationflags"] & 0x00000008 # a console of its own:
+ # CTRL_LOGOFF/SHUTDOWN reach install_console_close_handler
+ assert calls["kw"]["close_fds"] is True
+
+
+def test_the_launcher_is_this_very_executable_when_frozen(monkeypatch, tmp_path):
+ """PATH came first, and with two copies on it `autostart start` from one
+ install launched the other -- found on a machine with an MSIX build on PATH."""
+ stray = tmp_path / "stray"
+ stray.mkdir()
+ (stray / "meshbay-node.exe").write_text("", encoding="utf-8")
+ monkeypatch.setenv("PATH", str(stray))
+ monkeypatch.setattr(plat.sys, "frozen", True, raising=False)
+ frozen = r"C:\Programs\MeshBay\node-runtime\meshbay-node.exe"
+ monkeypatch.setattr(plat.sys, "executable", frozen)
+ assert plat._node_exe() == frozen
def test_autostart_run_refuses_off_windows(monkeypatch):
@@ -196,100 +207,165 @@ def test_autostart_run_refuses_off_windows(monkeypatch):
plat.autostart_run()
-# ── Graceful stop (CTRL_BREAK_EVENT + taskkill fallback) ────────────────────
+# ── Stopping ────────────────────────────────────────────────────────────────
#
-# autostart_end() references signal.CTRL_BREAK_EVENT, which genuinely does not
-# exist in the `signal` module off Windows -- monkeypatching sys.platform
-# cannot manufacture it, unlike the pure-Python behaviour tested above. Skip
-# rather than mock around it, matching test_configure_event_loop_selector_opt_in.
+# The CTRL_BREAK_EVENT path these tests used to pin was the bug: aimed at a
+# node with a console of its own, GenerateConsoleCtrlEvent reached every process
+# on the caller's console, so `autostart stop` and `restart-daemon` killed
+# themselves. The tests mocked os.kill, so they agreed with it by construction.
-@pytest.mark.skipif(sys.platform != "win32",
- reason="signal.CTRL_BREAK_EVENT exists only on win32")
-def test_autostart_end_stops_gracefully_when_ctrl_break_is_enough(monkeypatch, tmp_path):
+def test_autostart_end_force_stops_by_image_name(monkeypatch):
monkeypatch.setattr(sys, "platform", "win32")
- monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
- plat._pid_file().parent.mkdir(parents=True, exist_ok=True)
- plat._pid_file().write_text("4242", encoding="utf-8")
-
- kill_calls = []
- monkeypatch.setattr(plat.os, "kill", lambda pid, sig: kill_calls.append((pid, sig)))
- # Alive (our exe) on the pre-signal check, gone by the first poll after --
- # a plain constant can't tell those two calls apart.
- seen = {"n": 0}
-
- def fake_check(pid):
- seen["n"] += 1
- return seen["n"] == 1
-
- monkeypatch.setattr(plat, "_pid_is_meshbay_node", fake_check)
run_calls = []
monkeypatch.setattr(plat.subprocess, "run",
lambda argv, **kw: run_calls.append(argv))
-
plat.autostart_end()
-
- assert kill_calls == [(4242, plat.signal.CTRL_BREAK_EVENT)]
- assert run_calls == [] # no taskkill needed
- assert not plat._pid_file().exists()
+ [argv] = run_calls
+ assert argv[:5] == ["taskkill", "/F", "/T", "/IM", "meshbay-node.exe"]
-@pytest.mark.skipif(sys.platform != "win32",
- reason="signal.CTRL_BREAK_EVENT exists only on win32")
-def test_autostart_end_falls_back_to_taskkill_when_the_pid_never_exits(
- monkeypatch, tmp_path):
+def test_the_forced_stop_spares_the_command_that_runs_it(monkeypatch):
+ """The CLI is meshbay-node.exe too: `taskkill /IM meshbay-node.exe` killed
+ `autostart stop` and `restart-daemon` themselves -- found on a real install."""
monkeypatch.setattr(sys, "platform", "win32")
- monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
- plat._pid_file().parent.mkdir(parents=True, exist_ok=True)
- plat._pid_file().write_text("4242", encoding="utf-8")
-
- monkeypatch.setattr(plat.os, "kill", lambda pid, sig: None)
- monkeypatch.setattr(plat, "_pid_is_meshbay_node", lambda pid: True) # never exits
- monkeypatch.setattr(plat.time, "sleep", lambda s: None) # don't really wait
- clock = iter([0.0, 1.0, 6.0]) # deadline = 0.0 + 5.0; third read is past it
- monkeypatch.setattr(plat.time, "monotonic", lambda: next(clock))
run_calls = []
monkeypatch.setattr(plat.subprocess, "run",
lambda argv, **kw: run_calls.append(argv))
-
plat.autostart_end()
+ [argv] = run_calls
+ assert f"PID ne {os.getpid()}" in argv and f"PID ne {os.getppid()}" in argv
- assert run_calls == [["taskkill", "/IM", "meshbay-node.exe", "/F"]]
- assert not plat._pid_file().exists()
+@pytest.mark.skipif(sys.platform != "win32", reason="taskkill")
+def test_the_forced_stop_really_spares_its_caller(tmp_path):
+ """For real: a process with the node's image name runs the forced stop, and
+ must survive it while another process of that name does not. Under a name of
+ its own, or it would also kill the developer's running node."""
+ import shutil
+ import subprocess
+ import textwrap
+ import uuid
-def test_autostart_end_falls_back_to_taskkill_without_a_pidfile(monkeypatch, tmp_path):
- """No CTRL_BREAK_EVENT dependency here -- there is no pid to signal, so
- this one runs everywhere, same as the pre-existing behaviour it replaces."""
- monkeypatch.setattr(sys, "platform", "win32")
- monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
- run_calls = []
- monkeypatch.setattr(plat.subprocess, "run",
- lambda argv, **kw: run_calls.append(argv))
- plat.autostart_end()
- assert run_calls == [["taskkill", "/IM", "meshbay-node.exe", "/F"]]
+ image = f"mb-test-{uuid.uuid4().hex[:8]}.exe"
+ base = Path(getattr(sys, "_base_executable", sys.executable))
+ env = {**os.environ, "PYTHONHOME": sys.base_prefix}
+ def renamed_python(where: str) -> Path:
+ d = tmp_path / where
+ d.mkdir()
+ shutil.copy(base, d / image)
+ for dll in base.parent.glob("*.dll"):
+ shutil.copy(dll, d)
+ return d / image
-def test_autostart_end_ignores_a_stale_pid_reused_by_another_process(monkeypatch, tmp_path):
- """The recorded pid is alive but is not meshbay-node.exe -- Windows reused
- it after the daemon exited. Must not send CTRL_BREAK_EVENT to whatever
- that is; falls straight to taskkill (by image name, so harmless here)."""
- monkeypatch.setattr(sys, "platform", "win32")
- monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
- plat._pid_file().parent.mkdir(parents=True, exist_ok=True)
- plat._pid_file().write_text("4242", encoding="utf-8")
+ caller = renamed_python("caller")
+ victim = renamed_python("victim") # a "daemon" with a "transcode"
+ child_pid_file = tmp_path / "child.pid"
+ src = Path(plat.__file__).resolve().parents[1]
+ script = textwrap.dedent(f"""
+ import sys; sys.path.insert(0, {str(src)!r})
+ from meshbay_node import platform as p
+ p.NODE_IMAGE = {image!r}
+ p.autostart_end()
+ print("survived")
+ """)
+ other = subprocess.Popen(
+ [str(victim), "-c",
+ "import subprocess, sys; p = subprocess.Popen(['ping', '-n', '300', '127.0.0.1'],"
+ " stdout=subprocess.DEVNULL); open(sys.argv[1], 'w').write(str(p.pid)); p.wait()",
+ str(child_pid_file)], env=env)
+ child_pid = 0
+ try:
+ for _ in range(100):
+ if child_pid_file.exists() and child_pid_file.read_text(encoding="utf-8"):
+ break
+ time.sleep(0.1)
+ child_pid = int(child_pid_file.read_text(encoding="utf-8"))
+ r = subprocess.run([str(caller), "-c", script], capture_output=True, text=True,
+ timeout=60, env=env)
+ assert "survived" in r.stdout, (r.returncode, r.stdout, r.stderr)
+ assert other.wait(timeout=10) is not None, "the other process must be stopped"
+ assert not plat._pid_alive(child_pid), "and its children with it"
+ finally:
+ if other.poll() is None:
+ other.kill()
+ if child_pid:
+ subprocess.run(["taskkill", "/F", "/PID", str(child_pid)], capture_output=True)
- monkeypatch.setattr(plat, "_pid_is_meshbay_node", lambda pid: False)
- kill_calls = []
- monkeypatch.setattr(plat.os, "kill", lambda pid, sig: kill_calls.append((pid, sig)))
- run_calls = []
- monkeypatch.setattr(plat.subprocess, "run",
- lambda argv, **kw: run_calls.append(argv))
- plat.autostart_end()
+def test_nothing_signals_a_console_group_any_more():
+ """Read as code, not text: the docstrings say why CTRL_BREAK went."""
+ import ast
+ import inspect
+ tree = ast.parse(inspect.getsource(plat))
+ for node in ast.walk(tree):
+ assert not (isinstance(node, ast.Attribute) and node.attr == "CTRL_BREAK_EVENT")
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
+ assert not (node.func.attr == "kill" and getattr(node.func.value, "id", "") == "os")
+
+
+def _serve_node(tmp_path, pid):
+ """A control API that answers status and shutdown, as the daemon's does."""
+ import json
+ import threading
+ from http.server import BaseHTTPRequestHandler, HTTPServer
+
+ asked = []
+
+ class H(BaseHTTPRequestHandler):
+ def _send(self, body):
+ data = json.dumps(body).encode()
+ self.send_response(200)
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+
+ def do_GET(self): # noqa: N802
+ self._send({"version": "x", "status": "running", "pid": pid})
+
+ def do_POST(self): # noqa: N802
+ asked.append(self.path)
+ self._send({"stopping": True})
+
+ def log_message(self, *a):
+ pass
+
+ server = HTTPServer(("127.0.0.1", 0), H)
+ threading.Thread(target=server.serve_forever, daemon=True).start()
+ (tmp_path / "ui-token").write_text("tok", encoding="utf-8")
+ return server, asked
+
+
+def test_a_graceful_stop_asks_the_node_and_waits_for_its_process(monkeypatch, tmp_path):
+ server, asked = _serve_node(tmp_path, pid=4242)
+ alive = iter([True, True, False])
+ monkeypatch.setattr(plat, "_pid_alive", lambda pid: next(alive))
+ try:
+ assert plat.request_graceful_stop(tmp_path, server.server_address[1], timeout=10)
+ finally:
+ server.shutdown()
+ assert asked == ["/api/shutdown?t=tok"]
+
+
+def test_a_node_that_does_not_exit_is_reported_for_the_caller_to_force(monkeypatch, tmp_path):
+ server, _ = _serve_node(tmp_path, pid=4242)
+ monkeypatch.setattr(plat, "_pid_alive", lambda pid: True)
+ try:
+ assert not plat.request_graceful_stop(tmp_path, server.server_address[1], timeout=1)
+ finally:
+ server.shutdown()
+
+
+def test_nothing_to_stop_when_no_node_answers(tmp_path):
+ (tmp_path / "ui-token").write_text("tok", encoding="utf-8")
+ assert not plat.request_graceful_stop(tmp_path, 1, timeout=1)
+
- assert kill_calls == [] # never signalled the reused pid
- assert run_calls == [["taskkill", "/IM", "meshbay-node.exe", "/F"]]
- assert not plat._pid_file().exists()
+@pytest.mark.skipif(sys.platform != "win32", reason="tasklist")
+def test_pid_alive_sees_this_process_and_not_a_dead_one():
+ import os
+ assert plat._pid_alive(os.getpid())
+ assert not plat._pid_alive(9_999_991)
def test_autostart_end_is_a_noop_off_windows(monkeypatch):
@@ -413,7 +489,7 @@ def test_frozen_build_finds_default_env_beside_the_executable(monkeypatch, tmp_p
"""Where build-node-runtime.ps1 puts it, alongside ffmpeg."""
exe = tmp_path / "meshbay-node.exe"
exe.write_bytes(b"")
- (tmp_path / "default.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJtest\n")
+ (tmp_path / "default.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJtest\n", encoding="utf-8")
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "executable", str(exe))
assert plat.packaged_default_env() == tmp_path / "default.env"
@@ -434,7 +510,7 @@ def test_load_node_env_sets_names(monkeypatch, tmp_path):
"\n"
"MESHBAY_TMDB_DEFAULT_TOKEN=eyJloaded\n"
'QUOTED="value"\n'
- )
+ , encoding="utf-8")
monkeypatch.delenv("MESHBAY_TMDB_DEFAULT_TOKEN", raising=False)
monkeypatch.delenv("QUOTED", raising=False)
assert plat.load_node_env(tmp_path) == 2
@@ -445,7 +521,7 @@ def test_load_node_env_sets_names(monkeypatch, tmp_path):
def test_load_node_env_does_not_override_the_environment(monkeypatch, tmp_path):
"""systemd may have loaded the same file already, and an operator export
must win over a packaged default."""
- (tmp_path / "node.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJfromfile\n")
+ (tmp_path / "node.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJfromfile\n", encoding="utf-8")
monkeypatch.setenv("MESHBAY_TMDB_DEFAULT_TOKEN", "eyJfromenv")
assert plat.load_node_env(tmp_path) == 0
assert os.environ["MESHBAY_TMDB_DEFAULT_TOKEN"] == "eyJfromenv"
@@ -460,7 +536,7 @@ def test_load_node_env_reads_the_packaged_default_without_a_node_env(monkeypatch
"""A node onboarded by the desktop client has no node.env: nothing ran
`init` to copy one. The packaged token must reach it anyway."""
src = tmp_path / "default.env"
- src.write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJpackaged\n")
+ src.write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJpackaged\n", encoding="utf-8")
monkeypatch.setattr(plat, "packaged_default_env", lambda: src)
monkeypatch.delenv("MESHBAY_TMDB_DEFAULT_TOKEN", raising=False)
cfg = tmp_path / "config"
@@ -472,12 +548,12 @@ def test_load_node_env_reads_the_packaged_default_without_a_node_env(monkeypatch
def test_load_node_env_prefers_the_operator_node_env(monkeypatch, tmp_path):
src = tmp_path / "default.env"
- src.write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJpackaged\n")
+ src.write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJpackaged\n", encoding="utf-8")
monkeypatch.setattr(plat, "packaged_default_env", lambda: src)
monkeypatch.delenv("MESHBAY_TMDB_DEFAULT_TOKEN", raising=False)
cfg = tmp_path / "config"
cfg.mkdir()
- (cfg / "node.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJoperator\n")
+ (cfg / "node.env").write_text("MESHBAY_TMDB_DEFAULT_TOKEN=eyJoperator\n", encoding="utf-8")
plat.load_node_env(cfg)
assert os.environ["MESHBAY_TMDB_DEFAULT_TOKEN"] == "eyJoperator"