@@ -272,6 +272,8 @@ def __init__(
272272 self .terminate_timeout = terminate_timeout
273273 self .collectors : list [asyncio .Task [None ] | asyncio .Task [int ]] = []
274274 self .main_task : asyncio .Task [bool ] | None = None
275+ self .is_terminating = False
276+ self .do_terminate = False
275277
276278 async def _shutdown_process (self , proc : Process ) -> None :
277279 """Gracefully shutdown a child process."""
@@ -295,6 +297,8 @@ async def _main_task(self, proc: Process, wait_task: asyncio.Task[int]) -> bool:
295297 Return ``True`` if the process had a non-zero exit code during shutdown.
296298 """
297299 try :
300+ if self .do_terminate :
301+ self ._handle_sigint ()
298302 await proc .wait ()
299303 except asyncio .CancelledError :
300304 # SIGINT causes the task to be cancelled. We first need to uncancel it
@@ -352,7 +356,8 @@ async def _read_stream(
352356 # should be data and assume that the chunk is complete and EOF has been reached.
353357 if chunk_part :
354358 continue
355- is_eof = True
359+ # We mark the following line as 'no cover' since it should never happen.
360+ is_eof = True # pragma: no cover
356361
357362 # Re-combine the line (including separator, if EOF wasn't found).
358363 chunk = b"" .join (chunk_parts )
@@ -370,6 +375,7 @@ async def _stream_subprocess(
370375 args : Sequence [str ] | str ,
371376 * ,
372377 env : Mapping [str , str ] | None ,
378+ extra_tasks : Sequence [asyncio .Task [None ]],
373379 ) -> tuple [bool , int ]:
374380 """
375381 Start the process, all tasks, and wait for them to finish.
@@ -418,14 +424,33 @@ async def _stream_subprocess(
418424 self .main_task = self .loop .create_task (self ._main_task (proc , wait_task ))
419425
420426 # We wait for all tasks to finish and extract the return code.
421- await asyncio .wait ([* self .collectors , self .main_task ])
427+ await asyncio .wait ([* self .collectors , self .main_task , * extra_tasks ])
422428 return self .main_task .result (), wait_task .result ()
423429
430+ def _handle_sigint (self ) -> None :
431+ """
432+ SIGINT handler for event loop.
433+ """
434+ # In case this is called before the main task is created,
435+ # set a flag and that's it.
436+ if not self .main_task :
437+ self .do_terminate = True
438+ return
439+
440+ # Don't handle it twice.
441+ if self .is_terminating :
442+ return
443+ self .is_terminating = True
444+
445+ # Cancel the main task. This triggers graceful shutdown.
446+ self .main_task .cancel ()
447+
424448 def run (
425449 self ,
426450 args : Sequence [str ] | str ,
427451 * ,
428452 env : Mapping [str , str ] | None = None ,
453+ extra_tasks : Sequence [asyncio .Task [None ]] | None = None ,
429454 ) -> tuple [int , bytes , bytes ]:
430455 """
431456 Run the command with the given environment.
@@ -434,22 +459,10 @@ def run(
434459 Note that this function is **NOT** thread-safe, since we have to
435460 add a SIGINT handler to the event loop and later remove it.
436461 """
437- is_terminating = [False ]
438-
439- def handle_sigint () -> None :
440- # Don't handle it twice.
441- if is_terminating [0 ]:
442- return
443- is_terminating [0 ] = True
444-
445- # Cancel the main task. This triggers graceful shutdown.
446- if self .main_task :
447- self .main_task .cancel ()
448-
449- self .loop .add_signal_handler (signal .SIGINT , handle_sigint )
462+ self .loop .add_signal_handler (signal .SIGINT , self ._handle_sigint )
450463 try :
451464 is_canceled , return_code = self .loop .run_until_complete (
452- self ._stream_subprocess (args , env = env )
465+ self ._stream_subprocess (args , env = env , extra_tasks = extra_tasks or [] )
453466 )
454467 finally :
455468 self .loop .remove_signal_handler (signal .SIGINT )
0 commit comments