70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Sequence
|
|
|
|
from .redaction import redact_text
|
|
|
|
|
|
class CommandExecutionError(RuntimeError):
|
|
def __init__(self, message: str, *, output: str = ""):
|
|
super().__init__(message)
|
|
self.output = redact_text(output)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CommandResult:
|
|
returncode: int
|
|
stdout: str
|
|
stderr: str
|
|
|
|
@property
|
|
def output(self) -> str:
|
|
return redact_text("\n".join(part for part in (self.stdout, self.stderr) if part))
|
|
|
|
|
|
class SubprocessRunner:
|
|
"""Injectable no-shell subprocess boundary."""
|
|
|
|
def run(
|
|
self,
|
|
argv: Sequence[str],
|
|
*,
|
|
timeout: int,
|
|
cwd: Path | None = None,
|
|
input_text: str | None = None,
|
|
) -> CommandResult:
|
|
if not argv or any(not isinstance(arg, str) or "\x00" in arg for arg in argv):
|
|
raise CommandExecutionError("Refusing to execute invalid argv.")
|
|
env = os.environ.copy()
|
|
env.update({"PIP_NO_INPUT": "1", "PYTHONUNBUFFERED": "1"})
|
|
try:
|
|
completed = subprocess.run(
|
|
list(argv),
|
|
cwd=str(cwd) if cwd else None,
|
|
env=env,
|
|
shell=False,
|
|
check=False,
|
|
text=True,
|
|
input=input_text,
|
|
stdin=subprocess.DEVNULL if input_text is None else None,
|
|
capture_output=True,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
output = "\n".join(
|
|
str(value) for value in (getattr(exc, "stdout", ""), getattr(exc, "stderr", "")) if value
|
|
)
|
|
raise CommandExecutionError("Command timed out.", output=output) from exc
|
|
except OSError as exc:
|
|
raise CommandExecutionError(f"Unable to start command: {type(exc).__name__}") from exc
|
|
result = CommandResult(completed.returncode, completed.stdout or "", completed.stderr or "")
|
|
if completed.returncode != 0:
|
|
raise CommandExecutionError(
|
|
f"Command failed with exit code {completed.returncode}.", output=result.output
|
|
)
|
|
return result
|