diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-04 03:58:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-04 03:58:16 +0200 |
| commit | 220e6e701806213a576ce80fa655cd9cf4a51880 (patch) | |
| tree | 04b1f059044f4e02d1e8cb6c4a6588982cf395c4 /packages/meshbay-node/src/meshbay_node/platform.py | |
| parent | 5098e6cb54173b27673f5761ce187d799ba36b30 (diff) | |
| download | meshbay-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.py | 88 |
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) |