Skip to content

Commit 4c60ea0

Browse files
author
Michael Jeronimo
authored
[Foxy] Handle signals within the asyncio loop. (#506)
* Handle signals within the asyncio loop. (#476) * Workaround asyncio signal handling on Unix (#479) * Remove is_winsock_handle() and instead test if wrapping the handle in a socket.socket() works (#494) * Close the socket pair used for signal management (#497) * Only try to wrap the fd in a socket on Windows (#498) * Bring back deprecated launch.utilities.signal_management APIs Signed-off-by: Michel Hidalgo <michel@ekumenlabs.com> Signed-off-by: Ivan Santiago Paunovic <ivanpauno@ekumenlabs.com>
1 parent 6ce4b35 commit 4c60ea0

File tree

4 files changed

+445
-178
lines changed

4 files changed

+445
-178
lines changed

launch/launch/launch_service.py

Lines changed: 26 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import collections.abc
1919
import contextlib
2020
import logging
21+
import platform
2122
import signal
2223
import threading
2324
import traceback
@@ -42,10 +43,7 @@
4243
from .launch_description import LaunchDescription
4344
from .launch_description_entity import LaunchDescriptionEntity
4445
from .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
4947
from .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

launch/launch/utilities/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@
1919
from .ensure_argument_type_impl import ensure_argument_type
2020
from .normalize_to_list_of_substitutions_impl import normalize_to_list_of_substitutions
2121
from .perform_substitutions_impl import perform_substitutions
22-
from .signal_management import install_signal_handlers
23-
from .signal_management import on_sigint
24-
from .signal_management import on_sigquit
25-
from .signal_management import on_sigterm
22+
from .signal_management import AsyncSafeSignalManager
23+
from .signal_management import install_signal_handlers, on_sigint, on_sigquit, on_sigterm
2624
from .visit_all_entities_and_collect_futures_impl import visit_all_entities_and_collect_futures
2725

2826
__all__ = [
@@ -32,6 +30,7 @@
3230
'create_future',
3331
'ensure_argument_type',
3432
'perform_substitutions',
33+
'AsyncSafeSignalManager',
3534
'install_signal_handlers',
3635
'on_sigint',
3736
'on_sigquit',

0 commit comments

Comments
 (0)