import signal import contextlib import functools from veros import logger def do_not_disturb(function): """Decorator that catches SIGINT and SIGTERM signals (e.g. after keyboard interrupt) and makes sure that the function body is executed before exiting. Useful for ensuring that output files are written properly. """ signals = (signal.SIGINT, signal.SIGTERM) @functools.wraps(function) def dnd_wrapper(*args, **kwargs): old_handlers = {s: signal.getsignal(s) for s in signals} signal_received = {"sig": None, "frame": None} def handler(sig, frame): if signal_received["sig"] is None: signal_received["sig"] = sig signal_received["frame"] = frame logger.error(f"Signal {sig} received - cleaning up before exit") else: # force quit if more than one signal is received old_handlers[sig](sig, frame) for s in signals: signal.signal(s, handler) try: res = function(*args, **kwargs) finally: for s in signals: signal.signal(s, old_handlers[s]) sig = signal_received["sig"] if sig is not None: old_handlers[sig](signal_received["sig"], signal_received["frame"]) return res return dnd_wrapper @contextlib.contextmanager def signals_to_exception(signals=(signal.SIGINT, signal.SIGTERM)): """Context manager that converts system signals to exceptions. This allows for a graceful exit after receiving SIGTERM (e.g. through `kill` on UNIX systems). Example: >>> with signals_to_exception(): >>> try: >>> # do something >>> except SystemExit: >>> # graceful exit even upon receiving interrupt signal """ def signal_to_exception(sig, frame): logger.critical("Received interrupt signal {}", sig) raise SystemExit("Aborted") old_signals = {} for s in signals: # override signals with our handler old_signals[s] = signal.getsignal(s) signal.signal(s, signal_to_exception) try: yield finally: # re-attach old signals for s in signals: signal.signal(s, old_signals[s])