"""Run the public Node API and private Python worker as one service.""" import json import os from pathlib import Path import signal import subprocess import sys import time from urllib.request import urlopen from dotenv import load_dotenv def wait_for_analysis(process, should_stop, timeout_seconds=25 * 60): deadline = time.monotonic() + timeout_seconds while not should_stop(): exit_code = process.poll() if exit_code is not None: print(f"Analysis process exited before it was ready ({exit_code})", flush=True) return False try: with urlopen("http://127.0.0.1:8000/healthz", timeout=1) as response: payload = json.load(response) if response.status == 200 else None if isinstance(payload, dict) and payload.get("ok") is True: return process.poll() is None except (OSError, ValueError): pass if time.monotonic() >= deadline: print(f"Analysis process did not become ready within {timeout_seconds} seconds", flush=True) return False time.sleep(0.5) return False def main(): root = Path(__file__).resolve().parent load_dotenv(root / ".env") port = int(os.environ.get("PORT", "4000")) if not 1 <= port <= 65535 or port == 8000: raise ValueError("PORT must be between 1 and 65535 and differ from AI port 8000") env = dict(os.environ, PORT=str(port), ANALYSIS_API_URL="http://127.0.0.1:8000", BACKEND_INTERNAL_URL=f"http://127.0.0.1:{port}") children = [] stopping = False def stop(signum, frame): nonlocal stopping stopping = True for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, stop) try: analysis = subprocess.Popen([sys.executable, "-m", "uvicorn", "app:app", "--host", "127.0.0.1", "--port", "8000", "--workers", "1"], cwd=root, env=env) children.append(analysis) if not wait_for_analysis(analysis, lambda: stopping): return 0 if stopping else 1 children.append(subprocess.Popen(["node", "dist/server.js"], cwd=root, env=env)) while not stopping: for child in children: if child.poll() is not None: print(f"Backend process {child.pid} exited ({child.returncode}); stopping service", flush=True) return child.returncode or 1 time.sleep(0.25) return 0 finally: for child in children: if child.poll() is None: child.terminate() deadline = time.monotonic() + 15 for child in children: try: child.wait(timeout=max(0, deadline - time.monotonic())) except subprocess.TimeoutExpired: child.kill() child.wait() if __name__ == "__main__": sys.exit(main())