#!/usr/bin/env python # Copyright 2010-2022 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # # This is a helper which ebuild processes can use # to communicate with portage's main python process. import os import signal # For compatibility with Python < 3.8 raise_signal = getattr( signal, "raise_signal", lambda signum: os.kill(os.getpid(), signum) ) # Inherit from KeyboardInterrupt to avoid a traceback from asyncio. class SignalInterrupt(KeyboardInterrupt): def __init__(self, signum): self.signum = signum try: def signal_interrupt(signum, _frame): raise SignalInterrupt(signum) def debug_signal(_signum, _frame): import pdb pdb.set_trace() # Prevent "[Errno 32] Broken pipe" exceptions when writing to a pipe. signal.signal(signal.SIGPIPE, signal.SIG_DFL) signal.signal(signal.SIGTERM, signal_interrupt) signal.signal(signal.SIGUSR1, debug_signal) import errno import logging import pickle import sys import time if os.path.isfile( os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), ".portage_not_installed", ) ): pym_paths = [ os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "lib" ) ] sys.path.insert(0, pym_paths[0]) else: import sysconfig pym_paths = [ os.path.join(sysconfig.get_path("purelib"), x) for x in ("_emerge", "portage") ] # Avoid sandbox violations after Python upgrade. if os.environ.get("SANDBOX_ON") == "1": sandbox_write = os.environ.get("SANDBOX_WRITE", "").split(":") for pym_path in pym_paths: if pym_path not in sandbox_write: sandbox_write.append(pym_path) os.environ["SANDBOX_WRITE"] = ":".join(filter(None, sandbox_write)) del pym_path, sandbox_write del pym_paths import portage portage._internal_caller = True portage._disable_legacy_globals() from portage.util._eventloop.global_event_loop import global_event_loop from _emerge.AbstractPollTask import AbstractPollTask from _emerge.PipeReader import PipeReader RETURNCODE_WRITE_FAILED = 2 class FifoWriter(AbstractPollTask): __slots__ = ("buf", "fifo", "_fd") def _start(self): try: self._fd = os.open(self.fifo, os.O_WRONLY | os.O_NONBLOCK) except OSError as e: if e.errno == errno.ENXIO: # This happens if the daemon has been killed. self.returncode = RETURNCODE_WRITE_FAILED self._unregister() self._async_wait() return else: raise self.scheduler.add_writer(self._fd, self._output_handler) self._registered = True def _output_handler(self): # The whole buf should be able to fit in the fifo with # a single write call, so there's no valid reason for # os.write to raise EAGAIN here. fd = self._fd buf = self.buf while buf: try: buf = buf[os.write(fd, buf) :] except OSError: self.returncode = RETURNCODE_WRITE_FAILED self._async_wait() return self.returncode = os.EX_OK self._async_wait() def _cancel(self): self.returncode = self._cancelled_returncode self._unregister() def _unregister(self): self._registered = False if self._fd is not None: self.scheduler.remove_writer(self._fd) os.close(self._fd) self._fd = None class EbuildIpc: # Timeout for each individual communication attempt (we retry # as long as the daemon process appears to be alive). _COMMUNICATE_RETRY_TIMEOUT = 15 # seconds def __init__(self): self.fifo_dir = os.environ["PORTAGE_BUILDDIR"] self.ipc_in_fifo = os.path.join(self.fifo_dir, ".ipc", "in") self.ipc_out_fifo = os.path.join(self.fifo_dir, ".ipc", "out") self.ipc_lock_file = os.path.join(self.fifo_dir, ".ipc", "lock") def _daemon_is_alive(self): try: builddir_lock = portage.locks.lockfile( self.fifo_dir, wantnewlockfile=True, flags=os.O_NONBLOCK ) except portage.exception.TryAgain: return True else: portage.locks.unlockfile(builddir_lock) return False def communicate(self, args): # Make locks quiet since unintended locking messages displayed on # stdout could corrupt the intended output of this program. portage.locks._quiet = True lock_obj = portage.locks.lockfile(self.ipc_lock_file, unlinkfile=True) try: return self._communicate(args) finally: portage.locks.unlockfile(lock_obj) def _timeout_retry_msg(self, start_time, when): time_elapsed = time.time() - start_time portage.util.writemsg_level( f"ebuild-ipc timed out {when} after {time_elapsed} seconds, retrying...\n", level=logging.ERROR, noiselevel=-1, ) def _no_daemon_msg(self): portage.util.writemsg_level( portage.localization._("ebuild-ipc: daemon process not detected\n"), level=logging.ERROR, noiselevel=-1, ) def _run_writer(self, fifo_writer, msg): """ Wait on pid and return an appropriate exit code. This may return unsuccessfully due to timeout if the daemon process does not appear to be alive. """ start_time = time.time() fifo_writer.start() eof = fifo_writer.poll() is not None while not eof: fifo_writer._wait_loop(timeout=self._COMMUNICATE_RETRY_TIMEOUT) eof = fifo_writer.poll() is not None if eof: break elif self._daemon_is_alive(): self._timeout_retry_msg(start_time, msg) else: fifo_writer.cancel() self._no_daemon_msg() fifo_writer.wait() return 2 return fifo_writer.wait() def _receive_reply(self, input_fd): start_time = time.time() pipe_reader = PipeReader( input_files={"input_fd": input_fd}, scheduler=global_event_loop() ) pipe_reader.start() eof = pipe_reader.poll() is not None while not eof: pipe_reader._wait_loop(timeout=self._COMMUNICATE_RETRY_TIMEOUT) eof = pipe_reader.poll() is not None if not eof: if self._daemon_is_alive(): self._timeout_retry_msg( start_time, portage.localization._("during read") ) else: pipe_reader.cancel() self._no_daemon_msg() return 2 buf = pipe_reader.getvalue() retval = 2 if not buf: portage.util.writemsg_level( f"ebuild-ipc: {portage.localization._('read failed')}\n", level=logging.ERROR, noiselevel=-1, ) else: try: reply = pickle.loads(buf) except SystemExit: raise except Exception as e: # The pickle module can raise practically # any exception when given corrupt data. portage.util.writemsg_level( f"ebuild-ipc: {e}\n", level=logging.ERROR, noiselevel=-1 ) else: (out, err, retval) = reply if out: portage.util.writemsg_stdout(out, noiselevel=-1) if err: portage.util.writemsg(err, noiselevel=-1) return retval def _communicate(self, args): if not self._daemon_is_alive(): self._no_daemon_msg() return 2 # Open the input fifo before the output fifo, in order to make it # possible for the daemon to send a reply without blocking. This # improves performance, and also makes it possible for the daemon # to do a non-blocking write without a race condition. input_fd = os.open(self.ipc_out_fifo, os.O_RDONLY | os.O_NONBLOCK) # Use forks so that the child process can handle blocking IO # un-interrupted, while the parent handles all timeout # considerations. This helps to avoid possible race conditions # from interference between timeouts and blocking IO operations. msg = portage.localization._("during write") retval = self._run_writer( FifoWriter( buf=pickle.dumps(args), fifo=self.ipc_in_fifo, scheduler=global_event_loop(), ), msg, ) if retval != os.EX_OK: portage.util.writemsg_level( f"ebuild-ipc: {msg}: subprocess failure: {retval}\n", level=logging.ERROR, noiselevel=-1, ) return retval if not self._daemon_is_alive(): self._no_daemon_msg() return 2 return self._receive_reply(input_fd) def ebuild_ipc_main(args): ebuild_ipc = EbuildIpc() return ebuild_ipc.communicate(args) if __name__ == "__main__": try: sys.exit(ebuild_ipc_main(sys.argv[1:])) finally: global_event_loop().close() except KeyboardInterrupt as e: # Prevent traceback on ^C signum = getattr(e, "signum", signal.SIGINT) signal.signal(signum, signal.SIG_DFL) raise_signal(signum)