Download apps/core/infrastructure/subprocess_runner.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 6.05 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/infrastructure/subprocess_runner.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/core/infrastructure/subprocess_runner.py
-
curl -L -o subprocess_runner.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/core/infrastructure/subprocess_runner.py
6.05 kB
| """Module for subprocess runner.""" | |
| from __future__ import annotations | |
| import subprocess | |
| from dataclasses import dataclass | |
| from typing import Any | |
| from apps.core.exceptions import ExternalServiceError | |
| from apps.core.security.scrub import scrub_text | |
| class SubprocessOutput: | |
| stdout: str | |
| stderr: str | |
| returncode: int | |
| class SubprocessRunner: | |
| def __init__(self, *, allowed_programs: set[str] | None = None, max_output_chars: int = 20_000) -> None: | |
| self._allowed_programs = allowed_programs | |
| self._max_output_chars = int(max_output_chars) if max_output_chars is not None else 20_000 | |
| def _truncate(self, value: str) -> str: | |
| if not value: | |
| return "" | |
| if self._max_output_chars <= 0: | |
| return "" | |
| if len(value) <= self._max_output_chars: | |
| return value | |
| return value[: self._max_output_chars] + "...(truncated)" | |
| def _validate_args(self, args: list[str]) -> None: | |
| if not isinstance(args, list) or not args or not all(isinstance(a, str) for a in args): | |
| raise ExternalServiceError(message="外部命令参数无效", code="SUBPROCESS_INVALID_ARGS", errors={}) | |
| program = str(args[0] or "").strip() | |
| if not program: | |
| raise ExternalServiceError(message="外部命令参数无效", code="SUBPROCESS_INVALID_ARGS", errors={}) | |
| if self._allowed_programs is not None and program not in self._allowed_programs: | |
| raise ExternalServiceError( | |
| message="外部命令不允许", | |
| code="SUBPROCESS_NOT_ALLOWED", | |
| errors={"command": scrub_text(program)}, | |
| ) | |
| def run( | |
| self, | |
| *, | |
| args: list[str], | |
| timeout_seconds: float | None = None, | |
| check: bool = False, | |
| text: bool = True, | |
| ) -> SubprocessOutput: | |
| self._validate_args(args) | |
| try: | |
| cp = subprocess.run( # 安全:args 已通过 _validate_args 白名单校验,shell=False | |
| args, | |
| check=check, | |
| capture_output=True, | |
| text=text, | |
| timeout=timeout_seconds, | |
| ) | |
| except subprocess.CalledProcessError as e: | |
| stdout = e.stdout or "" | |
| stderr = e.stderr or "" | |
| if isinstance(stdout, bytes): | |
| stdout = stdout.decode("utf-8", errors="replace") | |
| if isinstance(stderr, bytes): | |
| stderr = stderr.decode("utf-8", errors="replace") | |
| raise ExternalServiceError( | |
| message="外部命令执行失败", | |
| code="SUBPROCESS_NONZERO_EXIT", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "returncode": int(getattr(e, "returncode", 0) or 0), | |
| "stdout": self._truncate(scrub_text(str(stdout))), | |
| "stderr": self._truncate(scrub_text(str(stderr))), | |
| }, | |
| ) from e | |
| except subprocess.TimeoutExpired as e: | |
| raise ExternalServiceError( | |
| message="外部命令执行超时", | |
| code="SUBPROCESS_TIMEOUT", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "timeout_seconds": timeout_seconds, | |
| "detail": self._truncate(scrub_text(str(e))), | |
| }, | |
| ) from e | |
| except FileNotFoundError as e: | |
| raise ExternalServiceError( | |
| message="外部命令不存在", | |
| code="SUBPROCESS_NOT_FOUND", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "detail": self._truncate(scrub_text(str(e))), | |
| }, | |
| ) from e | |
| except Exception as e: | |
| raise ExternalServiceError( | |
| message="外部命令执行失败", | |
| code="SUBPROCESS_FAILED", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "detail": self._truncate(scrub_text(str(e))), | |
| }, | |
| ) from e | |
| stdout = cp.stdout or "" | |
| stderr = cp.stderr or "" | |
| if isinstance(stdout, bytes): | |
| stdout = stdout.decode("utf-8", errors="replace") | |
| if isinstance(stderr, bytes): | |
| stderr = stderr.decode("utf-8", errors="replace") | |
| return SubprocessOutput( | |
| stdout=self._truncate(scrub_text(str(stdout))), | |
| stderr=self._truncate(scrub_text(str(stderr))), | |
| returncode=int(cp.returncode or 0), | |
| ) | |
| def popen(self, *, args: list[str], **kwargs: Any) -> subprocess.Popen[Any]: | |
| self._validate_args(args) | |
| if "shell" in kwargs and kwargs.get("shell"): | |
| raise ExternalServiceError( | |
| message="外部命令执行参数不安全", | |
| code="SUBPROCESS_UNSAFE_OPTIONS", | |
| errors={"command": scrub_text(str(args[0] if args else "")), "option": "shell"}, | |
| ) | |
| try: | |
| return subprocess.Popen( | |
| args, **kwargs | |
| ) # 安全:args 已通过 _validate_args 白名单校验,且明确拒绝 shell=True | |
| except FileNotFoundError as e: | |
| raise ExternalServiceError( | |
| message="外部命令不存在", | |
| code="SUBPROCESS_NOT_FOUND", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "detail": self._truncate(scrub_text(str(e))), | |
| }, | |
| ) from e | |
| except Exception as e: | |
| raise ExternalServiceError( | |
| message="外部命令执行失败", | |
| code="SUBPROCESS_FAILED", | |
| errors={ | |
| "command": scrub_text(str(args[0] if args else "")), | |
| "detail": self._truncate(scrub_text(str(e))), | |
| }, | |
| ) from e | |