############################################################################ # # # Copyright (c) 2017 eBay Inc. # # Modifications copyright (c) 2019 Carl Drougge # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # ############################################################################ # workarounds for sadnesses import os import signal import termios import fcntl import atexit from contextlib import contextmanager class SignalWrapper(object): """Some misguided kernels (like Linux) feel that SIGINFO is not needed. Perhapt some other kernels feel similarly about other signals?""" def __init__(self, signal_names=['SIGINFO'], key_values=[20], skip_input_if_possible=True): """signal_names is a list of signal names, which will be listened for if they exist. key_values is a list of (terminal input) key values, each will be considered equivalent of a signal if pressed. If skip_input_if_possible is True no input will be read if all signals exist. No differentiation is made between the signals.""" self.key_values = set(key_values) self.restore = {} self.signal_set = False self.clean = False self.use_input = False all_sigs = True atexit.register(self.cleanup) for name in signal_names: sig = getattr(signal, name, None) if sig is not None: old = signal.signal(sig, self.signal_arrived) self.restore[sig] = old signal.siginterrupt(sig, False) else: all_sigs = False if all_sigs and skip_input_if_possible: return self.tc_original = termios.tcgetattr(0) tc_changed = list(self.tc_original) tc_changed[3] &= ~(termios.ICANON | termios.IEXTEN) self.use_input = True termios.tcsetattr(0, termios.TCSADRAIN, tc_changed) def cleanup(self): if not self.clean: for sig, old in self.restore.items(): if old is None: old = signal.SIG_DFL signal.signal(sig, old) if self.use_input: termios.tcsetattr(0, termios.TCSADRAIN, self.tc_original) self.clean = True def check(self): if self.use_input: with nonblocking(0): while True: try: if ord(os.read(0, 1)) in self.key_values: self.signal_set = True except OSError: break if self.signal_set: self.signal_set = False return True else: return False def signal_arrived(self, sig, frame): self.signal_set = True @contextmanager def nonblocking(fd): fl_original = fcntl.fcntl(fd, fcntl.F_GETFL) fl_changed = fl_original | os.O_NONBLOCK fcntl.fcntl(fd, fcntl.F_SETFL, fl_changed) try: yield finally: fcntl.fcntl(fd, fcntl.F_SETFL, fl_original)