Files
Artemis-Eapil/artemis/interfaces/cli/commands/server_lifecycle.py
T
somew1nd c2c90a6378 feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream
- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization)
- Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration
- Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery
- Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution
- Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges
- Add comprehensive unit test coverage and edge-case verification scripts
2026-08-26 15:53:14 -07:00

256 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""CLI commands for Artemis server lifecycle management: restart, stop, and status."""
from __future__ import annotations
import datetime
from typing import Annotated
import webbrowser
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
import typer
from artemis.interfaces.cli.commands.ui import ui_command
from artemis.runtime.daemon_client import spawn_daemon
from artemis.runtime.server_lifecycle import (
find_server_pids,
get_server_status,
stop_server,
)
console = Console()
def _format_uptime(seconds: float | None) -> str:
"""Format seconds into human-friendly duration (e.g. 1h 23m 45s)."""
if seconds is None:
return "Unknown"
sec = int(seconds)
hours, remainder = divmod(sec, 3600)
minutes, remainder_sec = divmod(remainder, 60)
parts = []
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0 or hours > 0:
parts.append(f"{minutes}m")
parts.append(f"{remainder_sec}s")
return " ".join(parts)
def restart_command(
host: Annotated[
str,
typer.Option("--host", "-H", help="Bind host address for the UI server."),
] = "0.0.0.0",
port: Annotated[
int,
typer.Option("--port", "-p", help="Port to run the unified UI server on."),
] = 8000,
open_browser: Annotated[
bool,
typer.Option(
"--open/--no-open",
help="Automatically open the Showcase UI in default web browser.",
),
] = True,
force: Annotated[
bool,
typer.Option(
"--force",
"-f",
help="Force kill previous server immediately without waiting for graceful exit.",
),
] = False,
reload: Annotated[
bool,
typer.Option(
"--reload/--no-reload",
help="Enable uvicorn live auto-reload (for development).",
),
] = False,
daemon: Annotated[
bool,
typer.Option(
"--daemon",
"-d",
help="Run the new server as a detached background process instead of foreground.",
),
] = False,
) -> None:
"""Restart the Artemis Web UI server, terminating any existing instance on the port."""
console.print()
console.print(
Panel(
f"[bold cyan]πŸ”„ Artemis Server Restart[/bold cyan]\n"
f"[dim]Inspecting port {port} and recycling any running Artemis instances...[/dim]",
border_style="cyan",
expand=False,
)
)
# 1. Stop existing server
existing_pids = find_server_pids(port)
if existing_pids:
pid_str = ", ".join(map(str, existing_pids))
console.print(f" [yellow]πŸ›‘ Terminating running Artemis instance (PID: {pid_str})...[/yellow]")
else:
console.print(f" [dim]β„Ή No active Artemis server found on port {port}.[/dim]")
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=4.0)
if stopped_pids:
console.print(f" [green]βœ“ {msg}[/green]\n")
elif not success:
console.print(f" [red]⚠ {msg}[/red]\n")
else:
console.print(f" [green]βœ“ Port {port} is clear and ready.[/green]\n")
# 2. Launch fresh server instance
local_url = f"http://localhost:{port}"
if daemon:
console.print(f" [cyan]πŸš€ Starting Artemis in detached background daemon mode...[/cyan]")
proc = spawn_daemon(host=host, port=port)
if proc:
console.print(
f" [bold green]βœ“ Artemis server running in background (PID: {proc.pid})[/bold green]\n"
f" 🌐 [cyan]{local_url}[/cyan]\n"
f" πŸ› οΈ [cyan]{local_url}/admin[/cyan]\n"
)
if open_browser:
webbrowser.open(local_url)
else:
console.print(" [red]❌ Failed to spawn background daemon.[/red]\n")
raise typer.Exit(code=1)
return
# Default foreground mode
console.print(f" [cyan]πŸš€ Launching unified Artemis Showcase UI in current terminal...[/cyan]\n")
ui_command(host=host, port=port, open_browser=open_browser, reload=reload)
def stop_command(
port: Annotated[
int,
typer.Option("--port", "-p", help="Port of the Artemis server to stop."),
] = 8000,
force: Annotated[
bool,
typer.Option(
"--force",
"-f",
help="Force kill immediately without waiting for graceful shutdown.",
),
] = False,
) -> None:
"""Stop any running Artemis Web UI / server instance on the given port."""
console.print()
existing_pids = find_server_pids(port)
if existing_pids:
pid_str = ", ".join(map(str, existing_pids))
console.print(f" [yellow]πŸ›‘ Stopping Artemis server on port {port} (PID: {pid_str})...[/yellow]")
else:
console.print(f" [dim]Scanning port {port}...[/dim]")
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=4.0)
if stopped_pids:
console.print(
Panel(
f"[bold green]βœ“ Artemis server stopped successfully.[/bold green]\n\n"
f"[dim]{msg}[/dim]",
title="πŸ›‘ Server Stopped",
border_style="green",
expand=False,
)
)
elif not success:
console.print(
Panel(
f"[bold red]⚠ Failed to fully stop server on port {port}[/bold red]\n\n{msg}",
title="⚠ Warning",
border_style="red",
expand=False,
)
)
raise typer.Exit(code=1)
else:
console.print(
Panel(
f"[dim]No active Artemis server found on port {port}. Port is free.[/dim]",
title="β„Ή Status",
border_style="dim",
expand=False,
)
)
def status_command(
port: Annotated[
int,
typer.Option("--port", "-p", help="Port to query for Artemis server status."),
] = 8000,
) -> None:
"""Check whether the Artemis server is currently running and display runtime metadata."""
console.print()
status = get_server_status(port=port)
if status["running"]:
pids_str = ", ".join(map(str, status["pids"])) if status["pids"] else str(status["active_pid"] or "Unknown")
uptime_str = _format_uptime(status["uptime_seconds"])
metadata = status.get("metadata") or {}
started_at_str = "Unknown"
if metadata.get("started_at"):
try:
dt = datetime.datetime.fromtimestamp(metadata["started_at"])
started_at_str = dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
table = Table(show_header=False, box=None, padding=(0, 2))
table.add_row("[bold cyan]Status[/bold cyan]", "[bold green]● ONLINE / RUNNING[/bold green]")
table.add_row("[bold cyan]PID(s)[/bold cyan]", f"[yellow]{pids_str}[/yellow]")
table.add_row("[bold cyan]Port[/bold cyan]", f"[white]{port}[/white]")
table.add_row("[bold cyan]Uptime[/bold cyan]", f"[white]{uptime_str}[/white]")
table.add_row("[bold cyan]Started At[/bold cyan]", f"[dim]{started_at_str}[/dim]")
table.add_row("[bold cyan]Showcase UI[/bold cyan]", f"[cyan]{status['url']}[/cyan]")
table.add_row("[bold cyan]Admin Debug[/bold cyan]", f"[magenta]{status['admin_url']}[/magenta]")
if metadata.get("cwd"):
table.add_row("[bold cyan]Workspace[/bold cyan]", f"[dim]{metadata['cwd']}[/dim]")
console.print(
Panel(
table,
title="πŸš€ Artemis Server Status",
border_style="green",
expand=False,
)
)
console.print(" [dim]πŸ’‘ To restart: [bold]artemis restart[/bold] | To stop: [bold]artemis stop[/bold][/dim]\n")
else:
console.print(
Panel(
f"[dim]β—‹ OFFLINE / STOPPED[/dim]\n\n"
f"Port [cyan]{port}[/cyan] is currently free.\n"
f"[dim]No active Artemis server process detected.[/dim]",
title="β„Ή Artemis Server Status",
border_style="dim",
expand=False,
)
)
console.print(" [dim]πŸ’‘ To start: [bold]artemis ui[/bold] or [bold]./start.sh[/bold][/dim]\n")