From 753653b4df62723b82d32799825135822886eac7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 3 Sep 2026 16:20:23 +0200 Subject: refactor(node): platform abstraction for Windows portability (W1-W2-W5-W6-W7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform directories, signal handling, chmod guards, ffmpeg discovery, and platform-conditional CLI messages — all testable on Linux. See docs/WINDOWS-PORT.md §5 for the plan these implement. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-node/src/meshbay_node/daemon.py | 72 ++++++++++++++---------- 1 file changed, 41 insertions(+), 31 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py') diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index 7341423..47241cd 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -52,6 +52,7 @@ from meshbay_node.media_cache import MediaCache from meshbay_node.tmdb import TmdbClient from meshbay_node.musicbrainz import MusicBrainzClient from meshbay_node.keystore import create_keystore, load_keystore, load_or_create_keystore +from meshbay_node.platform import chmod_private, config_dir, data_dir, state_dir from meshbay_node.roster import Roster from meshbay_node.transport import ( Denylist, @@ -219,7 +220,7 @@ class NodeDaemon: self._config.data_dir.mkdir(parents=True, exist_ok=True) self._ui_token_file = self._config.data_dir / "ui-token" self._ui_token_file.write_text(ui_token) - self._ui_token_file.chmod(0o600) + chmod_private(self._ui_token_file) from meshbay_node.ui import create_ui_app ui_app = create_ui_app(self._state) ui_cfg = uvicorn.Config( @@ -677,8 +678,12 @@ class NodeDaemon: # 12. Wait for shutdown stop_event = asyncio.Event() loop = asyncio.get_event_loop() - for sig in (signal.SIGINT, signal.SIGTERM): - loop.add_signal_handler(sig, stop_event.set) + if sys.platform == "win32": + for sig in (signal.SIGINT, signal.SIGTERM): + signal.signal(sig, lambda *_: stop_event.set()) + else: + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, stop_event.set) # Milestone 14.8: re-read node.toml without dropping connections. try: loop.add_signal_handler( @@ -1734,7 +1739,7 @@ def main() -> None: "", ] cfg_path.write_text("\n".join(toml_lines) + "\n") - os.chmod(cfg_path, 0o600) + chmod_private(cfg_path) print(f"Config written to {cfg_path}") unlock_file = config_dir / "unlock.key" @@ -1742,7 +1747,7 @@ def main() -> None: import secrets key = secrets.token_urlsafe(32) unlock_file.write_text(key + "\n") - os.chmod(unlock_file, 0o600) + chmod_private(unlock_file) print(f"Unlock key created: {unlock_file}") cfg = load_config(cfg_path) @@ -1759,7 +1764,10 @@ def main() -> None: print() print("Next steps:") print(f" 1. Link this node key on {hub_url} → Settings → Link Node") - print(" 2. systemctl --user enable --now meshbay-node") + if sys.platform == "win32": + print(" 2. meshbay-node (start the daemon)") + else: + print(" 2. systemctl --user enable --now meshbay-node") print(" 3. meshbay-node group add --dir /path/to/files") print(" 4. meshbay-node gek init") print(" 5. meshbay-node operator pair") @@ -1768,12 +1776,12 @@ def main() -> None: if args.command == "reset": import shutil - config_dir = Path.home() / ".config" / "meshbay" - data_dir = Path.home() / ".local" / "share" / "meshbay" - state_dir = Path.home() / ".local" / "state" / "meshbay" + config_dir_ = config_dir() + data_dir_ = data_dir() + state_dir_ = state_dir() items = [] - for d in (config_dir, data_dir): + for d in (config_dir_, data_dir_): if d.exists(): for child in sorted(d.iterdir()): items.append(child) @@ -1800,10 +1808,10 @@ def main() -> None: import urllib.request import urllib.error - token_file = data_dir / "ui-token" + token_file = data_dir_ / "ui-token" if token_file.exists(): try: - cfg = Config(config_dir / "node.toml") + cfg = Config(config_dir_ / "node.toml") tok = token_file.read_text().strip() url = (f"http://127.0.0.1:{cfg.node.ui_port}" f"/api/unlink?t={tok}") @@ -1814,16 +1822,17 @@ def main() -> None: except Exception: print("Could not unlink from hub (daemon not reachable).") - _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"], - capture_output=True) + if sys.platform != "win32": + _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"], + capture_output=True) - for d in (config_dir, data_dir): + for d in (config_dir_, data_dir_): if d.exists(): shutil.rmtree(d) print(f"Removed {d}") - if state_dir.is_dir(): - shutil.rmtree(state_dir) - print(f"Removed {state_dir}") + if state_dir_.is_dir(): + shutil.rmtree(state_dir_) + print(f"Removed {state_dir_}") print("Node state erased. Run 'meshbay-node init' to start over.") return @@ -2056,16 +2065,9 @@ def main() -> None: return if args.command == "reload": - # Milestone 14.8. The daemon re-reads node.toml; groups that appeared or - # whose roots changed are picked up without dropping live connections. - # - # Delegated to systemd rather than hunting a PID with pgrep and signalling - # it directly: an unanchored (or merely unlucky) pattern match there has - # already SIGHUPed a developer's own running node by accident — see the - # comment this replaced, and test_cli_dispatch.py's stub_daemon fixture, - # which had to stub os.kill for exactly that reason. The unit already - # declares `ExecReload=/bin/kill -HUP $MAINPID`, so systemd sends the - # signal to the one process it actually started. + if sys.platform == "win32": + print("reload is not supported on Windows — restart the daemon instead.") + sys.exit(1) _systemctl_user( "reload", "meshbay-node", not_running_hint="Node is not running as a systemd unit — start it " @@ -2075,9 +2077,10 @@ def main() -> None: return if args.command == "restart-daemon": - # Same reasoning as reload: no PID hunting, no manual respawn — systemd - # already knows how to stop and start this unit, and does not need this - # process to guess where its log file is. + if sys.platform == "win32": + print("restart-daemon is not supported on Windows — stop and start " + "the daemon manually.") + sys.exit(1) _systemctl_user( "restart", "meshbay-node", not_running_hint="meshbay-node is not installed as a systemd unit — " @@ -2356,6 +2359,13 @@ def main() -> None: print("Error: hub.username not set in config. Run: meshbay-node init") sys.exit(1) + from meshbay_node.platform import check_media_tools + try: + check_media_tools(cfg.node.ffmpeg_path, cfg.node.ffprobe_path) + except RuntimeError as e: + print(f"Error: {e}") + sys.exit(1) + daemon = NodeDaemon(cfg, Path(args.config or DEFAULT_CONFIG_PATH)) asyncio.run(daemon.run()) -- cgit v1.2.3