9
9
from platform import system
10
10
from subprocess import Popen
11
11
from time import perf_counter , sleep
12
- from typing import Generator , List , Optional , Tuple
12
+ from typing import Callable , Generator , Iterable , List , Optional , Tuple , cast
13
13
14
14
try :
15
15
from signal import SIGUSR1 , Signals
@@ -40,6 +40,52 @@ def _last_modified(scan_dir: Path) -> Optional[float]:
40
40
return None
41
41
42
42
43
+ def _safe_wait_procs (
44
+ procs : Iterable [Process ],
45
+ timeout : Optional [float ] = 0 ,
46
+ callback : Optional [Callable [[Process ], object ]] = None ,
47
+ ) -> Tuple [List [Process ], List [Process ]]:
48
+ """Wrapper for psutil.wait_procs() to avoid AccessDenied.
49
+ This can be an issue on Windows.
50
+
51
+ Args:
52
+ See psutil.wait_procs().
53
+
54
+ Returns:
55
+ See psutil.wait_procs().
56
+ """
57
+ assert timeout is None or timeout >= 0
58
+
59
+ deadline = None if timeout is None else perf_counter () + timeout
60
+ while True :
61
+ remaining = None if deadline is None else max (deadline - perf_counter (), 0 )
62
+ try :
63
+ return cast (
64
+ Tuple [List [Process ], List [Process ]],
65
+ wait_procs (procs , timeout = remaining , callback = callback ),
66
+ )
67
+ except AccessDenied :
68
+ pass
69
+ if deadline is not None and deadline <= perf_counter ():
70
+ break
71
+ sleep (0.25 )
72
+
73
+ # manually check processes
74
+ alive : List [Process ] = []
75
+ gone : List [Process ] = []
76
+ for proc in procs :
77
+ try :
78
+ if not proc .is_running ():
79
+ gone .append (proc )
80
+ else :
81
+ alive .append (proc )
82
+ except AccessDenied :
83
+ alive .append (proc )
84
+ except NoSuchProcess :
85
+ gone .append (proc )
86
+ return (gone , alive )
87
+
88
+
43
89
def _writing_coverage (procs : List [Process ]) -> bool :
44
90
"""Check if any processes have open .gcda files.
45
91
@@ -275,7 +321,7 @@ def terminate(self) -> None:
275
321
self .parent .wait (timeout = 10 )
276
322
except (AccessDenied , NoSuchProcess , TimeoutExpired ): # pragma: no cover
277
323
pass
278
- procs = wait_procs (procs , timeout = 0 )[1 ]
324
+ procs = _safe_wait_procs (procs , timeout = 0 )[1 ]
279
325
280
326
use_kill = False
281
327
while procs :
@@ -291,7 +337,7 @@ def terminate(self) -> None:
291
337
except (AccessDenied , NoSuchProcess ): # pragma: no cover
292
338
pass
293
339
# wait for processes to terminate
294
- procs = wait_procs (procs , timeout = 30 )[1 ]
340
+ procs = _safe_wait_procs (procs , timeout = 30 )[1 ]
295
341
if use_kill :
296
342
break
297
343
use_kill = True
@@ -316,7 +362,7 @@ def wait(self, timeout: int = 300) -> int:
316
362
"""
317
363
try :
318
364
exit_code = self .parent .wait (timeout = timeout ) or 0
319
- except NoSuchProcess : # pragma: no cover
365
+ except ( AccessDenied , NoSuchProcess ) : # pragma: no cover
320
366
# this is triggered sometimes when the process goes away
321
367
exit_code = 0
322
368
return exit_code
@@ -330,4 +376,4 @@ def wait_procs(self, timeout: Optional[float] = 0) -> int:
330
376
Returns:
331
377
Number of processes still alive.
332
378
"""
333
- return len (wait_procs (self .processes (), timeout = timeout )[1 ])
379
+ return len (_safe_wait_procs (self .processes (), timeout = timeout )[1 ])
0 commit comments