""" pyserve up - Start all services """ import asyncio import signal import sys import time from pathlib import Path from typing import Any import click @click.command("up") @click.argument("services", nargs=-1) @click.option( "-d", "--detach", is_flag=True, help="Run in background (detached mode)", ) @click.option( "--build", is_flag=True, help="Build/reload applications before starting", ) @click.option( "--force-recreate", is_flag=True, help="Recreate services even if configuration hasn't changed", ) @click.option( "--scale", "scales", multiple=True, help="Scale SERVICE to NUM workers (e.g., --scale api=4)", ) @click.option( "--timeout", "timeout", default=60, type=int, help="Timeout in seconds for service startup", ) @click.option( "--wait", is_flag=True, help="Wait for services to be healthy before returning", ) @click.option( "--remove-orphans", is_flag=True, help="Remove services not defined in configuration", ) @click.pass_obj def up_cmd( ctx: Any, services: tuple[str, ...], detach: bool, build: bool, force_recreate: bool, scales: tuple[str, ...], timeout: int, wait: bool, remove_orphans: bool, ) -> None: """ Start services defined in configuration. If no services are specified, all services will be started. \b Examples: pyserve up # Start all services pyserve up -d # Start in background pyserve up api admin # Start specific services pyserve up --scale api=4 # Scale api to 4 workers pyserve up --wait # Wait for healthy status """ from .._runner import ServiceRunner from ..output import console, print_error, print_info, print_success, print_warning from ..state import StateManager config_path = Path(ctx.config_file) if not config_path.exists(): print_error(f"Configuration file not found: {config_path}") print_info("Run 'pyserve init' to create a configuration file") raise click.Abort() scale_map = {} for scale in scales: try: service, num = scale.split("=") scale_map[service] = int(num) except ValueError: print_error(f"Invalid scale format: {scale}. Use SERVICE=NUM") raise click.Abort() try: from ...config import Config config = Config.from_yaml(str(config_path)) except Exception as e: print_error(f"Failed to load configuration: {e}") raise click.Abort() state_manager = StateManager(Path(".pyserve"), ctx.project) if state_manager.is_daemon_running(): daemon_pid = state_manager.get_daemon_pid() print_warning(f"PyServe daemon is already running (PID: {daemon_pid})") if not click.confirm("Do you want to restart it?"): raise click.Abort() try: import os from typing import cast # FIXME: Please fix the cast usage here os.kill(cast(int, daemon_pid), signal.SIGTERM) time.sleep(2) except ProcessLookupError: pass state_manager.clear_daemon_pid() runner = ServiceRunner(config, state_manager) service_list = list(services) if services else None if detach: console.print("[bold]Starting PyServe in background...[/bold]") try: pid = runner.start_daemon( service_list, scale_map=scale_map, force_recreate=force_recreate, ) state_manager.set_daemon_pid(pid) print_success(f"PyServe started in background (PID: {pid})") print_info("Use 'pyserve ps' to see service status") print_info("Use 'pyserve logs -f' to follow logs") print_info("Use 'pyserve down' to stop") except Exception as e: print_error(f"Failed to start daemon: {e}") raise click.Abort() else: console.print("[bold]Starting PyServe...[/bold]") def signal_handler(signum: int, frame: Any) -> None: console.print("\n[yellow]Received shutdown signal...[/yellow]") runner.stop() sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) try: asyncio.run( runner.start( service_list, scale_map=scale_map, force_recreate=force_recreate, wait_healthy=wait, timeout=timeout, ) ) except KeyboardInterrupt: console.print("\n[yellow]Shutting down...[/yellow]") except Exception as e: print_error(f"Failed to start services: {e}") if ctx.debug: raise raise click.Abort()