66import subprocess
77import sys
88from pathlib import Path
9- from typing import Any , BinaryIO , TextIO , Union , cast
9+ from typing import BinaryIO , TextIO , Union , cast
1010
1111import anyio
1212from anyio import to_thread
2121 import win32job
2222else :
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
3030def 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
221190async 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
345296async def terminate_windows_process_tree (process : Union [Process , "FallbackProcess" ]) -> None :
0 commit comments