[Haik]: ckpt, abstracted helpers within NineRouterProcess

This commit is contained in:
haikdc
2026-04-05 20:47:18 -07:00
parent aa250ce203
commit 28820c91eb
5 changed files with 67 additions and 49 deletions
@@ -0,0 +1,164 @@
"""Subprocess lifecycle management for the 9Router process.
9Router is a local Node.js proxy that lets users connect their
Claude/ChatGPT/Gemini subscriptions to OpenSwarm without API keys.
"""
import asyncio
import os
import shutil
import subprocess
import threading
from typing import Optional
import httpx
from typeguard import typechecked
from backend.ports import NINE_ROUTER_PORT
from backend.apps.subscriptions.NineRouter.constants import NINE_ROUTER_V1, NINE_ROUTER_URL
from backend.apps.subscriptions.NineRouter.NineRouterProcess.helpers.forward_output import forward_output
from backend.apps.subscriptions.NineRouter.NineRouterProcess.helpers.find_9router_dir import find_9router_dir
from backend.apps.subscriptions.NineRouter.NineRouterProcess.helpers.find_node import find_node
P_PROCESS: subprocess.Popen | None = None
P_THIS_DIR: str = os.path.dirname(os.path.abspath(__file__))
@typechecked
def is_running() -> bool:
try:
r = httpx.get(f"{NINE_ROUTER_V1}/models", timeout=2.0)
return r.status_code == 200
except Exception:
return False
@typechecked
async def ensure_running() -> None:
"""Start 9Router if not already running."""
global P_PROCESS
_is_packaged: bool = os.environ.get("OPENSWARM_PACKAGED") == "1"
if is_running():
if not _is_packaged:
try:
result = subprocess.run(
["pgrep", "-f", "next-server"],
capture_output=True, text=True, timeout=3,
)
if result.stdout.strip():
print("9Router: killing stale standalone to use next dev", flush=True)
subprocess.run(["pkill", "-f", "next-server"], timeout=5)
await asyncio.sleep(2)
else:
return
except Exception:
return
else:
return
_9router_dir: Optional[str] = find_9router_dir(P_THIS_DIR)
cmd: list[str]
cwd: str | None
env: dict[str, str]
if _is_packaged and _9router_dir:
standalone_server: str = os.path.join(_9router_dir, "server.js")
if not os.path.exists(standalone_server):
standalone_server = os.path.join(_9router_dir, ".next", "standalone", "server.js")
if not os.path.exists(standalone_server):
print("9Router: standalone build not found in", _9router_dir, flush=True)
return
node: Optional[str] = find_node()
if not node:
print("9Router: Node.js not found, cannot start in packaged mode", flush=True)
return
print(f"9Router: starting (production) on port {NINE_ROUTER_PORT}...", flush=True)
cmd = [node, standalone_server]
cwd = os.path.dirname(standalone_server)
env = {
**os.environ,
"PORT": str(NINE_ROUTER_PORT),
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
"NODE_ENV": "production",
}
if node == os.environ.get("OPENSWARM_ELECTRON_PATH"):
env["ELECTRON_RUN_AS_NODE"] = "1"
elif _9router_dir:
npx: str | None = shutil.which("npx")
if not npx:
print("9Router: npx not found, cannot auto-start", flush=True)
return
if not os.path.isdir(os.path.join(_9router_dir, "node_modules")):
print("9Router: installing dependencies...", flush=True)
npm: str | None = shutil.which("npm")
if npm:
subprocess.run(
[npm, "install"], cwd=_9router_dir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120,
)
print(f"9Router: starting (dev) on port {NINE_ROUTER_PORT}...", flush=True)
cmd = [npx, "next", "dev", "--webpack", "-p", str(NINE_ROUTER_PORT)]
cwd = _9router_dir
env = {
**os.environ,
"PORT": str(NINE_ROUTER_PORT),
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
}
else:
npx = shutil.which("npx")
if not npx:
print("9Router: npx not found and no bundled 9router directory", flush=True)
return
print(f"9Router: starting (npx) on port {NINE_ROUTER_PORT}...", flush=True)
cmd = [npx, "9router", "--port", str(NINE_ROUTER_PORT), "--no-browser", "--skip-update"]
cwd = None
env = {
**os.environ,
"PORT": str(NINE_ROUTER_PORT),
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
}
try:
P_PROCESS = subprocess.Popen(
cmd, cwd=cwd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
threading.Thread(target=forward_output, args=(P_PROCESS.stdout,), daemon=True).start()
timeout: int = 20 if _is_packaged else 30
for _ in range(timeout * 2):
await asyncio.sleep(0.5)
if is_running():
print("9Router: started successfully", flush=True)
return
print(f"9Router: did not start within {timeout}s", flush=True)
except Exception as e:
print(f"9Router: failed to start: {e}", flush=True)
@typechecked
def stop() -> None:
global P_PROCESS
if P_PROCESS:
try:
P_PROCESS.terminate()
P_PROCESS.wait(timeout=5)
except Exception:
try:
P_PROCESS.kill()
except Exception:
pass
P_PROCESS = None
print("9Router stopped")
@@ -0,0 +1,26 @@
import os
from typing import Optional
from typeguard import typechecked
# TODO: type spec this entirely
@typechecked
def find_9router_dir(
root_dir: str,
) -> Optional[str]:
"""Locate the bundled 9Router directory (dev or packaged)."""
p_is_packaged: bool = os.environ.get("OPENSWARM_PACKAGED") == "1"
if p_is_packaged:
p_resources: str = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(root_dir))))
p_candidate: str = os.path.join(p_resources, "9router")
if os.path.isdir(p_candidate):
return p_candidate
else:
p_backend_dir: str = os.path.dirname(os.path.dirname(os.path.dirname(root_dir)))
p_project_root: str = os.path.dirname(p_backend_dir)
p_candidate = os.path.join(p_project_root, "9router")
if os.path.isdir(p_candidate):
return p_candidate
return None
@@ -0,0 +1,15 @@
import os
import shutil
from typing import Optional
from typeguard import typechecked
@typechecked
def find_node() -> Optional[str]:
node: Optional[str] = shutil.which("node")
if node:
return node
electron_path: Optional[str] = os.environ.get("OPENSWARM_ELECTRON_PATH")
if electron_path and os.path.exists(electron_path):
return electron_path
return None
@@ -0,0 +1,18 @@
from typeguard import typechecked
# TODO: type spec this entirely
@typechecked
def forward_output(pipe) -> None:
try:
for line in iter(pipe.readline, b""):
text = line.decode("utf-8", errors="replace").rstrip()
if text:
print(f"[9router] {text}", flush=True)
except Exception:
pass
finally:
try:
pipe.close()
except Exception:
pass