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
28+
29+ JobHandle = int
2830
2931
3032def get_windows_executable_command (command : str ) -> str :
@@ -157,6 +159,7 @@ async def create_windows_process(
157159 """
158160 # Create a job object for this process
159161 job = _create_job_object ()
162+ process = None
160163
161164 try :
162165 # First try using anyio with Windows-specific flags to hide console window
@@ -170,28 +173,9 @@ async def create_windows_process(
170173 stderr = errlog ,
171174 cwd = cwd ,
172175 )
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
192176 except NotImplementedError :
193177 # Windows often doesn't support async subprocess creation, use fallback
194- return await _create_windows_fallback_process (command , args , env , errlog , cwd , job )
178+ process = await _create_windows_fallback_process (command , args , env , errlog , cwd )
195179 except Exception :
196180 # Try again without creation flags
197181 process = await anyio .open_process (
@@ -201,21 +185,8 @@ async def create_windows_process(
201185 cwd = cwd ,
202186 )
203187
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
188+ _maybe_assign_process_to_job (process , job )
189+ return process
219190
220191
221192async def _create_windows_fallback_process (
@@ -224,7 +195,6 @@ async def _create_windows_fallback_process(
224195 env : dict [str , str ] | None = None ,
225196 errlog : TextIO | None = sys .stderr ,
226197 cwd : Path | str | None = None ,
227- job : Any = None ,
228198) -> FallbackProcess :
229199 """
230200 Create a subprocess using subprocess.Popen as a fallback when anyio fails.
@@ -244,24 +214,6 @@ async def _create_windows_fallback_process(
244214 bufsize = 0 , # Unbuffered output
245215 creationflags = getattr (subprocess , "CREATE_NO_WINDOW" , 0 ),
246216 )
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-
265217 except Exception :
266218 # If creationflags failed, fallback without them
267219 popen_obj = subprocess .Popen (
@@ -273,26 +225,11 @@ async def _create_windows_fallback_process(
273225 cwd = cwd ,
274226 bufsize = 0 ,
275227 )
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 )
228+ process = FallbackProcess (popen_obj )
229+ return process
291230
292- return process
293231
294-
295- def _create_job_object () -> Any :
232+ def _create_job_object () -> int | None :
296233 """
297234 Create a Windows Job Object configured to terminate all processes when closed.
298235
@@ -321,25 +258,41 @@ def _create_job_object() -> Any:
321258 return None
322259
323260
324- def _assign_process_to_job ( job : Any , process_handle : int ) -> bool :
261+ def _maybe_assign_process_to_job ( process : Union [ Process , "FallbackProcess" ], job : JobHandle | None ) -> None :
325262 """
326- Assign a process to a job object.
263+ Try to assign a process to a job object. If assignment fails
264+ for any reason, the job handle is closed.
327265
328266 Args:
329- job: The job object handle
330- process_handle: The process handle to assign
331-
332- Returns:
333- True if successful, False otherwise
267+ process: The process to assign to the job
268+ job: The job object handle (may be None)
334269 """
335- if sys .platform != "win32" or not job or not win32job :
336- return False
270+ if not job :
271+ return
272+
273+ if sys .platform != "win32" or not win32api or not win32con or not win32job :
274+ return
337275
338276 try :
339- win32job .AssignProcessToJobObject (job , process_handle )
340- return True
277+ # Open the process with required permissions
278+ process_handle = win32api .OpenProcess (
279+ win32con .PROCESS_SET_QUOTA | win32con .PROCESS_TERMINATE , False , process .pid
280+ )
281+ if not process_handle :
282+ raise Exception ("Failed to open process handle" )
283+
284+ try :
285+ # Assign process to job
286+ win32job .AssignProcessToJobObject (job , process_handle )
287+ # Store job on process for later cleanup
288+ process ._job_object = job # type: ignore
289+ finally :
290+ # Always close the process handle
291+ win32api .CloseHandle (process_handle )
341292 except Exception :
342- return False
293+ # If we can't assign to job, close it
294+ if win32api :
295+ win32api .CloseHandle (job )
343296
344297
345298async def terminate_windows_process_tree (process : Union [Process , "FallbackProcess" ]) -> None :
0 commit comments