More fixes to support arch distros.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user