aboutsummaryrefslogtreecommitdiffstats
path: root/jprov/runner.py
blob: 0f0a6294265607b65fb692b1e230c1053732df49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""Subprocess runner and logging utilities."""

from __future__ import annotations

import os
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Iterable, Mapping


@dataclass
class RunResult:
    cmd: list[str]
    returncode: int
    stdout: str
    stderr: str


def log_line(level: str, scope: str, message: str) -> None:
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    print(f"{ts} {level:<5} {scope}: {message}")


def run(
    cmd: Iterable[str],
    *,
    log_file: str | None = None,
    env: Mapping[str, str] | None = None,
    allow_fail: bool = False,
    dry_run: bool = False,
) -> RunResult:
    """Run a command and optionally tee output to a log file."""
    cmd_list = list(cmd)
    if dry_run:
        if log_file:
            with open(log_file, "a", encoding="utf-8") as fh:
                fh.write(f"[dry-run] {' '.join(cmd_list)}\n")
        return RunResult(cmd=cmd_list, returncode=0, stdout="", stderr="")
    env_map = os.environ.copy()
    if env:
        env_map.update(env)

    proc = subprocess.Popen(
        cmd_list,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        env=env_map,
    )
    stdout, stderr = proc.communicate()

    if log_file:
        with open(log_file, "a", encoding="utf-8") as fh:
            if stdout:
                fh.write(stdout)
            if stderr:
                fh.write(stderr)

    if proc.returncode != 0 and not allow_fail:
        detail = ""
        if stderr:
            detail = f" stderr: {stderr.strip()}"
        raise RuntimeError(
            f"command failed: {' '.join(cmd_list)} (exit {proc.returncode}){detail}"
        )

    return RunResult(cmd=cmd_list, returncode=proc.returncode, stdout=stdout, stderr=stderr)