import asyncio import json import subprocess from collections.abc import Sequence from typing import Any def run(cmd: Sequence[str], *, timeout: float | None = None) -> tuple[int, str, str]: """Run a command synchronously and capture its output. Args: cmd: program and arguments. timeout: seconds before the child is killed, or None to wait. Returns: (returncode, stdout, stderr), both decoded. Spawn failures (missing binary, other OSError) and timeouts are reported as returncode -1 with the reason in stderr instead of raising. """ try: proc = subprocess.run(cmd, capture_output=True, timeout=timeout) except FileNotFoundError: return -1, "", f"{cmd[0]} not found in PATH" except subprocess.TimeoutExpired: return -1, "", f"{cmd[0]} timed out" except (OSError, subprocess.SubprocessError) as e: return -1, "", str(e)[:200] return proc.returncode, proc.stdout.decode(errors="replace"), proc.stderr.decode(errors="replace") async def run_async(cmd: Sequence[str], *, timeout: float | None = None) -> tuple[int, str, str]: """Run a command asynchronously and capture its output. The child is killed when the timeout expires. Args: cmd: program and arguments. timeout: seconds before the child is killed, or None to wait. Returns: (returncode, stdout, stderr), both decoded. Spawn failures (missing binary, other OSError) and timeouts are reported as returncode -1 with the reason in stderr instead of raising. """ try: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) except FileNotFoundError: return -1, "", f"{cmd[0]} not found in PATH" except OSError as e: return -1, "", str(e)[:200] try: if timeout is None: out, err = await proc.communicate() else: out, err = await asyncio.wait_for(proc.communicate(), timeout) except TimeoutError: try: proc.kill() except ProcessLookupError: pass return -1, "", f"{cmd[0]} timed out" return proc.returncode or 0, out.decode(errors="replace"), err.decode(errors="replace") def run_json(cmd: Sequence[str], *, timeout: float | None = None) -> tuple[Any, str]: """Run a command synchronously and parse its stdout as JSON. Args: cmd: program and arguments. timeout: seconds before the child is killed, or None to wait. Returns: (parsed JSON, "") on success, else (None, error description). """ rc, out, err = run(cmd, timeout=timeout) if rc != 0: return None, err or f"{cmd[0]} failed (rc={rc})" try: return json.loads(out), "" except ValueError: return None, f"{cmd[0]} returned invalid JSON" async def run_json_async(cmd: Sequence[str], *, timeout: float | None = None) -> tuple[Any, str]: """Run a command asynchronously and parse its stdout as JSON. Args: cmd: program and arguments. timeout: seconds before the child is killed, or None to wait. Returns: (parsed JSON, "") on success, else (None, error description). """ rc, out, err = await run_async(cmd, timeout=timeout) if rc != 0: return None, err or f"{cmd[0]} failed (rc={rc})" try: return json.loads(out), "" except ValueError: return None, f"{cmd[0]} returned invalid JSON"