Spaces:
Configuration error
Configuration error
Download utils/subprocess_runner.py from varshithreddy1807/aria: direct link, hf CLI and curl.
- Browser
- Download file 9.24 kB
-
https://huggingface.co/spaces/varshithreddy1807/aria/resolve/main/utils/subprocess_runner.py
- Command line
-
hf download hf://spaces/varshithreddy1807/aria/utils/subprocess_runner.py
-
curl -L -o subprocess_runner.py https://huggingface.co/spaces/varshithreddy1807/aria/resolve/main/utils/subprocess_runner.py
9.24 kB
| """Safe subprocess execution utility. | |
| Provides robust, secure, and standardized process execution across the platform. | |
| Features: | |
| - Mandatory non-shell execution (`shell=False` enforced) | |
| - Enforced operation-specific timeouts | |
| - Automated secret redaction in logs, outputs, and exceptions | |
| - Non-interactive environment hardening for Git and external tools | |
| - Structured error handling via SafeSubprocessError | |
| """ | |
| import logging | |
| import os | |
| import subprocess | |
| import threading | |
| from typing import Dict, Optional, Sequence, Union | |
| logger = logging.getLogger(__name__) | |
| # Standard timeout categories (in seconds) | |
| SHORT_GIT_TIMEOUT = 15.0 | |
| INSPECTION_TIMEOUT = 30.0 | |
| HISTORY_ANALYSIS_TIMEOUT = 120.0 | |
| CLONE_TIMEOUT = 300.0 | |
| DEFAULT_TIMEOUT = 30.0 | |
| # Bounded output capture limit (16 KiB per stream to keep memory low on 512MB instances) | |
| DEFAULT_MAX_CAPTURE_BYTES = 16384 | |
| # Non-interactive Git environment overrides | |
| SAFE_GIT_ENV: Dict[str, str] = { | |
| "GIT_TERMINAL_PROMPT": "0", | |
| "GIT_ASKPASS": "", | |
| "GCM_INTERACTIVE": "never", | |
| "GIT_CONFIG_NOSYSTEM": "1", | |
| "LC_ALL": "C", | |
| } | |
| class SafeSubprocessError(RuntimeError): | |
| """Raised when a safe subprocess execution fails or times out.""" | |
| def __init__( | |
| self, | |
| cmd: Sequence[str], | |
| returncode: int, | |
| stdout: str = "", | |
| stderr: str = "", | |
| timed_out: bool = False, | |
| message: Optional[str] = None, | |
| ) -> None: | |
| self.cmd = list(cmd) | |
| self.returncode = returncode | |
| self.stdout = stdout | |
| self.stderr = stderr | |
| self.timed_out = timed_out | |
| if message is None: | |
| if timed_out: | |
| message = f"Command '{' '.join(self.cmd)}' timed out." | |
| else: | |
| message = f"Command '{' '.join(self.cmd)}' failed with exit code {returncode}: {stderr.strip()}" | |
| super().__init__(message) | |
| def redact_text(text: str, secrets: Sequence[str]) -> str: | |
| """Redacts all specified secrets from text.""" | |
| if not text or not secrets: | |
| return text | |
| redacted = text | |
| for secret in secrets: | |
| if secret and len(secret) > 0: | |
| redacted = redacted.replace(secret, "[REDACTED]") | |
| return redacted | |
| def _drain_pipe_bounded(pipe, max_bytes: int = DEFAULT_MAX_CAPTURE_BYTES) -> bytes: | |
| """Drain an OS pipe to completion while retaining at most max_bytes in memory. | |
| Discards any trailing data once max_bytes is reached to prevent high heap usage, | |
| while still reading to EOF to prevent the child process from blocking on full OS pipe buffers. | |
| """ | |
| if pipe is None: | |
| return b"" | |
| chunks = [] | |
| total_len = 0 | |
| try: | |
| while True: | |
| chunk = pipe.read(4096) | |
| if not chunk: | |
| break | |
| if total_len < max_bytes: | |
| to_take = min(len(chunk), max_bytes - total_len) | |
| chunks.append(chunk[:to_take]) | |
| total_len += to_take | |
| except Exception: | |
| pass | |
| finally: | |
| try: | |
| pipe.close() | |
| except Exception: | |
| pass | |
| return b"".join(chunks) | |
| def run_safe_command( | |
| cmd: Sequence[str], | |
| *, | |
| cwd: Optional[Union[str, os.PathLike]] = None, | |
| timeout: float = DEFAULT_TIMEOUT, | |
| env: Optional[Dict[str, str]] = None, | |
| secrets: Sequence[str] = (), | |
| check: bool = False, | |
| capture_output: bool = True, | |
| text: bool = True, | |
| max_output_length: int = 10000, | |
| max_capture_bytes: int = DEFAULT_MAX_CAPTURE_BYTES, | |
| ) -> subprocess.CompletedProcess: | |
| """Executes a subprocess safely with mandatory timeout, non-shell execution, and bounded output memory. | |
| Args: | |
| cmd: Command and arguments list. Must be a sequence of strings. | |
| cwd: Directory in which to execute the command. | |
| timeout: Execution timeout in seconds. | |
| env: Additional environment variables to set. | |
| secrets: List of sensitive strings to redact from logs, errors, and output. | |
| check: If True, raises SafeSubprocessError on non-zero exit code. | |
| capture_output: If True, capture stdout and stderr up to max_capture_bytes. | |
| text: If True, decode output as utf-8 text. | |
| max_output_length: Maximum length of stdout/stderr kept in exceptions. | |
| max_capture_bytes: Maximum bytes captured per stream (default 16 KiB). | |
| Returns: | |
| subprocess.CompletedProcess instance with sanitized stdout/stderr. | |
| Raises: | |
| SafeSubprocessError: If execution times out, executable is missing, or check=True fails. | |
| """ | |
| if not isinstance(cmd, (list, tuple)): | |
| raise TypeError( | |
| "Command must be a list or tuple of strings (non-shell execution)." | |
| ) | |
| cmd_list = [str(arg) for arg in cmd] | |
| # Build hardened environment | |
| merged_env = dict(os.environ) | |
| merged_env.update(SAFE_GIT_ENV) | |
| if env: | |
| merged_env.update(env) | |
| stdout_buf = [] | |
| stderr_buf = [] | |
| out_thread = None | |
| err_thread = None | |
| try: | |
| proc = subprocess.Popen( | |
| cmd_list, | |
| cwd=cwd, | |
| shell=False, # Strictly non-shell execution | |
| stdin=subprocess.DEVNULL, | |
| stdout=subprocess.PIPE if capture_output else None, | |
| stderr=subprocess.PIPE if capture_output else None, | |
| env=merged_env, | |
| ) | |
| except FileNotFoundError as exc: | |
| safe_cmd = [redact_text(arg, secrets) for arg in cmd_list] | |
| raise SafeSubprocessError( | |
| cmd=safe_cmd, | |
| returncode=-1, | |
| stdout="", | |
| stderr=f"Executable not found: {exc.filename}", | |
| timed_out=False, | |
| message=f"Executable not found for command: {' '.join(safe_cmd)}", | |
| ) from exc | |
| except Exception as exc: | |
| safe_cmd = [redact_text(arg, secrets) for arg in cmd_list] | |
| raise SafeSubprocessError( | |
| cmd=safe_cmd, | |
| returncode=-1, | |
| stdout="", | |
| stderr=str(exc), | |
| timed_out=False, | |
| message=f"Failed to execute command '{' '.join(safe_cmd)}': {exc}", | |
| ) from exc | |
| if capture_output: | |
| out_thread = threading.Thread( | |
| target=lambda: stdout_buf.append( | |
| _drain_pipe_bounded(proc.stdout, max_capture_bytes) | |
| ), | |
| daemon=True, | |
| ) | |
| err_thread = threading.Thread( | |
| target=lambda: stderr_buf.append( | |
| _drain_pipe_bounded(proc.stderr, max_capture_bytes) | |
| ), | |
| daemon=True, | |
| ) | |
| out_thread.start() | |
| err_thread.start() | |
| try: | |
| returncode = proc.wait(timeout=timeout) | |
| except subprocess.TimeoutExpired as exc: | |
| try: | |
| proc.kill() | |
| except Exception: | |
| pass | |
| try: | |
| proc.wait(timeout=2.0) | |
| except Exception: | |
| pass | |
| if out_thread: | |
| out_thread.join(timeout=1.0) | |
| if err_thread: | |
| err_thread.join(timeout=1.0) | |
| stdout_raw = stdout_buf[0] if stdout_buf else b"" | |
| stderr_raw = stderr_buf[0] if stderr_buf else b"" | |
| stdout_str = ( | |
| redact_text(stdout_raw.decode("utf-8", errors="replace"), secrets) | |
| if text | |
| else "" | |
| ) | |
| stderr_str = ( | |
| redact_text(stderr_raw.decode("utf-8", errors="replace"), secrets) | |
| if text | |
| else "" | |
| ) | |
| safe_cmd = [redact_text(arg, secrets) for arg in cmd_list] | |
| raise SafeSubprocessError( | |
| cmd=safe_cmd, | |
| returncode=-1, | |
| stdout=stdout_str[:max_output_length], | |
| stderr=stderr_str[:max_output_length], | |
| timed_out=True, | |
| message=f"Command '{' '.join(safe_cmd)}' timed out after {timeout}s", | |
| ) from exc | |
| if out_thread: | |
| out_thread.join(timeout=1.0) | |
| if err_thread: | |
| err_thread.join(timeout=1.0) | |
| stdout_raw = stdout_buf[0] if stdout_buf else b"" | |
| stderr_raw = stderr_buf[0] if stderr_buf else b"" | |
| # Redact output if text | |
| if text: | |
| stdout_clean = redact_text( | |
| stdout_raw.decode("utf-8", errors="replace"), secrets | |
| ) | |
| stderr_clean = redact_text( | |
| stderr_raw.decode("utf-8", errors="replace"), secrets | |
| ) | |
| completed = subprocess.CompletedProcess( | |
| args=[redact_text(arg, secrets) for arg in cmd_list], | |
| returncode=returncode, | |
| stdout=stdout_clean, | |
| stderr=stderr_clean, | |
| ) | |
| else: | |
| completed = subprocess.CompletedProcess( | |
| args=[redact_text(arg, secrets) for arg in cmd_list], | |
| returncode=returncode, | |
| stdout=stdout_raw, | |
| stderr=stderr_raw, | |
| ) | |
| if check and completed.returncode != 0: | |
| safe_cmd = [redact_text(arg, secrets) for arg in cmd_list] | |
| raise SafeSubprocessError( | |
| cmd=safe_cmd, | |
| returncode=completed.returncode, | |
| stdout=completed.stdout[:max_output_length] | |
| if isinstance(completed.stdout, str) | |
| else "", | |
| stderr=completed.stderr[:max_output_length] | |
| if isinstance(completed.stderr, str) | |
| else "", | |
| timed_out=False, | |
| ) | |
| return completed | |