1818import collections .abc
1919import contextlib
2020import logging
21+ import platform
2122import signal
2223import threading
2324import traceback
4243from .launch_description import LaunchDescription
4344from .launch_description_entity import LaunchDescriptionEntity
4445from .some_actions_type import SomeActionsType
45- from .utilities import install_signal_handlers
46- from .utilities import on_sigint
47- from .utilities import on_sigquit
48- from .utilities import on_sigterm
46+ from .utilities import AsyncSafeSignalManager
4947from .utilities import visit_all_entities_and_collect_futures
5048
5149
@@ -62,11 +60,6 @@ def __init__(
6260 """
6361 Create a LaunchService.
6462
65- If called outside of the main-thread before the function
66- :func:`launch.utilities.install_signal_handlers()` has been called,
67- a ValueError can be raised, as setting signal handlers cannot be done
68- outside of the main-thread.
69-
7063 :param: argv stored in the context for access by the entities, None results in []
7164 :param: noninteractive if True (not default), this service will assume it has
7265 no terminal associated e.g. it is being executed from a non interactive script
@@ -80,10 +73,6 @@ def __init__(
8073 # Setup logging
8174 self .__logger = launch .logging .get_logger ('launch' )
8275
83- # Install signal handlers if not already installed, will raise if not
84- # in main-thread, call manually in main-thread to avoid this.
85- install_signal_handlers ()
86-
8776 # Setup context and register a built-in event handler for bootstrapping.
8877 self .__context = LaunchContext (argv = self .__argv , noninteractive = noninteractive )
8978 self .__context .register_event_handler (OnIncludeLaunchDescription ())
@@ -197,12 +186,7 @@ def _prepare_run_loop(self):
197186 # Setup custom signal handlers for SIGINT, SIGTERM and maybe SIGQUIT.
198187 sigint_received = False
199188
200- def _on_sigint (signum , frame , prev_handler ):
201- # Ignore additional signals until we finish processing this one.
202- current_handler = signal .signal (signal .SIGINT , signal .SIG_IGN )
203- if current_handler is signal .SIG_IGN :
204- # This function has been called re-entrantly.
205- return
189+ def _on_sigint (signum ):
206190 nonlocal sigint_received
207191 base_msg = 'user interrupted with ctrl-c (SIGINT)'
208192 if not sigint_received :
@@ -214,57 +198,25 @@ def _on_sigint(signum, frame, prev_handler):
214198 sigint_received = True
215199 else :
216200 self .__logger .warning ('{} again, ignoring...' .format (base_msg ))
217- if callable (prev_handler ):
218- try :
219- # Run pre-existing signal handler.
220- prev_handler (signum , frame )
221- except KeyboardInterrupt :
222- # Ignore exception.
223- pass
224- # Restore current signal handler (not necessarily this one).
225- signal .signal (signal .SIGINT , current_handler )
226-
227- on_sigint (_on_sigint )
228-
229- def _on_sigterm (signum , frame , prev_handler ):
230- # Ignore additional signals until we finish processing this one.
231- current_handler = signal .signal (signal .SIGTERM , signal .SIG_IGN )
232- if current_handler is signal .SIG_IGN :
233- # This function has been called re-entrantly.
234- return
201+
202+ def _on_sigterm (signum ):
203+ signame = signal .Signals (signum ).name
204+ self .__logger .error (
205+ 'user interrupted with ctrl-\\ ({}), terminating...' .format (signame ))
235206 # TODO(wjwwood): try to terminate running subprocesses before exiting.
236- self .__logger .error ('using SIGTERM or SIGQUIT can result in orphaned processes' )
207+ self .__logger .error ('using {} can result in orphaned processes' . format ( signame ) )
237208 self .__logger .error ('make sure no processes launched are still running' )
238209 this_loop .call_soon (this_task .cancel )
239- if callable (prev_handler ):
240- # Run pre-existing signal handler.
241- prev_handler (signum , frame )
242- # Restore current signal handler (not necessarily this one).
243- signal .signal (signal .SIGTERM , current_handler )
244-
245- on_sigterm (_on_sigterm )
246-
247- def _on_sigquit (signum , frame , prev_handler ):
248- # Ignore additional signals until we finish processing this one.
249- current_handler = signal .signal (signal .SIGQUIT , signal .SIG_IGN )
250- if current_handler is signal .SIG_IGN :
251- # This function has been called re-entrantly.
252- return
253- self .__logger .error ('user interrupted with ctrl-\\ (SIGQUIT), terminating...' )
254- _on_sigterm (signum , frame , prev_handler )
255- # Restore current signal handler (not necessarily this one).
256- signal .signal (signal .SIGQUIT , current_handler )
257-
258- on_sigquit (_on_sigquit )
259-
260- # Yield asyncio loop and current task.
261- yield self .__loop_from_run_thread , this_task
262- finally :
263- # Unset the signal handlers while not running.
264- on_sigint (None )
265- on_sigterm (None )
266- on_sigquit (None )
267210
211+ with AsyncSafeSignalManager (this_loop ) as manager :
212+ # Setup signal handlers
213+ manager .handle (signal .SIGINT , _on_sigint )
214+ manager .handle (signal .SIGTERM , _on_sigterm )
215+ if platform .system () != 'Windows' :
216+ manager .handle (signal .SIGQUIT , _on_sigterm )
217+ # Yield asyncio loop and current task.
218+ yield this_loop , this_task
219+ finally :
268220 # No matter what happens, unset the loop.
269221 with self .__loop_from_run_thread_lock :
270222 self .__context ._set_asyncio_loop (None )
@@ -309,9 +261,6 @@ async def run_async(self, *, shutdown_when_idle=True) -> int:
309261 This should only ever be run from the main thread and not concurrently with other
310262 asynchronous runs.
311263
312- Note that custom signal handlers are set, and KeyboardInterrupt is caught and ignored
313- around the original signal handler. After the run ends, this behavior is undone.
314-
315264 :param: shutdown_when_idle if True (default), the service will shutdown when idle.
316265 """
317266 # Make sure this has not been called from any thread but the main thread.
@@ -397,14 +346,20 @@ def run(self, *, shutdown_when_idle=True) -> int:
397346 This should only ever be run from the main thread and not concurrently with
398347 asynchronous runs (see `run_async()` documentation).
399348
349+ Note that KeyboardInterrupt is caught and ignored, as signals are handled separately.
350+ After the run ends, this behavior is undone.
351+
400352 :param: shutdown_when_idle if True (default), the service will shutdown when idle
401353 """
402354 loop = osrf_pycommon .process_utils .get_loop ()
403355 run_async_task = loop .create_task (self .run_async (
404356 shutdown_when_idle = shutdown_when_idle
405357 ))
406- loop .run_until_complete (run_async_task )
407- return run_async_task .result ()
358+ while True :
359+ try :
360+ return loop .run_until_complete (run_async_task )
361+ except KeyboardInterrupt :
362+ continue
408363
409364 def __on_shutdown (self , event : Event , context : LaunchContext ) -> Optional [SomeActionsType ]:
410365 self .__shutting_down = True
0 commit comments