#!/usr/bin/env python3 """ TFM aInventory - Standalone Launcher (v1.12.0) Converted from run_standalone.sh to Python per Decision D-05. """ import os import sys import yaml import argparse import subprocess import signal import time import threading import logging import socket from typing import List, Optional, Dict, Any # Color codes class Colors: RED = '\033[0;31m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' MAGENTA = '\033[0;35m' CYAN = '\033[0;36m' NC = '\033[0m' # Setup logging logging.basicConfig(level=logging.INFO, format=f"{Colors.BLUE}[INFO]{Colors.NC} %(message)s") logger = logging.getLogger("run_standalone") processes: List[subprocess.Popen] = [] shutdown_event = threading.Event() def signal_handler(sig, frame): logger.info("Shutdown signal received. Stopping processes...") shutdown_event.set() for p in processes: try: p.terminate() except: pass # Give them a moment to terminate gracefully time.sleep(1) for p in processes: try: p.kill() except: pass sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) def load_yaml(file_path: str) -> Dict[str, Any]: if not os.path.exists(file_path): logger.warning(f"Config file {file_path} not found. Using defaults.") return {} try: with open(file_path, 'r') as f: return yaml.safe_load(f) or {} except Exception as e: logger.error(f"Failed to parse {file_path}: {e}") return {} def is_port_in_use(port: int) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(('localhost', port)) == 0 def log_stream(stream, prefix, color): for line in iter(stream.readline, ''): if shutdown_event.is_set(): break if line: print(f"{color}[{prefix}]{Colors.NC} {line.strip()}") stream.close() def start_backend(backend_port: int, backend_cfg: Dict[str, Any]): logger.info(f"Starting Backend on port {backend_port}...") app_cfg = backend_cfg.get("application", {}) env = os.environ.copy() env["DATA_DIR"] = app_cfg.get("data_dir", "./data") env["LOGS_DIR"] = app_cfg.get("logs_dir", "./logs") env["LOG_LEVEL"] = backend_cfg.get("logging", {}).get("log_level", "INFO") # Ensure directories exist os.makedirs(env["DATA_DIR"], exist_ok=True) os.makedirs(env["LOGS_DIR"], exist_ok=True) cmd = [ sys.executable, "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", str(backend_port) ] # Add --reload if in dev mode (detectable via environment or flag, # but the script is for 'run_standalone' which might be used for prod too) # For now, let's keep it simple. try: p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, bufsize=1, universal_newlines=True ) processes.append(p) threading.Thread(target=log_stream, args=(p.stdout, "BACKEND", Colors.GREEN), daemon=True).start() threading.Thread(target=log_stream, args=(p.stderr, "BACKEND", Colors.GREEN), daemon=True).start() return p except FileNotFoundError: logger.error("uvicorn not found. Please install it with: pip install uvicorn") sys.exit(1) def start_frontend(frontend_port: int, frontend_cfg: Dict[str, Any]): logger.info(f"Starting Frontend on port {frontend_port}...") api_cfg = frontend_cfg.get("api", {}) env = os.environ.copy() env["NEXT_PUBLIC_API_URL"] = api_cfg.get("backend_url", "http://localhost:8916") env["PORT"] = str(frontend_port) # Check if we should use 'npm run dev' or 'node server.js' # For standalone, we might prefer 'npm run start' if built, or 'npm run dev' # Bash script used 'npm run start' cmd = ["npm", "run", "dev"] if os.path.exists("frontend/server.js"): # If built for production cmd = ["node", "frontend/server.js"] try: p = subprocess.Popen( cmd, cwd="frontend" if os.path.isdir("frontend") else None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, bufsize=1, universal_newlines=True ) processes.append(p) threading.Thread(target=log_stream, args=(p.stdout, "FRONTEND", Colors.MAGENTA), daemon=True).start() threading.Thread(target=log_stream, args=(p.stderr, "FRONTEND", Colors.MAGENTA), daemon=True).start() return p except FileNotFoundError: logger.error("npm or node not found. Please install Node.js") sys.exit(1) def main(): parser = argparse.ArgumentParser(description="TFM aInventory Standalone Launcher") group = parser.add_mutually_exclusive_group() group.add_argument("--backend-only", action="store_true", help="Start only the backend") group.add_argument("--frontend-only", action="store_true", help="Start only the frontend") args = parser.parse_args() # Load all configs to find ports and settings backend_cfg = load_yaml("config/backend.yaml") frontend_cfg = load_yaml("config/frontend.yaml") network_cfg = load_yaml("config/network.yaml") ports_cfg = network_cfg.get("ports", {}) backend_port = ports_cfg.get("backend_port", 8916) frontend_port = ports_cfg.get("frontend_port", 8917) if not args.frontend_only: if is_port_in_use(backend_port): logger.error(f"Port {backend_port} is already in use. Cannot start backend.") if not args.backend_only: # If we are supposed to start both, we fail. sys.exit(1) else: start_backend(backend_port, backend_cfg) if not args.backend_only: if is_port_in_use(frontend_port): logger.error(f"Port {frontend_port} is already in use. Cannot start frontend.") if not args.frontend_only: sys.exit(1) else: start_frontend(frontend_port, frontend_cfg) if not processes: logger.error("No processes started. Check configuration and port availability.") sys.exit(1) logger.info("All services started. Press Ctrl+C to stop.") # Monitor processes try: while not shutdown_event.is_set(): time.sleep(1) for p in processes: if p.poll() is not None: logger.error(f"Process {p.pid} exited with code {p.returncode}") shutdown_event.set() break except KeyboardInterrupt: pass finally: signal_handler(None, None) if __name__ == "__main__": main()