mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
"""Subprocess lifecycle management for the 9Router process."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import threading
|
|
|
|
import httpx
|
|
|
|
from backend.ports import NINE_ROUTER_PORT
|
|
|
|
import subprocess as _sp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
NINE_ROUTER_URL = f"http://localhost:{NINE_ROUTER_PORT}"
|
|
NINE_ROUTER_V1 = f"{NINE_ROUTER_URL}/v1"
|
|
|
|
_process: subprocess.Popen | None = None
|
|
|
|
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _forward_output(pipe):
|
|
"""Read subprocess output line by line and print with [9router] prefix."""
|
|
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
|
|
|
|
|
|
def is_running() -> bool:
|
|
"""Check if 9Router is running."""
|
|
try:
|
|
r = httpx.get(f"{NINE_ROUTER_V1}/models", timeout=2.0)
|
|
return r.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _find_9router_dir() -> str | None:
|
|
"""Locate the bundled 9Router directory (works in both dev and packaged mode)."""
|
|
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
|
|
|
|
if _is_packaged:
|
|
# Packaged Electron app — 9router is in extraResources
|
|
# _THIS_DIR is <resources>/backend/apps/nine_router/
|
|
_resources = os.path.dirname(os.path.dirname(os.path.dirname(_THIS_DIR)))
|
|
_candidate = os.path.join(_resources, "9router")
|
|
if os.path.isdir(_candidate):
|
|
return _candidate
|
|
else:
|
|
# Dev mode — 9router is at project root
|
|
_backend_dir = os.path.dirname(os.path.dirname(_THIS_DIR))
|
|
_project_root = os.path.dirname(_backend_dir)
|
|
_candidate = os.path.join(_project_root, "9router")
|
|
if os.path.isdir(_candidate):
|
|
return _candidate
|
|
|
|
return None
|
|
|
|
|
|
def _find_node() -> str | None:
|
|
"""Find a Node.js binary (works in both dev and packaged mode)."""
|
|
node = shutil.which("node")
|
|
if node:
|
|
return node
|
|
|
|
# In packaged Electron app, use the Electron binary with ELECTRON_RUN_AS_NODE=1
|
|
electron_path = os.environ.get("OPENSWARM_ELECTRON_PATH")
|
|
if electron_path and os.path.exists(electron_path):
|
|
return electron_path
|
|
|
|
return None
|
|
|
|
|
|
async def ensure_running():
|
|
"""Start 9Router if not already running."""
|
|
global _process
|
|
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
|
|
|
|
if is_running():
|
|
if not _is_packaged:
|
|
try:
|
|
result = _sp.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)
|
|
_sp.run(["pkill", "-f", "next-server"], timeout=5)
|
|
await asyncio.sleep(2)
|
|
else:
|
|
return
|
|
except Exception:
|
|
return
|
|
else:
|
|
return
|
|
|
|
_9router_dir = _find_9router_dir()
|
|
|
|
if _is_packaged and _9router_dir:
|
|
standalone_server = 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 = _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 = 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 = 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:
|
|
_process = subprocess.Popen(
|
|
cmd,
|
|
cwd=cwd,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
env=env,
|
|
)
|
|
threading.Thread(
|
|
target=_forward_output, args=(_process.stdout,), daemon=True,
|
|
).start()
|
|
|
|
timeout = 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)
|
|
|
|
|
|
def stop():
|
|
"""Stop the 9Router subprocess."""
|
|
global _process
|
|
if _process:
|
|
try:
|
|
_process.terminate()
|
|
_process.wait(timeout=5)
|
|
except Exception:
|
|
try:
|
|
_process.kill()
|
|
except Exception:
|
|
pass
|
|
_process = None
|
|
logger.info("9Router stopped")
|