|
6 | 6 |
|
7 | 7 | import anyio |
8 | 8 | import anyio.lowlevel |
| 9 | +from anyio.abc import Process |
9 | 10 | from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream |
10 | 11 | from anyio.streams.text import TextReceiveStream |
11 | 12 | from pydantic import BaseModel, Field |
|
38 | 39 | ) |
39 | 40 |
|
40 | 41 |
|
| 42 | +class ProcessTerminatedEarlyError(Exception): |
| 43 | + """Raised when a process terminates unexpectedly.""" |
| 44 | + |
| 45 | + |
41 | 46 | def get_default_environment() -> dict[str, str]: |
42 | 47 | """ |
43 | 48 | Returns a default environment object including only environment variables deemed |
@@ -110,7 +115,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder |
110 | 115 | command = _get_executable_command(server.command) |
111 | 116 |
|
112 | 117 | # Open process with stderr piped for capture |
113 | | - process = await _create_platform_compatible_process( |
| 118 | + process: Process = await _create_platform_compatible_process( |
114 | 119 | command=command, |
115 | 120 | args=server.args, |
116 | 121 | env=( |
@@ -163,20 +168,36 @@ async def stdin_writer(): |
163 | 168 | except anyio.ClosedResourceError: |
164 | 169 | await anyio.lowlevel.checkpoint() |
165 | 170 |
|
| 171 | + process_error: str | None = None |
| 172 | + |
166 | 173 | async with ( |
167 | 174 | anyio.create_task_group() as tg, |
168 | 175 | process, |
169 | 176 | ): |
170 | 177 | tg.start_soon(stdout_reader) |
171 | 178 | tg.start_soon(stdin_writer) |
| 179 | + # tg.start_soon(monitor_process, tg.cancel_scope) |
172 | 180 | try: |
173 | 181 | yield read_stream, write_stream |
174 | 182 | finally: |
175 | | - # Clean up process to prevent any dangling orphaned processes |
176 | | - if sys.platform == "win32": |
177 | | - await terminate_windows_process(process) |
| 183 | + await read_stream.aclose() |
| 184 | + await write_stream.aclose() |
| 185 | + await read_stream_writer.aclose() |
| 186 | + await write_stream_reader.aclose() |
| 187 | + |
| 188 | + if process.returncode is not None and process.returncode != 0: |
| 189 | + process_error = f"Process exited with code {process.returncode}." |
178 | 190 | else: |
179 | | - process.terminate() |
| 191 | + # Clean up process to prevent any dangling orphaned processes |
| 192 | + if sys.platform == "win32": |
| 193 | + await terminate_windows_process(process) |
| 194 | + else: |
| 195 | + process.terminate() |
| 196 | + |
| 197 | + if process_error: |
| 198 | + # Raise outside the task group so that the error is not wrapped in an |
| 199 | + # ExceptionGroup |
| 200 | + raise ProcessTerminatedEarlyError(process_error) |
180 | 201 |
|
181 | 202 |
|
182 | 203 | def _get_executable_command(command: str) -> str: |
|
0 commit comments