[Haik]: created helper sub fulder and fixed imports

This commit is contained in:
haikdc
2026-04-05 21:13:18 -07:00
parent dd79b10e3c
commit 2dec5b6283
7 changed files with 7 additions and 7 deletions
@@ -0,0 +1,162 @@
"""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.helpers.constants import NINE_ROUTER_V1, NINE_ROUTER_URL
from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.forward_output import forward_output
from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.find_9router_dir import find_9router_dir
from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.find_node import find_node
_THIS_DIR: str = os.path.dirname(os.path.abspath(__file__))
class NineRouterProcess:
def __init__(self) -> None:
self._process: Optional[subprocess.Popen] = None
@typechecked
def is_running(self) -> 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(self) -> None:
"""Start 9Router if not already running."""
_is_packaged: bool = os.environ.get("OPENSWARM_PACKAGED") == "1"
if self.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(_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:
self._process = subprocess.Popen(
cmd, cwd=cwd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)
threading.Thread(target=forward_output, args=(self._process.stdout,), daemon=True).start()
timeout: int = 20 if _is_packaged else 30
for _ in range(timeout * 2):
await asyncio.sleep(0.5)
if self.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(self) -> None:
if self._process:
try:
self._process.terminate()
self._process.wait(timeout=5)
except Exception:
try:
self._process.kill()
except Exception:
pass
self._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