Launch the proxy server and return its URL.
(self)
| 25 | self._ca_file_path: str | None = None |
| 26 | |
| 27 | async def start(self) -> str: |
| 28 | """Launch the proxy server and return its URL.""" |
| 29 | if self._proxy_url: |
| 30 | return self._proxy_url |
| 31 | |
| 32 | # The harness server is in the shared test directory |
| 33 | server_path = os.path.join( |
| 34 | os.path.dirname(__file__), "..", "..", "..", "test", "harness", "server.ts" |
| 35 | ) |
| 36 | server_path = os.path.abspath(server_path) |
| 37 | |
| 38 | # On Windows, use shell=True to find npx |
| 39 | use_shell = platform.system() == "Windows" |
| 40 | |
| 41 | self._process = subprocess.Popen( |
| 42 | ["npx", "tsx", server_path], |
| 43 | stdout=subprocess.PIPE, |
| 44 | stderr=None, # Inherit stderr to parent for debugging |
| 45 | text=True, |
| 46 | cwd=os.path.dirname(server_path), |
| 47 | shell=use_shell, |
| 48 | ) |
| 49 | |
| 50 | # Read until the server prints "Listening: http://..."; npm/npx may emit |
| 51 | # wrapper output first on some platforms. |
| 52 | line = "" |
| 53 | match = None |
| 54 | while True: |
| 55 | line = self._process.stdout.readline() |
| 56 | if not line: |
| 57 | self._process.kill() |
| 58 | raise RuntimeError("Failed to read proxy URL") |
| 59 | match = re.search(r"Listening: (http://[^\s]+)", line.strip()) |
| 60 | if match: |
| 61 | break |
| 62 | |
| 63 | self._proxy_url = match.group(1) |
| 64 | metadata_match = re.search(r"(\{.*\})\s*$", line.strip()) |
| 65 | if not metadata_match: |
| 66 | self._process.kill() |
| 67 | raise RuntimeError(f"Proxy startup line missing CONNECT proxy metadata: {line}") |
| 68 | try: |
| 69 | metadata = json.loads(metadata_match.group(1)) |
| 70 | except json.JSONDecodeError as exc: |
| 71 | self._process.kill() |
| 72 | raise RuntimeError(f"Failed to parse proxy startup metadata: {line}") from exc |
| 73 | self._connect_proxy_url = metadata.get("connectProxyUrl") |
| 74 | self._ca_file_path = metadata.get("caFilePath") |
| 75 | if not self._connect_proxy_url or not self._ca_file_path: |
| 76 | self._process.kill() |
| 77 | raise RuntimeError(f"Proxy startup metadata missing CONNECT proxy details: {line}") |
| 78 | return self._proxy_url |
| 79 | |
| 80 | async def stop(self, skip_writing_cache: bool = False): |
| 81 | """Gracefully shut down the proxy server. |