summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/platform.py
diff options
context:
space:
mode:
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)