aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-04 03:58:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-04 03:58:16 +0200
commit220e6e701806213a576ce80fa655cd9cf4a51880 (patch)
tree04b1f059044f4e02d1e8cb6c4a6588982cf395c4 /packages/meshbay-node/src/meshbay_node/daemon.py
parent5098e6cb54173b27673f5761ce187d799ba36b30 (diff)
downloadmeshbay-220e6e701806213a576ce80fa655cd9cf4a51880.tar.gz
feat: Windows daemon lifecycle (W3) — Startup-folder autostart
The Linux node runs under `systemctl --user`. Windows has no per-user equivalent that works without elevation: `schtasks /create /sc ONLOGON` (even `/rl LIMITED /it`) fails with "Access is denied" for a non-admin user, because a logon trigger touches machine-wide scheduler state. So autostart is a `.vbs` in the per-user Startup folder instead: CreateObject("WScript.Shell").Run Chr(34) & "<exe>" & Chr(34), 0, False wscript runs it at every sign-in, hidden (0) and non-blocking. No admin, no console window, no new dependency. Verified end to end: the launcher brings the daemon up with no window and it answers its loopback API. node/platform.py autostart_install/remove/status — write / delete / detect the launcher autostart_run/end — start now (DETACHED|NO_WINDOW) / taskkill _node_exe — PATH, then next to sys.executable, then argv[0] node/daemon.py new `autostart install|remove|start|stop|status` verb reload (win32) -> POST /api/reload on the loopback API restart-daemon (win32) -> autostart_end + autostart_run reset (win32) -> also removes the launcher client/main.js, preload.js node:autostart handler + winAutostart* helpers (kept in step with platform.py) node:service-status (win32) probes the daemon; stop/restart/start use taskkill + a detached, windowless spawn Tests: 8 autostart cases in test_platform.py (mocked sys.platform, APPDATA pointed at tmp); `autostart status` added to the CLI dispatch sweep. Full meshbay-node suite green on Windows (784 passed / 34 skipped). Still open: no CTRL_CLOSE_EVENT handler, so a bare taskkill / window close does not run _shutdown() (SetConsoleCtrlHandler, follow-up). Service mode (pywin32/NSSM) stays Phase 2. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py79
1 files changed, 67 insertions, 12 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 5aa6654..b5a249d 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -1643,7 +1643,8 @@ def main() -> None:
choices=["init", "reset", "status", "gek-init",
"gek", "operator", "member", "group", "file",
"video", "denylist", "stun", "reload",
- "restart-daemon", "calibrate-argon2"],
+ "restart-daemon", "autostart",
+ "calibrate-argon2"],
help="init: provision config + keystore | reset: erase all "
"node state | status: node state and keys "
"| operator pair: pair a "
@@ -1652,15 +1653,18 @@ def main() -> None:
"| video rematch: re-resolve TMDB matches for a group's "
"videos | denylist show|clear "
"| stun list|add|remove|reset "
- "| reload: re-read node.toml "
- "(systemctl --user reload) | restart-daemon: restart "
- "the systemd unit (systemctl --user restart) "
+ "| reload: re-read node.toml (hot; systemd or the "
+ "loopback API) | restart-daemon: restart the node "
+ "(systemd unit, or the Windows autostart launcher) "
+ "| autostart install|remove|start|stop|status "
+ "(Windows: run meshbay-node at each sign-in) "
"| calibrate-argon2: benchmark")
parser.add_argument("subcommand", nargs="?",
help="'pair' for operator; list|invite|revoke|unpin for "
"member; list|add|remove for group; init|rotate for gek; "
"list|rm for file; rematch for video; show|clear for "
- "denylist; list|add|remove|reset for stun")
+ "denylist; list|add|remove|reset for stun; "
+ "install|remove|start|stop|status for autostart")
parser.add_argument("target", nargs="?",
help="username for member invite|revoke|unpin; group name "
"for group add; file id for file rm; identifier for "
@@ -1771,7 +1775,8 @@ def main() -> None:
print("Next steps:")
print(f" 1. Link this node key on {hub_url} → Settings → Link Node")
if sys.platform == "win32":
- print(" 2. meshbay-node (start the daemon)")
+ print(" 2. meshbay-node autostart install (run at each sign-in)")
+ print(" — or just: meshbay-node (start it now, this session)")
else:
print(" 2. systemctl --user enable --now meshbay-node")
print(" 3. meshbay-node group add <name> --dir /path/to/files")
@@ -1828,7 +1833,10 @@ def main() -> None:
except Exception:
print("Could not unlink from hub (daemon not reachable).")
- if sys.platform != "win32":
+ if sys.platform == "win32":
+ from meshbay_node.platform import autostart_remove
+ autostart_remove()
+ else:
_sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"],
capture_output=True)
@@ -2072,8 +2080,12 @@ def main() -> None:
if args.command == "reload":
if sys.platform == "win32":
- print("reload is not supported on Windows — restart the daemon instead.")
- sys.exit(1)
+ # 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 "
@@ -2084,9 +2096,16 @@ def main() -> None:
if args.command == "restart-daemon":
if sys.platform == "win32":
- print("restart-daemon is not supported on Windows — stop and start "
- "the daemon manually.")
- sys.exit(1)
+ from meshbay_node.platform import autostart_end, autostart_run
+ autostart_end() # kill whatever is running now
+ try:
+ autostart_run()
+ except RuntimeError as e:
+ print(f"Could not restart: {e}. Stop the daemon (Ctrl+C) and "
+ "relaunch it from where meshbay-node is on PATH.")
+ sys.exit(1)
+ print("restarted the node")
+ return
_systemctl_user(
"restart", "meshbay-node",
not_running_hint="meshbay-node is not installed as a systemd unit — "
@@ -2096,6 +2115,42 @@ def main() -> None:
"watch logs: journalctl --user -u meshbay-node -f")
return
+ if args.command == "autostart":
+ 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"
+ if sub == "install":
+ _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":
+ try:
+ _plat.autostart_run()
+ except RuntimeError as e:
+ print(f"Could not start: {e}")
+ sys.exit(1)
+ print("started")
+ elif sub == "stop":
+ _plat.autostart_end()
+ print("stopped")
+ elif sub == "status":
+ st = _plat.autostart_status()
+ if st["installed"]:
+ print("autostart installed — runs meshbay-node at sign-in")
+ else:
+ print("autostart not installed — meshbay-node autostart install")
+ else:
+ print("autostart: install | remove | start | stop | status")
+ sys.exit(1)
+ return
+
if args.command == "denylist":
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
sub = args.subcommand or "show"