More fixes to support arch distros.

This commit is contained in:
2026-08-28 20:15:32 +02:00
parent d2c66b6316
commit b37b5291ae
5 changed files with 309 additions and 219 deletions

View File

@@ -7,11 +7,15 @@ from datetime import datetime
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import threading
import time
from typing import Mapping, Sequence, TextIO
from typing import Callable, Mapping, Sequence, TextIO
OutputLineFilter = Callable[[str], str | None]
@dataclass(frozen=True)
@@ -53,6 +57,7 @@ class CommandRunner:
timeout_seconds: float = 300.0,
check: bool = True,
stream_output: bool = False,
output_line_filter: OutputLineFilter | None = None,
) -> CommandOutcome:
arguments = [str(value) for value in argv]
if not arguments:
@@ -81,6 +86,7 @@ class CommandRunner:
environment=environment,
timeout_seconds=timeout_seconds,
started=started,
output_line_filter=output_line_filter,
)
else:
try:
@@ -126,6 +132,7 @@ class CommandRunner:
environment: Mapping[str, str],
timeout_seconds: float,
started: float,
output_line_filter: OutputLineFilter | None,
) -> CommandOutcome:
process = subprocess.Popen(
arguments,
@@ -135,6 +142,7 @@ class CommandRunner:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=1,
start_new_session=True,
)
stdout: list[str] = []
stderr: list[str] = []
@@ -144,13 +152,21 @@ class CommandRunner:
capture: list[str],
destination: TextIO,
) -> None:
while True:
line = stream.readline()
if not line:
return
capture.append(line)
destination.write(line)
destination.flush()
try:
while True:
line = stream.readline()
if not line:
return
if output_line_filter is not None:
line = output_line_filter(line)
if line is None:
continue
capture.append(line)
destination.write(line)
destination.flush()
except (OSError, ValueError):
# Shutdown can close a pipe while its relay thread is leaving readline().
return
assert process.stdout is not None
assert process.stderr is not None
@@ -171,12 +187,12 @@ class CommandRunner:
returncode = process.wait(timeout=timeout_seconds)
except subprocess.TimeoutExpired:
timed_out = True
process.kill()
returncode = process.wait()
stdout_thread.join()
stderr_thread.join()
process.stdout.close()
process.stderr.close()
returncode = _terminate_process_group(process)
except BaseException:
_terminate_process_group(process)
_finish_stream_relays(process, stdout_thread, stderr_thread)
raise
_finish_stream_relays(process, stdout_thread, stderr_thread)
return CommandOutcome(
argv=arguments,
returncode=None if timed_out else returncode,
@@ -210,3 +226,49 @@ def _as_text(value: bytes | str | None) -> str:
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value
def _terminate_process_group(
process: subprocess.Popen[str],
*,
grace_seconds: float = 5.0,
) -> int | None:
"""Stop the complete child session, allowing Proton a short clean exit."""
if process.poll() is not None:
return process.returncode
try:
os.killpg(process.pid, signal.SIGTERM)
except ProcessLookupError:
return process.poll()
try:
return process.wait(timeout=grace_seconds)
except subprocess.TimeoutExpired:
pass
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
return process.poll()
try:
return process.wait(timeout=grace_seconds)
except subprocess.TimeoutExpired:
return process.poll()
def _finish_stream_relays(
process: subprocess.Popen[str],
stdout_thread: threading.Thread,
stderr_thread: threading.Thread,
) -> None:
"""Bound relay cleanup so a second interrupt can never be required."""
stdout_thread.join(timeout=2.0)
stderr_thread.join(timeout=2.0)
for stream in (process.stdout, process.stderr):
if stream is not None:
try:
stream.close()
except OSError:
pass
stdout_thread.join(timeout=0.25)
stderr_thread.join(timeout=0.25)