def __init__(self, popen, stdin, stdout, stderr): self._proc = popen self.stdin = stdin # type: Optional[SendStream] self.stdout = stdout # type: Optional[ReceiveStream] self.stderr = stderr # type: Optional[ReceiveStream] self.stdio = None # type: Optional[StapledStream] if self.stdin is not None and self.stdout is not None: self.stdio = StapledStream(self.stdin, self.stdout) self._wait_lock = Lock() self._pidfd = None if can_try_pidfd_open: try: fd = pidfd_open(self._proc.pid, 0) except OSError: # Well, we tried, but it didn't work (probably because we're # running on an older kernel, or in an older sandbox, that # hasn't been updated to support pidfd_open). We'll fall back # on waitid instead. pass else: # It worked! Wrap the raw fd up in a Python file object to # make sure it'll get closed. self._pidfd = open(fd) self.args = self._proc.args self.pid = self._proc.pid
def has_pidfd_support(): if not hasattr(os, 'pidfd_open'): return False try: os.close(os.pidfd_open(os.getpid())) except OSError: return False return True
def add_child_handler(self, pid, callback, *args): existing = self._callbacks.get(pid) if existing is not None: self._callbacks[pid] = existing[0], callback, args else: pidfd = os.pidfd_open(pid) self._loop._add_reader(pidfd, self._do_wait, pid) self._callbacks[pid] = pidfd, callback, args
def wait(self, *, timeout: Union[int, float, None] = None) -> Optional[int]: # We check is_running() up front so we don't run into PID reuse. # After that, we can safely just check pid_exists() or os.waitpid(). if not self.is_running(): with self._lock, self._exitcode_lock: return self._exitcode start_time = time.monotonic( ) if timeout is not None and timeout > 0 else 0 # Assume it's a child of the current process by default is_child = self._pid > 0 if timeout is None and self._pid > 0: # Wait with no timeout # We don't lock on self._lock because this is blocking with self._exitcode_lock: try: wstatus = os.waitpid(self._pid, 0)[1] except ChildProcessError: # Not a child of the current process # Fall through to the polling loop is_child = False else: self._dead = True self._exitcode = (-os.WTERMSIG(wstatus) if os.WIFSIGNALED(wstatus) else os.WEXITSTATUS(wstatus)) return self._exitcode elif ( # pylint: disable=chained-comparison timeout is not None and timeout <= 0 and self._pid > 0): # Zero timeout try: if self._wait_child_poll(): return self._exitcode else: raise TimeoutExpired(timeout, pid=self._pid) except ChildProcessError: # We already checked is_running(), so we know it's still running raise TimeoutExpired(timeout, pid=self._pid) # pylint: disable=raise-missing-from while True: if is_child: try: if self._wait_child_poll(): return self._exitcode except ChildProcessError: # On Linux 5.3+ (and Python 3.9+), pidfd_open() may avoid a busy loop if hasattr(os, "pidfd_open"): assert self._pid > 0 assert timeout is not None try: pidfd = os.pidfd_open(self._pid) # pylint: disable=no-member except OSError: pass else: remaining_time = ((start_time + timeout) - time.monotonic() if timeout > 0 else 0) readfds, _, _ = select.select([pidfd], [], [], max( remaining_time, 0)) os.close(pidfd) if not readfds: # Timeout expired, and still not dead raise TimeoutExpired( # pylint: disable=raise-missing-from timeout, pid=self._pid) # Dead, but now it may be a zombie, so we need to keep watching it # Fall through to the normal monitoring code # Switch to pid_exists() is_child = False # Restart the loop so it gets checked immediately, not 0.01 seconds from now continue else: if not pid_exists(self._pid): with self._lock, self._exitcode_lock: return self._exitcode interval = 0.01 if timeout is not None: remaining_time = (start_time + timeout ) - time.monotonic() if timeout > 0 else 0 if remaining_time <= 0: raise TimeoutExpired(timeout, pid=self._pid) interval = min(interval, remaining_time) time.sleep(interval)
# Run jailer/firecracker and wait for the process to finish but store a reference to the child # process. 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)
def _init(self, command, *, stdin=None, stdout=None, stderr=None, **options): for key in ('universal_newlines', 'text', 'encoding', 'errors', 'bufsize'): if options.get(key): raise TypeError( "trio.Process only supports communicating over " "unbuffered byte streams; the '{}' option is not supported" .format(key)) self.stdin = None # type: Optional[SendStream] self.stdout = None # type: Optional[ReceiveStream] self.stderr = None # type: Optional[ReceiveStream] self.stdio = None # type: Optional[StapledStream] if os.name == "posix": if isinstance(command, str) and not options.get("shell"): raise TypeError( "command must be a sequence (not a string) if shell=False " "on UNIX systems") if not isinstance(command, str) and options.get("shell"): raise TypeError( "command must be a string (not a sequence) if shell=True " "on UNIX systems") self._wait_lock = Lock() if stdin == subprocess.PIPE: self.stdin, stdin = create_pipe_to_child_stdin() if stdout == subprocess.PIPE: self.stdout, stdout = create_pipe_from_child_output() if stderr == subprocess.STDOUT: # If we created a pipe for stdout, pass the same pipe for # stderr. If stdout was some non-pipe thing (DEVNULL or a # given FD), pass the same thing. If stdout was passed as # None, keep stderr as STDOUT to allow subprocess to dup # our stdout. Regardless of which of these is applicable, # don't create a new Trio stream for stderr -- if stdout # is piped, stderr will be intermixed on the stdout stream. if stdout is not None: stderr = stdout elif stderr == subprocess.PIPE: self.stderr, stderr = create_pipe_from_child_output() try: self._proc = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, **options) finally: # Close the parent's handle for each child side of a pipe; # we want the child to have the only copy, so that when # it exits we can read EOF on our side. if self.stdin is not None: os.close(stdin) if self.stdout is not None: os.close(stdout) if self.stderr is not None: os.close(stderr) self._pidfd = None if can_try_pidfd_open: try: fd = pidfd_open(self._proc.pid, 0) except OSError: # Well, we tried, but it didn't work (probably because we're # running on an older kernel, or in an older sandbox, that # hasn't been updated to support pidfd_open). We'll fall back # on waitid instead. pass else: # It worked! Wrap the raw fd up in a Python file object to # make sure it'll get closed. self._pidfd = open(fd) if self.stdin is not None and self.stdout is not None: self.stdio = StapledStream(self.stdin, self.stdout) self.args = self._proc.args self.pid = self._proc.pid