329 lines
9.4 KiB
Python
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()
|