| 79 | |
| 80 | |
| 81 | class ProcessLog: |
| 82 | def __init__(self, path: Path) -> None: |
| 83 | self.path = path |
| 84 | self.queue: queue.Queue[str] = queue.Queue() |
| 85 | self._file = path.open("wb") |
| 86 | self._thread: threading.Thread | None = None |
| 87 | |
| 88 | def attach(self, proc: subprocess.Popen[bytes]) -> None: |
| 89 | def reader() -> None: |
| 90 | assert proc.stdout is not None |
| 91 | for raw in iter(proc.stdout.readline, b""): |
| 92 | self._file.write(raw) |
| 93 | self._file.flush() |
| 94 | self.queue.put(raw.decode("utf-8", errors="replace").rstrip("\n")) |
| 95 | self._file.close() |
| 96 | |
| 97 | self._thread = threading.Thread(target=reader, daemon=True) |
| 98 | self._thread.start() |
| 99 | |
| 100 | def wait_for(self, needle: str, timeout_s: float) -> None: |
| 101 | deadline = time.time() + timeout_s |
| 102 | tail: list[str] = [] |
| 103 | while time.time() < deadline: |
| 104 | try: |
| 105 | line = self.queue.get(timeout=0.2) |
| 106 | tail.append(line) |
| 107 | if needle in line: |
| 108 | return |
| 109 | except queue.Empty: |
| 110 | pass |
| 111 | raise TimeoutError(f"timed out waiting for {needle!r}; tail={tail[-12:]}") |
| 112 | |
| 113 | |
| 114 | class PFlashDaemon: |