|
8 | 8 |
|
9 | 9 |
|
10 | 10 | class GrpcServerProcess: |
| 11 | + _SERVER_STARTUP_TIMEOUT = 60 |
| 12 | + |
11 | 13 | def __init__(self, config_file_path): |
12 | 14 | server_exe = self._get_grpc_server_exe() |
13 | | - self._proc = subprocess.Popen([str(server_exe), config_file_path], stdout=subprocess.PIPE) |
14 | | - |
15 | | - # Read/parse output until we find the port number or the process exits; discard the rest. |
| 15 | + self._proc = subprocess.Popen( |
| 16 | + [str(server_exe), config_file_path], |
| 17 | + stdout=subprocess.PIPE, |
| 18 | + stderr=subprocess.PIPE, |
| 19 | + ) |
| 20 | + self._stderr_lines = [] |
| 21 | + self._stderr_thread = threading.Thread(target=self._capture_stderr, daemon=True) |
| 22 | + self._stderr_thread.start() |
| 23 | + |
| 24 | + # Read/parse output until we find the port number, the process exits, or we time out. |
16 | 25 | try: |
17 | 26 | self.server_port = None |
18 | | - while self.server_port is None and self._proc.poll() is None: |
19 | | - line = self._proc.stdout.readline() |
20 | | - match = re.search(rb"Server listening on port (\d+)", line) |
21 | | - if match: |
22 | | - self.server_port = int(match.group(1)) |
| 27 | + reader_thread = threading.Thread(target=self._read_port, daemon=True) |
| 28 | + reader_thread.start() |
| 29 | + reader_thread.join(timeout=self._SERVER_STARTUP_TIMEOUT) |
| 30 | + |
| 31 | + if reader_thread.is_alive(): |
| 32 | + self._proc.kill() |
| 33 | + self._stderr_thread.join(timeout=5) |
| 34 | + stderr_output = b''.join(self._stderr_lines).decode(errors='replace').strip() |
| 35 | + msg = f"Server did not start within {self._SERVER_STARTUP_TIMEOUT} seconds" |
| 36 | + if stderr_output: |
| 37 | + msg += f"\nServer stderr:\n{stderr_output}" |
| 38 | + raise RuntimeError(msg) |
23 | 39 |
|
24 | 40 | if self._proc.poll() is not None: |
25 | | - raise RuntimeError(f"Server exited with return code {self._proc.returncode}") |
| 41 | + self._stderr_thread.join(timeout=5) |
| 42 | + stderr_output = b''.join(self._stderr_lines).decode(errors='replace').strip() |
| 43 | + msg = f"Server exited with return code {self._proc.returncode}" |
| 44 | + if stderr_output: |
| 45 | + msg += f"\nServer stderr:\n{stderr_output}" |
| 46 | + raise RuntimeError(msg) |
| 47 | + |
| 48 | + if self.server_port is None: |
| 49 | + raise RuntimeError("Server process ended without reporting a port") |
26 | 50 |
|
27 | 51 | self._stdout_thread = threading.Thread(target=self._discard_output, args=(self._proc.stdout,), daemon=True) |
28 | 52 | self._stdout_thread.start() |
@@ -52,6 +76,19 @@ def _get_grpc_server_exe(self): |
52 | 76 | pytest.skip("NI gRPC Device Server not installed") |
53 | 77 | return server_exe |
54 | 78 |
|
| 79 | + def _read_port(self): |
| 80 | + while self.server_port is None and self._proc.poll() is None: |
| 81 | + line = self._proc.stdout.readline() |
| 82 | + if not line: |
| 83 | + break |
| 84 | + match = re.search(rb"Server listening on port (\d+)", line) |
| 85 | + if match: |
| 86 | + self.server_port = int(match.group(1)) |
| 87 | + |
| 88 | + def _capture_stderr(self): |
| 89 | + for line in self._proc.stderr: |
| 90 | + self._stderr_lines.append(line) |
| 91 | + |
55 | 92 | def _discard_output(self, stdout): |
56 | 93 | while True: |
57 | 94 | data = stdout.read(8196) |
|
0 commit comments