def signal_handler(sig, frame): """Handler for terminating signals. Send CtrlAltDel to the firecracker process on first invocation. Forward the signal and kill firecracker on subsequent invocations. :sig: signal number :frame: current stack frame """ if not hasattr(signal_handler, "SentCtrlAltDel"): # Send Ctrl+Alt+Del via the Firecracker API socket so the guest can shut down gracefully. shutdown_request = ('PUT /actions HTTP/1.0\r\n' 'Content-Type: application/json\r\n' 'Content-Length: 33\r\n' '\r\n' '{"action_type": "SendCtrlAltDel"}') api_client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) api_client.settimeout(0.1) api_client.connect( bytes(Path(instance_chroot / 'run' / 'firecracker.socket'))) api_client.send(shutdown_request.encode()) try: while len(api_client.recv(256)) >= 256: pass except socket.timeout: pass api_client.close() signal_handler.SentCtrlAltDel = True else: # This is the second signal. If the guest did not react to Ctrl+Alt+Del the first time, # it won't do so when sending it again. Pass the signal to the Firecracker process and # follow up with SIGKILL. if firecracker_process.poll() is None: # Our direct child process is still running. Signal it. firecracker_process.send_signal(sig) try: firecracker_process.communicate(timeout=0.25) except subprocess.TimeoutExpired: firecracker_process.kill() else: # Our direct child has terminated. This means it forked and we are really waiting # for firecracker_pid. if type(firecracker_pid) == int: os.kill(firecracker_pid, sig) time.sleep(0.25) try: os.kill(firecracker_pid, signal.SIGKILL) except ProcessLookupError: pass else: signal.pidfd_send_signal(firecracker_pid.fileno(), sig) time.sleep(0.25) try: signal.pidfd_send_signal(firecracker_pid.fileno(), signal.SIGKILL) except ProcessLookupError: pass
def send_signal_safe(own_pid, target_pid): """Sends SIGUSR1 to a process if both processes have the same associated command name. (Race condition free) Requires: - Python 3.9+ - Linux 5.1+ Args: own_pid (int): the pid of this process target_pid (int): the pid of the process to send the signal to """ pidfile = None try: pidfile = os.open(f'/proc/{target_pid}', os.O_DIRECTORY) if is_same_command(own_pid, target_pid): signal.pidfd_send_signal(pidfile, signal.SIGUSR1) except FileNotFoundError: pass except OSError as error: # not sure if errno is really set.. # at least the documentation of the used functions says so.. # see e.g.: https://github.com/python/cpython/commit/7483451577916e693af6d20cf520b2cc7e2174d2#diff-99fb04b208835118fdca0d54b76a00c450da3eaff09d2b53e8a03d63bbe88e30R1279-R1281 # and https://docs.python.org/3/c-api/exceptions.html#c.PyErr_SetFromErrno # caused by either pidfile_open or pidfd_send_signal if error.errno != errno.ESRCH: raise # else: the process is death finally: if pidfile is not None: os.close(pidfile)
def test_pidfd_send_signal(self): with self.assertRaises(OSError) as cm: signal.pidfd_send_signal(0, signal.SIGINT) if cm.exception.errno == errno.ENOSYS: self.skipTest("kernel does not support pidfds") self.assertEqual(cm.exception.errno, errno.EBADF) my_pidfd = os.open(f'/proc/{os.getpid()}', os.O_DIRECTORY) self.addCleanup(os.close, my_pidfd) with self.assertRaisesRegexp(TypeError, "^siginfo must be None$"): signal.pidfd_send_signal(my_pidfd, signal.SIGINT, object(), 0) with self.assertRaises(KeyboardInterrupt): signal.pidfd_send_signal(my_pidfd, signal.SIGINT)
firecracker_process = subprocess.Popen(jailer_cmd) firecracker_process.communicate() if args.new_pid_ns: # With --new-pid-ns, jailer forks and therefore does not block until firecracker terminates. # We need to wait for firecracker before we can exit and clean up the chroot directory. with Path(instance_chroot / 'firecracker.pid').open('r') as f: # Get the PID of the firecracker process from the file firecracker.pid in the root of the # chroot directory. firecracker_pid = int(f.read()) try: # pidfd_open is superior but requires Python 3.9+ and Linux 5.3+. # If this fails, fall back to traditional process management. firecracker_pid = open(os.pidfd_open(firecracker_pid)) except Exception: pass while True: try: # Send signal 0 (no signal), to check if the process is still alive. if type(firecracker_pid) == int: os.kill(firecracker_pid, 0) else: signal.pidfd_send_signal(firecracker_pid.fileno(), 0) except ProcessLookupError: # The firecracker process has exited, we can clean up now. if type(firecracker_pid) != int: firecracker_pid.close() break time.sleep(0.25) sys.exit(firecracker_process.returncode)