diff --git a/skyvern/cli/commands.py b/skyvern/cli/commands.py index 019ac926..897fff5e 100644 --- a/skyvern/cli/commands.py +++ b/skyvern/cli/commands.py @@ -6,6 +6,7 @@ from .init_command import init, init_browser from .quickstart import quickstart_app from .run_commands import run_app from .status import status_app +from .stop_commands import stop_app from .tasks import tasks_app from .workflow import workflow_app @@ -23,6 +24,7 @@ cli_app.add_typer(workflow_app, name="workflow", help="Workflow management comma cli_app.add_typer(tasks_app, name="tasks", help="Task management commands.") cli_app.add_typer(docs_app, name="docs", help="Open Skyvern documentation.") cli_app.add_typer(status_app, name="status", help="Check if Skyvern services are running.") +cli_app.add_typer(stop_app, name="stop", help="Stop Skyvern services.") init_app = typer.Typer( invoke_without_command=True, help="Interactively configure Skyvern and its dependencies.", diff --git a/skyvern/cli/stop_commands.py b/skyvern/cli/stop_commands.py new file mode 100644 index 00000000..dda02f35 --- /dev/null +++ b/skyvern/cli/stop_commands.py @@ -0,0 +1,75 @@ +from typing import List + +import psutil +import typer +from rich.panel import Panel + +from .console import console + +stop_app = typer.Typer(help="Commands to stop Skyvern services.") + + +def get_pids_on_port(port: int) -> List[int]: + """Return a list of PIDs listening on the given port.""" + pids = [] + try: + for conn in psutil.net_connections(kind="inet"): + if conn.laddr and conn.laddr.port == port and conn.pid and conn.status == psutil.CONN_LISTEN: + pids.append(conn.pid) + except Exception: + pass + return list(set(pids)) + + +def kill_pids(pids: List[int], service_name: str) -> bool: + """Kill the given list of PIDs in a cross-platform way.""" + if not pids: + console.print(f"[yellow]No {service_name} processes found.[/yellow]") + return False + + killed_any = False + for pid in pids: + try: + # Use psutil for cross-platform process killing + process = psutil.Process(pid) + process.terminate() + + # Wait for the process to exit, use kill() as fallback + process_stopped = False + try: + process.wait(timeout=3) + process_stopped = True + except psutil.TimeoutExpired: + console.print(f"[yellow]Process {pid} didn't terminate gracefully, forcing kill...[/yellow]") + process.kill() + try: + process.wait(timeout=3) + process_stopped = True + except psutil.TimeoutExpired: + console.print(f"[red]Process {pid} remains unresponsive even after force kill[/red]") + + if process_stopped: + killed_any = True + console.print(f"[green]✅ Stopped {service_name} process (PID: {pid})[/green]") + else: + console.print(f"[red]❌ Failed to stop {service_name} process (PID: {pid})[/red]") + except psutil.NoSuchProcess: + console.print(f"[yellow]Process {pid} was already stopped[/yellow]") + except psutil.AccessDenied: + console.print(f"[red]Access denied when trying to stop process {pid}[/red]") + except Exception as e: + console.print(f"[red]Failed to stop process {pid}: {e}[/red]") + + return killed_any + + +@stop_app.command(name="server") +def stop_server(port: int = typer.Option(8000, "--port", "-p", help="Port number for the Skyvern API server")) -> None: + """Stop the Skyvern API server running on the specified port (default: 8000).""" + console.print(Panel(f"[bold red]Stopping Skyvern API Server (port {port})...[/bold red]", border_style="red")) + + pids = get_pids_on_port(port) + if kill_pids(pids, f"Skyvern API server (port {port})"): + console.print(f"[green]🛑 Skyvern API server on port {port} stopped successfully.[/green]") + else: + console.print(f"[yellow]No Skyvern API server found running on port {port}.[/yellow]")