Wrapper around a Popen process that continuously drains stdout and stderr in background threads to prevent OS pipe buffers from filling up and blocking the child process. Captured output is logged when the process is cleaned up.
| 16 | from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS, GRACEFUL_SHUTDOWN_TIMEOUT, READER_THREAD_JOIN_TIMEOUT |
| 17 | |
| 18 | class CloudflaredProcess: |
| 19 | """ |
| 20 | Wrapper around a Popen process that continuously drains stdout and stderr |
| 21 | in background threads to prevent OS pipe buffers from filling up and |
| 22 | blocking the child process. Captured output is logged when the process |
| 23 | is cleaned up. |
| 24 | """ |
| 25 | |
| 26 | def __init__(self, cmd, allow_input, capture_output): |
| 27 | output = subprocess.PIPE if capture_output else subprocess.DEVNULL |
| 28 | stdin = subprocess.PIPE if allow_input else None |
| 29 | self.process = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=subprocess.STDOUT) |
| 30 | |
| 31 | self._capture_output = capture_output |
| 32 | self._stdout_lines = [] |
| 33 | self._threads = [] |
| 34 | if capture_output: |
| 35 | self._threads.append(self._start_reader(self.process.stdout, self._stdout_lines)) |
| 36 | |
| 37 | @staticmethod |
| 38 | def _start_reader(pipe, sink): |
| 39 | def _drain(): |
| 40 | for line in pipe: |
| 41 | sink.append(line) |
| 42 | pipe.close() |
| 43 | t = threading.Thread(target=_drain, daemon=True) |
| 44 | t.start() |
| 45 | return t |
| 46 | |
| 47 | def terminate(self): |
| 48 | """Terminate the process if it is still running.""" |
| 49 | if self.process.poll() is None: |
| 50 | self.process.terminate() |
| 51 | |
| 52 | def cleanup(self): |
| 53 | """Terminate, wait for exit, join reader threads, and log output.""" |
| 54 | self.terminate() |
| 55 | try: |
| 56 | self.process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT) |
| 57 | except subprocess.TimeoutExpired: |
| 58 | self.process.kill() |
| 59 | self.process.wait() |
| 60 | for t in self._threads: |
| 61 | t.join(timeout=READER_THREAD_JOIN_TIMEOUT) |
| 62 | if self._capture_output: |
| 63 | stdout = b"".join(self._stdout_lines).decode("utf-8", errors="replace") |
| 64 | if stdout: |
| 65 | LOGGER.info(f"cloudflared stdout:\n{stdout}") |
| 66 | |
| 67 | @property |
| 68 | def stdout_lines(self): |
| 69 | return self._stdout_lines |
| 70 | |
| 71 | # Proxy common Popen attributes so callers can still use the wrapper |
| 72 | # as if it were a Popen (e.g. send_signal, stdin, pid, returncode). |
| 73 | def __getattr__(self, name): |
| 74 | return getattr(self.process, name) |
| 75 |
no outgoing calls
no test coverage detected
searching dependent graphs…