import os import signal import threading import time import socket import select import itertools # Equivalent to the C function raise(), which Python doesn't wrap if os.name == "nt": import cffi _ffi = cffi.FFI() _ffi.cdef("int raise(int);") _lib = _ffi.dlopen("api-ms-win-crt-runtime-l1-1-0.dll") signal_raise = getattr(_lib, "raise") else: def signal_raise(signum): os.kill(os.getpid(), signum) def raise_SIGINT_soon(): time.sleep(1) signal_raise(signal.SIGINT) def drain(sock): try: while True: sock.recv(1024) except BlockingIOError: pass def main(): writer, reader = socket.socketpair() writer.setblocking(False) reader.setblocking(False) signal.set_wakeup_fd(writer.fileno()) # Keep trying until we lose the race... for attempt in itertools.count(): print(f"Attempt {attempt}: start") # Arrange for SIGINT to be delivered 1 second from now thread = threading.Thread(target=raise_SIGINT_soon) thread.start() # Fake an IO loop that's trying to sleep for 10 seconds (but will # hopefully get interrupted after just 1 second) start = time.monotonic() target = start + 10 try: select_calls = 0 while True: now = time.monotonic() if now > target: break select_calls += 1 r, _, _ = select.select([reader], [], [], target - now) if r: drain(reader) except KeyboardInterrupt: pass else: print(f"Attempt {attempt}: no KeyboardInterrupt?!") # We expect a successful run to take 1 second, and a failed run to # take 10 seconds, so 2 seconds is a reasonable cutoff to distinguish # them. duration = time.monotonic() - start if duration < 2: print(f"Attempt {attempt}: OK, trying again") else: print(f"Attempt {attempt}: FAILED, took {duration} seconds") print(f"select_calls = {select_calls}") break thread.join() if __name__ == "__main__": main()