|
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