feat: add portable NexusOS CLI and packaging
This commit is contained in:
+42
-15
@@ -12,12 +12,16 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
try:
|
||||
import psutil
|
||||
except ImportError: # optional in the portable/Termux core install
|
||||
psutil = None
|
||||
|
||||
from .nexus_config import PROJECT_ROOT, RUNTIME_DIR
|
||||
from .nexus_config import FRONTEND_SOURCE_DIR, RUNTIME_DIR
|
||||
|
||||
FRONTEND_DIR = PROJECT_ROOT / "interface" / "web"
|
||||
FRONTEND_DIR = FRONTEND_SOURCE_DIR
|
||||
PID_FILE = RUNTIME_DIR / "pids" / "frontend.pid"
|
||||
LOG_FILE = RUNTIME_DIR / "frontend.log"
|
||||
PORT = 5173
|
||||
@@ -41,12 +45,30 @@ def _is_ours(pid: int) -> bool:
|
||||
# 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())
|
||||
if psutil is not None:
|
||||
cmd = " ".join(psutil.Process(pid).cmdline())
|
||||
elif os.name != "nt":
|
||||
cmd = (Path(f"/proc/{pid}/cmdline").read_bytes()
|
||||
.replace(b"\0", b" ").decode(errors="replace"))
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
return "vite" in cmd or ("npm" in cmd and "dev" in cmd)
|
||||
|
||||
|
||||
def _alive(pid: int | None) -> bool:
|
||||
if pid is None:
|
||||
return False
|
||||
if psutil is not None:
|
||||
return psutil.pid_exists(pid)
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _http_up() -> bool:
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1.5) as r:
|
||||
@@ -60,7 +82,7 @@ def is_running() -> bool:
|
||||
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):
|
||||
if _alive(pid):
|
||||
return True
|
||||
return _http_up()
|
||||
|
||||
@@ -68,6 +90,8 @@ def is_running() -> bool:
|
||||
def start() -> dict:
|
||||
if is_running():
|
||||
return {"status": "already_running"}
|
||||
if not FRONTEND_DIR.is_dir():
|
||||
return {"status": "unavailable", "detail": "Vite source is not included in wheel installs"}
|
||||
npm = _npm()
|
||||
if not npm:
|
||||
return {"status": "error", "detail": "npm not found - install Node.js"}
|
||||
@@ -92,17 +116,20 @@ def start() -> dict:
|
||||
|
||||
def stop() -> dict:
|
||||
pid = _read_pid()
|
||||
if pid is not None and psutil.pid_exists(pid) and _is_ours(pid):
|
||||
if _alive(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()
|
||||
if psutil is not None:
|
||||
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()
|
||||
elif os.name != "nt":
|
||||
os.kill(pid, 15)
|
||||
except Exception:
|
||||
pass
|
||||
PID_FILE.unlink(missing_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user