Files
Netbox-Store/host_agent/src/netbox_store_agent/runner.py
T
MrBlake f36d6be511
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
feat: add NetBox plugin store
2026-08-24 20:51:25 +02:00

71 lines
2.4 KiB
Python

from __future__ import annotations
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol, Sequence
from .config import Config
from .errors import ExecutionError, ValidationError
@dataclass(frozen=True)
class CommandResult:
argv: tuple[str, ...]
stdout: str
stderr: str
class Runner(Protocol):
def run(self, argv: Sequence[str]) -> CommandResult: ...
class SubprocessRunner:
MAX_CAPTURE_CHARS = 16_000
def __init__(self, config: Config):
self.config = config
def run(self, argv: Sequence[str]) -> CommandResult:
command = tuple(argv)
if not command or not all(isinstance(item, str) and item for item in command):
raise ValidationError("command argv must contain non-empty strings")
if any("\x00" in item or "\r" in item or "\n" in item for item in command):
raise ValidationError("command argv contains a forbidden control character")
if not Path(command[0]).is_absolute():
raise ValidationError("command executable must be an absolute path")
environment = os.environ.copy()
for key in tuple(environment):
if key.startswith("PIP_") or key in {"PYTHONPATH", "PYTHONHOME"}:
environment.pop(key, None)
environment.update(
{
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PIP_NO_INPUT": "1",
"PYTHONNOUSERSITE": "1",
}
)
try:
completed = subprocess.run(
command,
shell=False,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
errors="replace",
timeout=self.config.commands.command_timeout_seconds,
check=False,
env=environment,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise ExecutionError(f"command could not complete: {command[0]}") from exc
stdout = completed.stdout[-self.MAX_CAPTURE_CHARS :]
stderr = completed.stderr[-self.MAX_CAPTURE_CHARS :]
if completed.returncode != 0:
raise ExecutionError(
f"command failed with exit code {completed.returncode}: {command[0]}"
)
return CommandResult(command, stdout, stderr)