""" 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: # Build command list cmd = ["atom-os", command] if args: cmd.extend(args) logger.info(f"Executing: atom-os {command} {' '.join(args) if args else ''}") # Execute with timeout result = subprocess.run( cmd, capture_output=True, text=True, timeout=30 # 30 second timeout per plan requirement ) 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 # Parse status output for running state # Expected format: "Status: RUNNING" or "Status: NOT RUNNING" 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 # Extract PID from status output # Expected format: "PID: 12345" 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 # seconds 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