Improvements to try resolve the issues with login checksums.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime
|
||||
import json
|
||||
@@ -16,6 +17,44 @@ from typing import Callable, Mapping, Sequence, TextIO
|
||||
|
||||
|
||||
OutputLineFilter = Callable[[str], str | None]
|
||||
MAX_CAPTURE_CHARACTERS = 1024 * 1024
|
||||
|
||||
|
||||
class BoundedTextCapture:
|
||||
"""Retain the newest subprocess output without unbounded memory growth."""
|
||||
|
||||
def __init__(self, max_characters: int = MAX_CAPTURE_CHARACTERS) -> None:
|
||||
if max_characters <= 0:
|
||||
raise ValueError("capture size must be positive")
|
||||
self._max_characters = max_characters
|
||||
self._parts: deque[str] = deque()
|
||||
self._characters = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def append(self, value: str) -> None:
|
||||
if not value:
|
||||
return
|
||||
with self._lock:
|
||||
if len(value) >= self._max_characters:
|
||||
self._parts.clear()
|
||||
self._parts.append(value[-self._max_characters:])
|
||||
self._characters = self._max_characters
|
||||
return
|
||||
self._parts.append(value)
|
||||
self._characters += len(value)
|
||||
while self._characters > self._max_characters:
|
||||
overflow = self._characters - self._max_characters
|
||||
oldest = self._parts[0]
|
||||
if len(oldest) <= overflow:
|
||||
self._parts.popleft()
|
||||
self._characters -= len(oldest)
|
||||
else:
|
||||
self._parts[0] = oldest[overflow:]
|
||||
self._characters -= overflow
|
||||
|
||||
def getvalue(self) -> str:
|
||||
with self._lock:
|
||||
return "".join(self._parts)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -103,8 +142,8 @@ class CommandRunner:
|
||||
outcome = CommandOutcome(
|
||||
argv=arguments,
|
||||
returncode=completed.returncode,
|
||||
stdout=completed.stdout,
|
||||
stderr=completed.stderr,
|
||||
stdout=_bounded_text(completed.stdout),
|
||||
stderr=_bounded_text(completed.stderr),
|
||||
duration_seconds=time.monotonic() - started,
|
||||
timed_out=False,
|
||||
dry_run=False,
|
||||
@@ -113,8 +152,8 @@ class CommandRunner:
|
||||
outcome = CommandOutcome(
|
||||
argv=arguments,
|
||||
returncode=None,
|
||||
stdout=_as_text(exc.stdout),
|
||||
stderr=_as_text(exc.stderr),
|
||||
stdout=_bounded_text(_as_text(exc.stdout)),
|
||||
stderr=_bounded_text(_as_text(exc.stderr)),
|
||||
duration_seconds=time.monotonic() - started,
|
||||
timed_out=True,
|
||||
dry_run=False,
|
||||
@@ -144,12 +183,12 @@ class CommandRunner:
|
||||
bufsize=1,
|
||||
start_new_session=True,
|
||||
)
|
||||
stdout: list[str] = []
|
||||
stderr: list[str] = []
|
||||
stdout = BoundedTextCapture()
|
||||
stderr = BoundedTextCapture()
|
||||
|
||||
def relay(
|
||||
stream: TextIO,
|
||||
capture: list[str],
|
||||
capture: BoundedTextCapture,
|
||||
destination: TextIO,
|
||||
) -> None:
|
||||
try:
|
||||
@@ -196,8 +235,8 @@ class CommandRunner:
|
||||
return CommandOutcome(
|
||||
argv=arguments,
|
||||
returncode=None if timed_out else returncode,
|
||||
stdout="".join(stdout),
|
||||
stderr="".join(stderr),
|
||||
stdout=stdout.getvalue(),
|
||||
stderr=stderr.getvalue(),
|
||||
duration_seconds=time.monotonic() - started,
|
||||
timed_out=timed_out,
|
||||
dry_run=False,
|
||||
@@ -228,6 +267,10 @@ def _as_text(value: bytes | str | None) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def _bounded_text(value: str) -> str:
|
||||
return value[-MAX_CAPTURE_CHARACTERS:]
|
||||
|
||||
|
||||
def _terminate_process_group(
|
||||
process: subprocess.Popen[str],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user