Files
cadSet/CADDesigner-Code/start_caddesigner_full.py
T
2026-07-22 19:38:36 +08:00

329 lines
9.4 KiB
Python

#!/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()