#!/usr/bin/env python3 """ CADDesigner full service startup script. Starts the API and React Web UI by default. """ import argparse from pathlib import Path import signal import urllib.error import urllib.request from bootstrap_env import load_project_env load_project_env() from SimpleLLMFunc.logger import app_log, push_error import subprocess import sys import time project_root = Path(__file__).parent sys.path.insert(0, str(project_root)) default_working_dir = project_root / "workspace" def validate_directory(path_str: str) -> Path: """Validate and return a valid directory path.""" path = Path(path_str).resolve() if not path.exists(): try: path.mkdir(parents=True, exist_ok=True) app_log(f"Created working directory: {path}") except Exception as exc: raise argparse.ArgumentTypeError(f"Unable to create directory {path}: {exc}") from exc elif not path.is_dir(): raise argparse.ArgumentTypeError(f"Path {path} is not a directory") return path def resolve_display_host(host: str) -> str: """Host address used for log display.""" if host in {"0.0.0.0", "::"}: return "localhost" return host def resolve_local_connect_host(host: str) -> str: """Host address used for inter-process connections on the same machine.""" if host in {"0.0.0.0", "::"}: return "127.0.0.1" return host def build_http_url(host: str, port: int) -> str: return f"http://{host}:{port}" class ServiceManager: """Service manager.""" def __init__(self): self.processes: list[tuple[str, subprocess.Popen]] = [] self.running = False signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) def _signal_handler(self, signum, frame): """Signal handler.""" app_log(f"\nReceived signal {signum}; shutting down all services...") self.stop_all() sys.exit(0) def start_api_server(self, args): """Start the API server.""" app_log("Starting API server...") cmd = [ sys.executable, str(project_root / "start_caddesigner_api.py"), "--host", args.api_host, "--port", str(args.api_port), "--working-dir", str(args.working_dir), "--log-level", args.log_level, ] if args.reload: cmd.append("--reload") if args.workers > 1 and not args.reload: cmd.extend(["--workers", str(args.workers)]) if args.debug: cmd.append("--debug") process = subprocess.Popen(cmd) self.processes.append(("API server", process)) return process def start_ui_server(self, args): """Start React Web UI.""" ui_label = "React Web UI" app_log(f"Starting {ui_label}...") internal_api_url = build_http_url( resolve_local_connect_host(args.api_host), args.api_port, ) cmd = [ sys.executable, str(project_root / "start_caddesigner_ui.py"), "--host", args.ui_host, "--port", str(args.ui_port), "--api-url", internal_api_url, ] if args.debug: cmd.append("--debug") process = subprocess.Popen(cmd) self.processes.append((ui_label, process)) return process def wait_for_api_ready(self, args, api_process, timeout_seconds: float = 30.0): """Wait for the API service to pass the health check.""" health_url = ( build_http_url(resolve_local_connect_host(args.api_host), args.api_port) + "/health" ) deadline = time.time() + timeout_seconds while time.time() < deadline: if api_process.poll() is not None: push_error("API server process exited early") return False try: with urllib.request.urlopen(health_url, timeout=1.0) as response: if response.status == 200: app_log(f"API health check passed: {health_url}") return True except (urllib.error.URLError, TimeoutError, OSError): time.sleep(0.5) continue time.sleep(0.5) push_error(f"API health check timed out: {health_url}") return False def start_both_services(self, args): """Start all services.""" self.running = True api_display_url = build_http_url( resolve_display_host(args.api_host), args.api_port ) ui_display_url = build_http_url( resolve_display_host(args.ui_host), args.ui_port ) ui_label = "React UI" app_log("Starting CADDesigner full service suite...") app_log("=" * 60) api_process = self.start_api_server(args) app_log(f"API server process started (PID: {api_process.pid})") app_log("Waiting for API health check to pass...") if not self.wait_for_api_ready(args, api_process): return False ui_process = self.start_ui_server(args) app_log(f"{ui_label} process started (PID: {ui_process.pid})") app_log("=" * 60) app_log(f"API server: {api_display_url}") app_log(f"{ui_label}: {ui_display_url}") app_log(f"API documentation: {api_display_url}/docs") app_log(f"Working directory: {args.working_dir}") app_log("Web UI implementation: React + Vite") if args.reload: app_log("Development mode: enabled (API auto reload; React built-in HMR)") app_log("Press Ctrl+C to stop all services") app_log("=" * 60) try: while self.running and any( process.poll() is None for _, process in self.processes ): time.sleep(1) except KeyboardInterrupt: app_log("\nStopping all services...") finally: self.stop_all() return True def stop_all(self): """Stop all processes.""" self.running = False for name, process in self.processes: if process.poll() is None: try: app_log(f"Stopping {name}...") process.terminate() process.wait(timeout=5) app_log(f"{name} stopped") except subprocess.TimeoutExpired: app_log(f"Force terminating {name}...") process.kill() except Exception as exc: push_error(f"Error while stopping {name}: {exc}") self.processes.clear() app_log("All services stopped") def main(): """Main function.""" parser = argparse.ArgumentParser( description="CADDesigner full service launcher", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Usage examples: # Start API + React UI with the default configuration %(prog)s # Start with custom ports %(prog)s --api-port 8001 --ui-port 7861 # Enable development mode %(prog)s --reload # Start in a specific working directory %(prog)s --working-dir /path/to/workspace Service addresses: API server: http://localhost:8000 React UI: http://localhost:7860 API docs: http://localhost:8000/docs """, ) api_group = parser.add_argument_group("API server configuration") api_group.add_argument( "--api-host", default="0.0.0.0", help="API server host address (default: 0.0.0.0)", ) api_group.add_argument( "--api-port", type=int, default=8000, help="API server port (default: 8000)", ) api_group.add_argument( "--workers", type=int, default=1, help="Number of API server worker processes (default: 1)", ) api_group.add_argument( "--log-level", choices=["debug", "info", "warning", "error"], default="info", help="API server log level (default: info)", ) ui_group = parser.add_argument_group("React Web UI configuration") ui_group.add_argument( "--ui-host", default="0.0.0.0", help="Web UI host address (default: 0.0.0.0)", ) ui_group.add_argument( "--ui-port", type=int, default=7860, help="Web UI port (default: 7860)", ) common_group = parser.add_argument_group("Common configuration") common_group.add_argument( "--working-dir", type=validate_directory, default=default_working_dir, help="Working directory path (default: ./workspace)", ) common_group.add_argument( "--reload", action="store_true", help="Enable development mode (API server auto reload)", ) common_group.add_argument( "--debug", action="store_true", help="Enable debug mode", ) args = parser.parse_args() manager = ServiceManager() try: success = manager.start_both_services(args) if not success: sys.exit(1) except KeyboardInterrupt: app_log("\nUser requested service shutdown") except Exception as exc: push_error(f"Service startup failed: {exc}") if args.debug: import traceback push_error(traceback.format_exc()) sys.exit(1) if __name__ == "__main__": main()