| """ |
| Atom CLI Skill Wrapper - Subprocess execution for CLI commands. |
| |
| Provides safe subprocess execution wrapper for Atom CLI commands invoked |
| from community skills. Handles timeouts, error handling, and logging. |
| |
| Reference: Phase 25 Plan 02 - Subprocess Execution Wrapper |
| """ |
|
|
| import logging |
| import re |
| import subprocess |
| import time |
| from typing import Dict, Any, Optional, List |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def execute_atom_cli_command(command: str, args: Optional[List[str]] = None) -> Dict[str, Any]: |
| """ |
| Execute Atom CLI command via subprocess. |
| |
| Provides safe command execution with timeout enforcement, structured |
| output, and comprehensive error handling for skill integration. |
| |
| Args: |
| command: CLI command name (e.g., "daemon", "status", "stop") |
| args: Optional list of command arguments (e.g., ["--port", "3000"]) |
| |
| Returns: |
| Dict with keys: |
| - success: bool (True if returncode == 0) |
| - stdout: str (captured standard output) |
| - stderr: str (captured standard error) |
| - returncode: int (process exit code) |
| |
| Raises: |
| No exceptions raised - all errors captured in return dict |
| |
| Examples: |
| >>> result = execute_atom_cli_command("status") |
| >>> if result["success"]: |
| ... print(result["stdout"]) |
| ... else: |
| ... print(f"Error: {result['stderr']}") |
| |
| >>> result = execute_atom_cli_command("daemon", ["--port", "3000"]) |
| >>> print(f"PID: {result['returncode']}") |
| |
| Notes: |
| - 30 second timeout prevents hanging commands |
| - subprocess.TimeoutExpired caught and returned as error |
| - Generic exceptions caught and returned with stderr message |
| """ |
| try: |
| |
| cmd = ["atom-os", command] |
| if args: |
| cmd.extend(args) |
|
|
| logger.info(f"Executing: atom-os {command} {' '.join(args) if args else ''}") |
|
|
| |
| result = subprocess.run( |
| cmd, |
| capture_output=True, |
| text=True, |
| timeout=30 |
| ) |
|
|
| return { |
| "success": result.returncode == 0, |
| "stdout": result.stdout, |
| "stderr": result.stderr, |
| "returncode": result.returncode |
| } |
|
|
| except subprocess.TimeoutExpired: |
| logger.error(f"Command 'atom-os {command}' timed out after 30 seconds") |
| return { |
| "success": False, |
| "stdout": "", |
| "stderr": "Command timed out after 30 seconds", |
| "returncode": -1 |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to execute 'atom-os {command}': {e}") |
| return { |
| "success": False, |
| "stdout": "", |
| "stderr": str(e), |
| "returncode": -1 |
| } |
|
|
|
|
| def is_daemon_running() -> bool: |
| """ |
| Check if Atom daemon is currently running. |
| |
| Executes status command and parses output for running state. |
| |
| Returns: |
| True if daemon is running, False otherwise |
| |
| Examples: |
| >>> if is_daemon_running(): |
| ... print("Daemon is active") |
| ... else: |
| ... print("Daemon is stopped") |
| |
| Notes: |
| - Parses status output for "RUNNING" status string |
| - Returns False on command failure or parse errors |
| """ |
| try: |
| result = execute_atom_cli_command("status") |
|
|
| if not result["success"]: |
| return False |
|
|
| |
| |
| status_match = re.search(r"Status:\s*(\w+)", result["stdout"], re.IGNORECASE) |
| if status_match: |
| status = status_match.group(1).upper() |
| return status == "RUNNING" |
|
|
| return False |
|
|
| except Exception as e: |
| logger.error(f"Failed to check daemon status: {e}") |
| return False |
|
|
|
|
| def get_daemon_pid() -> Optional[int]: |
| """ |
| Get daemon process ID from status output. |
| |
| Executes status command and extracts PID using regex. |
| |
| Returns: |
| Daemon PID as int if running, None if not found |
| |
| Examples: |
| >>> pid = get_daemon_pid() |
| >>> if pid: |
| ... print(f"Daemon PID: {pid}") |
| |
| Notes: |
| - Uses regex pattern: r\"PID:\\\\s+(\\\\d+)\" |
| - Returns None if daemon not running or PID not found |
| """ |
| try: |
| result = execute_atom_cli_command("status") |
|
|
| if not result["success"]: |
| return None |
|
|
| |
| |
| pid_match = re.search(r"PID:\s+(\d+)", result["stdout"]) |
| if pid_match: |
| return int(pid_match.group(1)) |
|
|
| return None |
|
|
| except Exception as e: |
| logger.error(f"Failed to get daemon PID: {e}") |
| return None |
|
|
|
|
| def wait_for_daemon_ready(max_wait: int = 10) -> bool: |
| """ |
| Poll daemon status until running or timeout. |
| |
| Prevents race conditions after starting daemon by waiting for |
| the daemon to initialize and become ready. |
| |
| Args: |
| max_wait: Maximum seconds to wait (default: 10) |
| |
| Returns: |
| True if daemon ready, False if timeout exceeded |
| |
| Examples: |
| >>> execute_atom_cli_command("daemon", ["--port", "3000"]) |
| >>> if wait_for_daemon_ready(max_wait=5): |
| ... print("Daemon started successfully") |
| ... else: |
| ... print("Daemon failed to start") |
| |
| Notes: |
| - Polls every 0.5 seconds |
| - Logs progress at each attempt |
| - Prevents Pitfall 4 from RESEARCH.md (race conditions) |
| """ |
| start_time = time.time() |
| poll_interval = 0.5 |
|
|
| while True: |
| elapsed = time.time() - start_time |
|
|
| if is_daemon_running(): |
| logger.info(f"Daemon ready after {elapsed:.1f}s") |
| return True |
|
|
| if elapsed >= max_wait: |
| logger.warning(f"Daemon not ready after {max_wait}s timeout") |
| return False |
|
|
| logger.info(f"Waiting for daemon... {elapsed:.1f}s") |
| time.sleep(poll_interval) |
|
|
|
|
| def mock_daemon_response( |
| stdout: str = "", |
| stderr: str = "", |
| returncode: int = 0 |
| ) -> Dict[str, Any]: |
| """ |
| Create mock daemon response for testing. |
| |
| Helper function for unit tests to simulate command execution |
| results without actually running subprocess. |
| |
| Args: |
| stdout: Mock standard output |
| stderr: Mock standard error |
| returncode: Mock return code (0 for success) |
| |
| Returns: |
| Dict matching execute_atom_cli_command format |
| |
| Examples: |
| >>> mock = mock_daemon_response( |
| ... stdout="Status: RUNNING\\nPID: 12345", |
| ... returncode=0 |
| ... ) |
| >>> assert mock["success"] == True |
| """ |
| return { |
| "success": returncode == 0, |
| "stdout": stdout, |
| "stderr": stderr, |
| "returncode": returncode |
| } |
|
|
|
|
| def build_command_args( |
| port: Optional[int] = None, |
| host: Optional[str] = None, |
| workers: Optional[int] = None, |
| host_mount: bool = False, |
| dev: bool = False, |
| foreground: bool = False |
| ) -> List[str]: |
| """ |
| Build command arguments list for daemon/start commands. |
| |
| Helper function to construct argument list from keyword parameters. |
| |
| Args: |
| port: Port number for web server |
| host: Host address to bind to |
| workers: Number of worker processes |
| host_mount: Enable host filesystem mount |
| dev: Enable development mode |
| foreground: Run in foreground (not daemon mode) |
| |
| Returns: |
| List of command arguments (e.g., ["--port", "8000", "--dev"]) |
| |
| Examples: |
| >>> args = build_command_args(port=3000, dev=True) |
| >>> execute_atom_cli_command("daemon", args) |
| """ |
| args = [] |
|
|
| if port is not None: |
| args.extend(["--port", str(port)]) |
|
|
| if host is not None: |
| args.extend(["--host", host]) |
|
|
| if workers is not None: |
| args.extend(["--workers", str(workers)]) |
|
|
| if host_mount: |
| args.append("--host-mount") |
|
|
| if dev: |
| args.append("--dev") |
|
|
| if foreground: |
| args.append("--foreground") |
|
|
| return args |
|
|