# # Copyright (C) 2017 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . # # Authors: # Tristan Van Berkom import os import signal import sys import threading import traceback from contextlib import contextmanager, ExitStack from collections import deque from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, MutableSequence # Global per process state for handling of sigterm/sigtstp/sigcont, # note that it is expected that this only ever be used by new processes # the scheduler starts, not the main process. # # FIXME: We should ideally be using typing.Deque as type hints below, not # typing.MutableSequence. However, that is only available in Python versions # 3.5.4 onward and 3.6.1 onward. # Debian 9 ships with 3.5.3. terminator_stack = deque() # type: MutableSequence[Callable] suspendable_stack = deque() # type: MutableSequence[Callable] # Per process SIGTERM handler def terminator_handler(signal_, frame): while terminator_stack: terminator_ = terminator_stack.pop() try: terminator_() except: # noqa pylint: disable=bare-except # Ensure we print something if there's an exception raised when # processing the handlers. Note that the default exception # handler won't be called because we os._exit next, so we must # catch all possible exceptions with the unqualified 'except' # clause. traceback.print_exc(file=sys.stderr) print('Error encountered in BuildStream while processing custom SIGTERM handler:', terminator_, file=sys.stderr) # Use special exit here, terminate immediately, recommended # for precisely this situation where child processes are teminated. os._exit(-1) # terminator() # # A context manager for interruptable tasks, this guarantees # that while the code block is running, the supplied function # will be called upon process termination. # # Note that after handlers are called, the termination will be handled by # terminating immediately with os._exit(). This means that SystemExit will not # be raised and 'finally' clauses will not be executed. # # Args: # terminate_func (callable): A function to call when aborting # the nested code block. # @contextmanager def terminator(terminate_func): global terminator_stack # pylint: disable=global-statement # Signal handling only works in the main thread if threading.current_thread() != threading.main_thread(): yield return outermost = bool(not terminator_stack) terminator_stack.append(terminate_func) if outermost: original_handler = signal.signal(signal.SIGTERM, terminator_handler) try: yield finally: if outermost: signal.signal(signal.SIGTERM, original_handler) terminator_stack.pop() # Just a simple object for holding on to two callbacks class Suspender(): def __init__(self, suspend_callback, resume_callback): self.suspend = suspend_callback self.resume = resume_callback # Per process SIGTSTP handler def suspend_handler(sig, frame): # Suspend callbacks from innermost frame first for suspender in reversed(suspendable_stack): suspender.suspend() # Use SIGSTOP directly now on self, dont introduce more SIGTSTP # # Here the process sleeps until SIGCONT, which we simply # dont handle. We know we'll pickup execution right here # when we wake up. os.kill(os.getpid(), signal.SIGSTOP) # Resume callbacks from outermost frame inwards for suspender in suspendable_stack: suspender.resume() # suspendable() # # A context manager for handling process suspending and resumeing # # Args: # suspend_callback (callable): A function to call as process suspend time. # resume_callback (callable): A function to call as process resume time. # # This must be used in code blocks which start processes that become # their own session leader. In these cases, SIGSTOP and SIGCONT need # to be propagated to the child process group. # # This context manager can also be used recursively, so multiple # things can happen at suspend/resume time (such as tracking timers # and ensuring durations do not count suspended time). # @contextmanager def suspendable(suspend_callback, resume_callback): global suspendable_stack # pylint: disable=global-statement outermost = bool(not suspendable_stack) suspender = Suspender(suspend_callback, resume_callback) suspendable_stack.append(suspender) if outermost: original_stop = signal.signal(signal.SIGTSTP, suspend_handler) try: yield finally: if outermost: signal.signal(signal.SIGTSTP, original_stop) suspendable_stack.pop() # blocked() # # A context manager for running a code block with blocked signals # # Args: # signals (list): A list of unix signals to block # ignore (bool): Whether to ignore entirely the signals which were # received and pending while the process had blocked them # @contextmanager def blocked(signal_list, ignore=True): with ExitStack() as stack: # Optionally add the ignored() context manager to this context if ignore: stack.enter_context(ignored(signal_list)) # Set and save the sigprocmask blocked_signals = signal.pthread_sigmask(signal.SIG_BLOCK, signal_list) try: yield finally: # If we have discarded the signals completely, this line will cause # the discard_handler() to trigger for each signal in the list signal.pthread_sigmask(signal.SIG_SETMASK, blocked_signals) # ignored() # # A context manager for running a code block with ignored signals # # Args: # signals (list): A list of unix signals to ignore # @contextmanager def ignored(signal_list): orig_handlers = {} for sig in signal_list: orig_handlers[sig] = signal.signal(sig, signal.SIG_IGN) try: yield finally: for sig in signal_list: signal.signal(sig, orig_handlers[sig])