aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/platform.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/platform.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/platform.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/platform.py88
1 files changed, 88 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/platform.py b/packages/meshbay-node/src/meshbay_node/platform.py
index 351289f..b59418f 100644
--- a/packages/meshbay-node/src/meshbay_node/platform.py
+++ b/packages/meshbay-node/src/meshbay_node/platform.py
@@ -3,6 +3,7 @@
import asyncio
import os
import shutil
+import subprocess
import sys
from pathlib import Path
@@ -109,3 +110,90 @@ def ffmpeg_cmd() -> str:
def ffprobe_cmd() -> str:
return _ffprobe_path
+
+
+# ── Autostart (Windows) ──────────────────────────────────────────────────────
+#
+# The Windows stand-in for the Linux `systemctl --user` unit. Task Scheduler
+# would be nicer (retry semantics), but a logon-triggered task needs elevation
+# to create — and this must work for an ordinary user with no admin rights.
+# So: a `.vbs` launcher in the per-user Startup folder. wscript runs it hidden
+# (Run(..., 0, ...)) at every sign-in; no console window, no admin, no
+# third-party dependency.
+
+TASK_NAME = "MeshBay Node" # the name the Electron client shows
+
+
+def autostart_supported() -> bool:
+ return sys.platform == "win32"
+
+
+def _startup_vbs() -> Path:
+ base = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
+ return (Path(base) / "Microsoft" / "Windows" / "Start Menu" / "Programs"
+ / "Startup" / "MeshBay Node.vbs")
+
+
+def _node_exe() -> str | None:
+ """Best guess at the meshbay-node launcher: PATH first, then next to the
+ interpreter (a venv's Scripts/ dir, or a bundled runtime), then argv[0]."""
+ found = shutil.which("meshbay-node")
+ if found:
+ return found
+ for cand in (Path(sys.executable).parent / "meshbay-node.exe",
+ Path(sys.argv[0])):
+ if cand.name.lower().startswith("meshbay-node") and cand.exists():
+ return str(cand.resolve())
+ return None
+
+
+def autostart_status() -> dict:
+ """{'installed': bool, 'state': str}. 'state' is left empty — there is no
+ Task Scheduler to ask 'is it running'; the Node page probes the daemon."""
+ if not autostart_supported():
+ return {"installed": False, "state": ""}
+ return {"installed": _startup_vbs().exists(), "state": ""}
+
+
+def autostart_install(exe: str | None = None) -> None:
+ """Write the Startup-folder launcher. Raises RuntimeError on failure."""
+ if not autostart_supported():
+ raise RuntimeError("autostart is Windows-only")
+ exe = exe or _node_exe()
+ if not exe:
+ raise RuntimeError(
+ "cannot locate the meshbay-node launcher — pass its path, or run "
+ "this from where meshbay-node is on PATH")
+ vbs = _startup_vbs()
+ vbs.parent.mkdir(parents=True, exist_ok=True)
+ # Chr(34) is a literal " — wraps the path so a space in it doesn't split the
+ # command. 0 = hidden window, False = don't wait. (A Windows path cannot
+ # itself contain ", so no further escaping is needed.)
+ vbs.write_text(
+ f'CreateObject("WScript.Shell").Run Chr(34) & "{exe}" & Chr(34), 0, False\n',
+ encoding="utf-8", newline="\r\n")
+
+
+def autostart_remove() -> None:
+ """Delete the Startup-folder launcher if present."""
+ if autostart_supported():
+ _startup_vbs().unlink(missing_ok=True)
+
+
+def autostart_run() -> None:
+ """Start the daemon now, detached and windowless. Raises RuntimeError if
+ the launcher cannot be located."""
+ if not autostart_supported():
+ raise RuntimeError("autostart is Windows-only")
+ exe = _node_exe()
+ if not exe:
+ raise RuntimeError("cannot locate the meshbay-node launcher")
+ subprocess.Popen([exe], creationflags=0x00000008 | 0x08000000, # DETACHED | NO_WINDOW
+ close_fds=True)
+
+
+def autostart_end() -> None:
+ """Stop any running daemon (hard: there is no CTRL_CLOSE handler yet)."""
+ if autostart_supported():
+ subprocess.run(["taskkill", "/IM", "meshbay-node.exe", "/F"],
+ capture_output=True)