Ported from the private repo via bin/publish.sh, plus a manual catch-up on files that had drifted out of sync before today: - launch_nexus.ps1: health-check based restart decisions instead of a bare port-listen check (a wedged leftover process squatting a port used to look "already running" and block the real service from starting), a script-path quoting fix for Start-Process, hidden console via a wscript.exe wrapper (bin/launch_nexus_hidden.vbs), and a taskbar/window icon for the native app window. - Sidebar: slim icon+text nav rows instead of bulky bordered buttons, tighter spacing throughout. - Settings: full-width layout, a Vite dev-server Start/Stop toggle (synapse/frontend_manager.py + /frontend/* endpoints), and the Linux-only Icon Branding section now gated on the new /status `platform` field instead of always rendering. - Chatbot: a Think toggle next to the model picker, so extended thinking can be flipped without leaving the chat page. - management/ncp.py: faster start/stop polling (0.25s steps instead of 1s), Vite no longer blocks `ncp start` on Linux and is skipped outright on Windows. Note: the private repo also has a Mail (IMAP/SMTP) feature; it's intentionally not included here, so the Mail-only pieces of main.py, App.jsx, and requirements-windows.txt were left out of this port.
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""Start/stop the Vite dev server from the web UI.
|
|
|
|
Dev-only convenience: production serves the built interface/web/dist from this
|
|
same backend, but hot-reload editing of the frontend needs Vite running
|
|
separately. Shares management/ncp.py's PID file (runtime/pids/frontend.pid)
|
|
and process patterns so `ncp status` / `ncp stop --frontend` see the same
|
|
process regardless of which side started it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import urllib.request
|
|
|
|
import psutil
|
|
|
|
from .nexus_config import PROJECT_ROOT, RUNTIME_DIR
|
|
|
|
FRONTEND_DIR = PROJECT_ROOT / "interface" / "web"
|
|
PID_FILE = RUNTIME_DIR / "pids" / "frontend.pid"
|
|
LOG_FILE = RUNTIME_DIR / "frontend.log"
|
|
PORT = 5173
|
|
|
|
|
|
def _npm() -> str | None:
|
|
return shutil.which("npm.cmd" if os.name == "nt" else "npm")
|
|
|
|
|
|
def _read_pid() -> int | None:
|
|
try:
|
|
return int(PID_FILE.read_text().strip())
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def _is_ours(pid: int) -> bool:
|
|
# On Windows, npm.cmd can't run as a CreateProcess image directly, so the
|
|
# OS wraps it as `cmd.exe /c ...npm.cmd run dev ...` - the ".cmd" splits
|
|
# "npm" from "run dev", so a literal "npm run dev" substring never matches
|
|
# the tracked PID's own cmdline. Loosen to "vite" (matches once the tree
|
|
# gets that far) or "npm"+"dev" both present (matches the wrapper hop too).
|
|
try:
|
|
cmd = " ".join(psutil.Process(pid).cmdline())
|
|
except Exception:
|
|
return False
|
|
return "vite" in cmd or ("npm" in cmd and "dev" in cmd)
|
|
|
|
|
|
def _http_up() -> bool:
|
|
try:
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1.5) as r:
|
|
return r.status == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def is_running() -> bool:
|
|
"""Mirrors ncp.py's own status check: PID liveness first, then whether the
|
|
port actually answers (covers Vite started by another process, or a lost
|
|
PID file) - not the stricter pattern match `stop()` uses before killing."""
|
|
pid = _read_pid()
|
|
if pid is not None and psutil.pid_exists(pid):
|
|
return True
|
|
return _http_up()
|
|
|
|
|
|
def start() -> dict:
|
|
if is_running():
|
|
return {"status": "already_running"}
|
|
npm = _npm()
|
|
if not npm:
|
|
return {"status": "error", "detail": "npm not found - install Node.js"}
|
|
|
|
argv = [npm, "run", "dev", "--", "--host", "0.0.0.0"]
|
|
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
LOG_FILE.write_text("")
|
|
# CREATE_NO_WINDOW (not DETACHED_PROCESS): a detached child has no console
|
|
# of its own, so Windows hands one to any console program it spawns in
|
|
# turn - npm.cmd -> node -> vite would each flash a window. CREATE_NO_WINDOW
|
|
# gives it a console that's never shown, inherited down the chain.
|
|
kwargs = ({"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP
|
|
| getattr(subprocess, "CREATE_NO_WINDOW", 0)}
|
|
if os.name == "nt" else {"start_new_session": True})
|
|
with open(LOG_FILE, "ab") as log:
|
|
proc = subprocess.Popen(argv, cwd=str(FRONTEND_DIR), stdout=log,
|
|
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
|
|
**kwargs)
|
|
PID_FILE.write_text(str(proc.pid))
|
|
return {"status": "started", "pid": proc.pid}
|
|
|
|
|
|
def stop() -> dict:
|
|
pid = _read_pid()
|
|
if pid is not None and psutil.pid_exists(pid) and _is_ours(pid):
|
|
try:
|
|
proc = psutil.Process(pid)
|
|
# npm.cmd -> node -> vite is a multi-hop tree; kill it depth-first
|
|
# so the parent doesn't outlive its children as an orphaned shell.
|
|
for child in proc.children(recursive=True):
|
|
try:
|
|
child.terminate()
|
|
except Exception:
|
|
pass
|
|
proc.terminate()
|
|
except Exception:
|
|
pass
|
|
PID_FILE.unlink(missing_ok=True)
|
|
return {"status": "stopped"}
|