Skip to content

Commit b0fc957

Browse files
refactor
1 parent b932452 commit b0fc957

File tree

1 file changed

+40
-89
lines changed

1 file changed

+40
-89
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 40 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import subprocess
77
import sys
88
from pathlib import Path
9-
from typing import Any, BinaryIO, TextIO, Union, cast
9+
from typing import BinaryIO, TextIO, Union, cast
1010

1111
import anyio
1212
from anyio import to_thread
@@ -21,10 +21,10 @@
2121
import win32job
2222
else:
2323
# Type stubs for non-Windows platforms
24-
win32api = None # type: ignore
25-
win32con = None # type: ignore
26-
win32job = None # type: ignore
27-
pywintypes = None # type: ignore
24+
win32api = None
25+
win32con = None
26+
win32job = None
27+
pywintypes = None
2828

2929

3030
def get_windows_executable_command(command: str) -> str:
@@ -157,6 +157,7 @@ async def create_windows_process(
157157
"""
158158
# Create a job object for this process
159159
job = _create_job_object()
160+
process = None
160161

161162
try:
162163
# First try using anyio with Windows-specific flags to hide console window
@@ -170,28 +171,9 @@ async def create_windows_process(
170171
stderr=errlog,
171172
cwd=cwd,
172173
)
173-
174-
# Assign process to job if we have one
175-
if job and hasattr(process, "pid") and win32api and win32con:
176-
try:
177-
# Open the process with required permissions
178-
process_handle = win32api.OpenProcess(
179-
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, process.pid
180-
)
181-
if process_handle:
182-
_assign_process_to_job(job, process_handle)
183-
win32api.CloseHandle(process_handle)
184-
# Store job on process for later cleanup
185-
process._job_object = job # type: ignore
186-
except Exception:
187-
# If we can't assign to job, close it
188-
if job and win32api:
189-
win32api.CloseHandle(job)
190-
191-
return process
192174
except NotImplementedError:
193175
# Windows often doesn't support async subprocess creation, use fallback
194-
return await _create_windows_fallback_process(command, args, env, errlog, cwd, job)
176+
process = await _create_windows_fallback_process(command, args, env, errlog, cwd)
195177
except Exception:
196178
# Try again without creation flags
197179
process = await anyio.open_process(
@@ -201,21 +183,8 @@ async def create_windows_process(
201183
cwd=cwd,
202184
)
203185

204-
# Assign process to job if we have one
205-
if job and hasattr(process, "pid") and win32api and win32con:
206-
try:
207-
process_handle = win32api.OpenProcess(
208-
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, process.pid
209-
)
210-
if process_handle:
211-
_assign_process_to_job(job, process_handle)
212-
win32api.CloseHandle(process_handle)
213-
process._job_object = job # type: ignore
214-
except Exception:
215-
if job and win32api:
216-
win32api.CloseHandle(job)
217-
218-
return process
186+
_maybe_assign_process_to_job(process, job)
187+
return process
219188

220189

221190
async def _create_windows_fallback_process(
@@ -224,7 +193,6 @@ async def _create_windows_fallback_process(
224193
env: dict[str, str] | None = None,
225194
errlog: TextIO | None = sys.stderr,
226195
cwd: Path | str | None = None,
227-
job: Any = None,
228196
) -> FallbackProcess:
229197
"""
230198
Create a subprocess using subprocess.Popen as a fallback when anyio fails.
@@ -244,24 +212,6 @@ async def _create_windows_fallback_process(
244212
bufsize=0, # Unbuffered output
245213
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
246214
)
247-
process = FallbackProcess(popen_obj)
248-
249-
# Assign to job if provided
250-
if job and win32api and win32con:
251-
try:
252-
process_handle = win32api.OpenProcess(
253-
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, popen_obj.pid
254-
)
255-
if process_handle:
256-
_assign_process_to_job(job, process_handle)
257-
win32api.CloseHandle(process_handle)
258-
process._job_object = job # type: ignore
259-
except Exception:
260-
if job:
261-
win32api.CloseHandle(job)
262-
263-
return process
264-
265215
except Exception:
266216
# If creationflags failed, fallback without them
267217
popen_obj = subprocess.Popen(
@@ -273,26 +223,11 @@ async def _create_windows_fallback_process(
273223
cwd=cwd,
274224
bufsize=0,
275225
)
276-
process = FallbackProcess(popen_obj)
277-
278-
# Assign to job if provided
279-
if job and win32api and win32con:
280-
try:
281-
process_handle = win32api.OpenProcess(
282-
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, popen_obj.pid
283-
)
284-
if process_handle:
285-
_assign_process_to_job(job, process_handle)
286-
win32api.CloseHandle(process_handle)
287-
process._job_object = job # type: ignore
288-
except Exception:
289-
if job:
290-
win32api.CloseHandle(job)
291-
292-
return process
226+
process = FallbackProcess(popen_obj)
227+
return process
293228

294229

295-
def _create_job_object() -> Any:
230+
def _create_job_object() -> int | None:
296231
"""
297232
Create a Windows Job Object configured to terminate all processes when closed.
298233
@@ -321,25 +256,41 @@ def _create_job_object() -> Any:
321256
return None
322257

323258

324-
def _assign_process_to_job(job: Any, process_handle: int) -> bool:
259+
def _maybe_assign_process_to_job(process: Union[Process, "FallbackProcess"], job: int | None) -> None:
325260
"""
326-
Assign a process to a job object.
261+
Try to assign a process to a job object. If assignment fails
262+
for any reason, the job handle is closed.
327263
328264
Args:
329-
job: The job object handle
330-
process_handle: The process handle to assign
331-
332-
Returns:
333-
True if successful, False otherwise
265+
process: The process to assign to the job
266+
job: The job object handle (may be None)
334267
"""
335-
if sys.platform != "win32" or not job or not win32job:
336-
return False
268+
if not job:
269+
return
270+
271+
if sys.platform != "win32" or not win32api or not win32con or not win32job:
272+
return
337273

338274
try:
339-
win32job.AssignProcessToJobObject(job, process_handle)
340-
return True
275+
# Open the process with required permissions
276+
process_handle = win32api.OpenProcess(
277+
win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, process.pid
278+
)
279+
if not process_handle:
280+
raise Exception("Failed to open process handle")
281+
282+
try:
283+
# Assign process to job
284+
win32job.AssignProcessToJobObject(job, process_handle)
285+
# Store job on process for later cleanup
286+
process._job_object = job # type: ignore
287+
finally:
288+
# Always close the process handle
289+
win32api.CloseHandle(process_handle)
341290
except Exception:
342-
return False
291+
# If we can't assign to job, close it
292+
if win32api:
293+
win32api.CloseHandle(job)
343294

344295

345296
async def terminate_windows_process_tree(process: Union[Process, "FallbackProcess"]) -> None:

0 commit comments

Comments
 (0)