|
1 | 1 | import os |
| 2 | +import shutil |
| 3 | +import subprocess |
2 | 4 | import sys |
3 | 5 | from contextlib import asynccontextmanager |
4 | 6 | from pathlib import Path |
@@ -101,15 +103,19 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder |
101 | 103 | read_stream_writer, read_stream = anyio.create_memory_object_stream(0) |
102 | 104 | write_stream, write_stream_reader = anyio.create_memory_object_stream(0) |
103 | 105 |
|
104 | | - process = await anyio.open_process( |
105 | | - [server.command, *server.args], |
| 106 | + command = _get_executable_command(server.command) |
| 107 | + |
| 108 | + # Open process with stderr piped for capture |
| 109 | + process = await _create_platform_compatible_process( |
| 110 | + command=command, |
| 111 | + args=server.args, |
106 | 112 | env=( |
107 | 113 | {**get_default_environment(), **server.env} |
108 | 114 | if server.env is not None |
109 | 115 | else get_default_environment() |
110 | 116 | ), |
111 | | - stderr=errlog, |
112 | | - cwd=server.cwd, |
| 117 | + errlog=errlog, |
| 118 | + cwd=server.cwd |
113 | 119 | ) |
114 | 120 |
|
115 | 121 | async def stdout_reader(): |
@@ -159,4 +165,92 @@ async def stdin_writer(): |
159 | 165 | ): |
160 | 166 | tg.start_soon(stdout_reader) |
161 | 167 | tg.start_soon(stdin_writer) |
162 | | - yield read_stream, write_stream |
| 168 | + try: |
| 169 | + yield read_stream, write_stream |
| 170 | + finally: |
| 171 | + # Clean up process |
| 172 | + try: |
| 173 | + process.terminate() |
| 174 | + if sys.platform == "win32": |
| 175 | + try: |
| 176 | + with anyio.fail_after(2.0): |
| 177 | + await process.wait() |
| 178 | + except TimeoutError: |
| 179 | + # Force kill if it doesn't terminate |
| 180 | + process.kill() |
| 181 | + except Exception: |
| 182 | + pass |
| 183 | + |
| 184 | + |
| 185 | +def _get_executable_command(command: str) -> str: |
| 186 | + """ |
| 187 | + Get the correct executable command normalized for the current platform. |
| 188 | +
|
| 189 | + Args: |
| 190 | + command: Base command (e.g., 'uvx', 'npx') |
| 191 | +
|
| 192 | + Returns: |
| 193 | + List[str]: Platform-appropriate command |
| 194 | + """ |
| 195 | + |
| 196 | + try: |
| 197 | + if sys.platform != "win32": |
| 198 | + return command |
| 199 | + else: |
| 200 | + # For Windows, we need more sophisticated path resolution |
| 201 | + # First check if command exists in PATH as-is |
| 202 | + command_path = shutil.which(command) |
| 203 | + if command_path: |
| 204 | + return command_path |
| 205 | + |
| 206 | + # Check for Windows-specific extensions |
| 207 | + for ext in [".cmd", ".bat", ".exe", ".ps1"]: |
| 208 | + ext_version = f"{command}{ext}" |
| 209 | + ext_path = shutil.which(ext_version) |
| 210 | + if ext_path: |
| 211 | + return ext_path |
| 212 | + |
| 213 | + # For regular commands or if we couldn't find special versions |
| 214 | + return command |
| 215 | + except Exception: |
| 216 | + return command |
| 217 | + |
| 218 | + |
| 219 | +async def _create_platform_compatible_process( |
| 220 | + command: str, |
| 221 | + args: list[str], |
| 222 | + env: dict[str, str] | None = None, |
| 223 | + errlog: int | TextIO = subprocess.PIPE, |
| 224 | + cwd: Path | str | None = None, |
| 225 | +): |
| 226 | + """ |
| 227 | + Creates a subprocess in a platform-compatible way. |
| 228 | + Returns a process handle. |
| 229 | + """ |
| 230 | + |
| 231 | + process = None |
| 232 | + |
| 233 | + if sys.platform == "win32": |
| 234 | + try: |
| 235 | + process = await anyio.open_process( |
| 236 | + [command, *args], |
| 237 | + env=env, |
| 238 | + # Ensure we don't create console windows for each process |
| 239 | + creationflags=subprocess.CREATE_NO_WINDOW # type: ignore |
| 240 | + if hasattr(subprocess, "CREATE_NO_WINDOW") |
| 241 | + else 0, |
| 242 | + stderr=errlog, |
| 243 | + cwd=cwd, |
| 244 | + ) |
| 245 | + |
| 246 | + return process |
| 247 | + except Exception: |
| 248 | + # Don't raise, let's try to create the process using the default method |
| 249 | + process = None |
| 250 | + |
| 251 | + # Default method for creating the process |
| 252 | + process = await anyio.open_process( |
| 253 | + [command, *args], env=env, stderr=errlog, cwd=cwd |
| 254 | + ) |
| 255 | + |
| 256 | + return process |
0 commit comments