"""Starting, stopping and reloading the node: systemd here, the Startup launcher or the service task on Windows.""" import sys from meshbay_node.cli.api import _daemon_api, _systemctl_user from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config def reload(args) -> None: if sys.platform == "win32": # No systemd, no SIGHUP: the daemon exposes a hot reload on its # own loopback API (the same one ops.reload_config drives). cfg = load_config(args.config or DEFAULT_CONFIG_PATH) _daemon_api(cfg, "/api/reload", method="POST") print("sent reload to the running node") return _systemctl_user( "reload", "meshbay-node", not_running_hint="Node is not running as a systemd unit — start it " "with: systemctl --user start meshbay-node", success="sent reload to meshbay-node", watch="watch the result: journalctl --user -u meshbay-node -f") return def _await_daemon(cfg, since: float, timeout: float = 30.0) -> dict | None: """The status of a daemon started at or after `since`, or None. The token file is rewritten at every start, so one older than `since` belongs to the instance that was just stopped — answering from it would report the old daemon as the new one. """ import json import time import urllib.request token_file = cfg.data_dir / "ui-token" deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: if token_file.stat().st_mtime >= since: token = token_file.read_text(encoding="utf-8").strip() url = f"http://127.0.0.1:{cfg.node.ui_port}/api/status?t={token}" with urllib.request.urlopen(url, timeout=2) as r: return json.loads(r.read()) except (OSError, ValueError): pass time.sleep(0.5) return None def _running_status(cfg) -> dict | None: """The status of whatever node answers right now, or None.""" return _await_daemon(cfg, since=0, timeout=0.1) def _stop_node(cfg) -> str: """Stop the node, whichever session it runs in: through its own control API first, so it shuts down properly, then by force. Returns how it went.""" import time from meshbay_node.platform import ( autostart_end, request_graceful_stop, service_end, service_state, service_status, ) if request_graceful_stop(cfg.data_dir, cfg.node.ui_port): how = "stopped" elif _running_status(cfg) is None: how = "not running" else: how = "stopped (forced)" if service_status()["installed"]: # Also leaves the task "Ready": a /run while it still reads "Running" # is dropped (MultipleInstances IgnoreNew), leaving no node at all. service_end() deadline = time.monotonic() + 15 while service_state().lower() == "running" and time.monotonic() < deadline: time.sleep(0.25) autostart_end() # a node in this session that would not stop return how def _start_and_confirm(args, action: str) -> None: """Start the node the way this machine is set up to, and report what actually answered -- never "started" about a node nobody checked.""" import time from meshbay_node import __version__ from meshbay_node.platform import autostart_run, log_file, service_run, service_status cfg = load_config(args.config or DEFAULT_CONFIG_PATH) if action == "restart": _stop_node(cfg) else: already = _running_status(cfg) if already is not None: print(f"already running — node {already.get('version', '?')}, " f"{already.get('status', '?')}") return since = time.time() - 1 service = service_status()["installed"] try: service_run() if service else autostart_run() except RuntimeError as e: print(f"Could not start the node: {e}") sys.exit(1) status = _await_daemon(cfg, since) if status is None: print("no node answered within 30s of being started" + (" by the service task." if service else ".")) print(f"See the node's log: {log_file()}") sys.exit(1) running = status.get("version", "?") print(f"{'restarted' if action == 'restart' else 'started'} — node {running}, " f"{status.get('status', '?')}") if running != __version__: print(f"warning: that is node {running} but this is {__version__} — " "its files were not replaced, or another copy was started.") def restart_daemon(args) -> None: if sys.platform == "win32": _start_and_confirm(args, "restart") return _systemctl_user( "restart", "meshbay-node", not_running_hint="meshbay-node is not installed as a systemd unit — " "see packaging/systemd/", success="meshbay-node restarted via systemd", watch="check status: systemctl --user status meshbay-node\n" "watch logs: journalctl --user -u meshbay-node -f") return def autostart(args) -> None: from meshbay_node import platform as _plat if sys.platform != "win32": print("autostart is Windows-only — elsewhere use " "'systemctl --user enable --now meshbay-node'.") sys.exit(1) sub = args.subcommand or "status" service_mode = _plat.service_status()["installed"] if sub == "install": if service_mode: # Both would start the node: at boot, then again at sign-in. print("The node already runs as a background service. Remove it " "first (meshbay-node service remove, elevated) to start it at " "sign-in instead.") sys.exit(1) _plat.autostart_install() print("Installed the Startup launcher — meshbay-node starts at " "each sign-in (no window, no admin).") print("Start it now with: meshbay-node autostart start") elif sub == "remove": _plat.autostart_remove() print("Removed the Startup launcher.") elif sub == "start": _start_and_confirm(args, "start") elif sub == "stop": print(_stop_node(load_config(args.config or DEFAULT_CONFIG_PATH))) elif sub == "status": st = _plat.autostart_status() if st["installed"]: print("autostart installed — runs meshbay-node at sign-in") elif service_mode: print("autostart not used — the node runs as a background service") else: print("autostart not installed — meshbay-node autostart install") else: print("autostart: install | remove | start | stop | status") sys.exit(1) return def service(args) -> None: from meshbay_node import platform as _plat if sys.platform != "win32": print("service mode is Windows-only — elsewhere use " "'systemctl --user enable --now meshbay-node'.") sys.exit(1) sub = args.subcommand or "status" if sub == "install": # A node already running in this session holds the control API's port: # the service's own would exit at once, leaving the old one in charge. _stop_node(load_config(args.config or DEFAULT_CONFIG_PATH)) try: _plat.service_install() except RuntimeError as e: print(f"Could not install: {e}") if "denied" in str(e).lower(): print("Run this from an elevated (Administrator) prompt.") sys.exit(1) print(f"Registered the {_plat.TASK_NAME!r} scheduled task — it starts " "meshbay-node at boot, as this user, whether or not you have " "signed in yet (no password stored).") print("Start it now with: meshbay-node service start") elif sub == "remove": # Deleting a task does not end its running instance: stop the node # first, or it runs on in session 0 with nothing left to stop it. _stop_node(load_config(args.config or DEFAULT_CONFIG_PATH)) _plat.service_remove() print(f"Removed the {_plat.TASK_NAME!r} scheduled task.") elif sub == "start": if not _plat.service_status()["installed"]: print("service not installed — meshbay-node service install " "(needs an elevated prompt)") sys.exit(1) _start_and_confirm(args, "start") elif sub == "stop": print(_stop_node(load_config(args.config or DEFAULT_CONFIG_PATH))) elif sub == "status": st = _plat.service_status() if st["installed"]: print(f"service installed — {st['state'] or 'unknown state'}") else: print("service not installed — meshbay-node service install " "(needs an elevated prompt)") else: print("service: install | remove | start | stop | status") sys.exit(1) return