#!/usr/bin/python # Copyright 2010-2012 Gentoo Foundation # 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 errno import logging import os import pickle import platform import select import signal import sys import time import traceback def debug_signal(signum, frame): import pdb pdb.set_trace() if platform.python_implementation() == 'Jython': debug_signum = signal.SIGUSR2 # bug #424259 else: debug_signum = signal.SIGUSR1 signal.signal(debug_signum, debug_signal) # Avoid sandbox violations after python upgrade. pym_path = os.path.join(os.path.dirname( os.path.dirname(os.path.realpath(__file__))), "pym") if os.environ.get("SANDBOX_ON") == "1": sandbox_write = os.environ.get("SANDBOX_WRITE", "").split(":") if pym_path not in sandbox_write: sandbox_write.append(pym_path) os.environ["SANDBOX_WRITE"] = \ ":".join(filter(None, sandbox_write)) import portage portage._disable_legacy_globals() class EbuildIpc(object): # Timeout for each individual communication attempt (we retry # as long as the daemon process appears to be alive). _COMMUNICATE_RETRY_TIMEOUT_SECONDS = 15 _BUFSIZE = 4096 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( portage.localization._( 'ebuild-ipc timed out %s after %d seconds,' + \ ' retrying...\n') % (when, time_elapsed), 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 _wait(self, pid, pr, 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() while True: try: events = select.select([pr], [], [], self._COMMUNICATE_RETRY_TIMEOUT_SECONDS) except select.error as e: portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % \ (portage.localization._('during select'), e), level=logging.ERROR, noiselevel=-1) continue if events[0]: break if self._daemon_is_alive(): self._timeout_retry_msg(start_time, msg) else: self._no_daemon_msg() try: os.kill(pid, signal.SIGKILL) os.waitpid(pid, 0) except OSError as e: portage.util.writemsg_level( "ebuild-ipc: %s\n" % (e,), level=logging.ERROR, noiselevel=-1) return 2 try: wait_retval = os.waitpid(pid, 0) except OSError as e: portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % (msg, e), level=logging.ERROR, noiselevel=-1) return 2 if not os.WIFEXITED(wait_retval[1]): portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % (msg, portage.localization._('subprocess failure: %s') % \ wait_retval[1]), level=logging.ERROR, noiselevel=-1) return 2 return os.WEXITSTATUS(wait_retval[1]) def _receive_reply(self, input_fd): # Timeouts are handled by the parent process, so just # block until input is available. For maximum portability, # use a single atomic read. buf = None while True: try: events = select.select([input_fd], [], []) except select.error as e: portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % \ (portage.localization._('during select for read'), e), level=logging.ERROR, noiselevel=-1) continue if events[0]: # For maximum portability, use os.read() here since # array.fromfile() and file.read() are both known to # erroneously return an empty string from this # non-blocking fifo stream on FreeBSD (bug #337465). try: buf = os.read(input_fd, self._BUFSIZE) except OSError as e: if e.errno != errno.EAGAIN: portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % \ (portage.localization._('read error'), e), level=logging.ERROR, noiselevel=-1) break # Assume that another event will be generated # if there's any relevant data. continue # Only one (atomic) read should be necessary. if buf: break retval = 2 if not buf: portage.util.writemsg_level( "ebuild-ipc: %s\n" % \ (portage.localization._('read failed'),), 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( "ebuild-ipc: %s\n" % (e,), 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. pr, pw = os.pipe() pid = os.fork() if pid == 0: retval = 2 try: os.close(pr) # File streams are in unbuffered mode since we do atomic # read and write of whole pickles. output_file = open(self.ipc_in_fifo, 'wb', 0) output_file.write(pickle.dumps(args)) output_file.close() retval = os.EX_OK except SystemExit: raise except: traceback.print_exc() finally: os._exit(retval) os.close(pw) msg = portage.localization._('during write') retval = self._wait(pid, pr, msg) os.close(pr) if retval != os.EX_OK: portage.util.writemsg_level( "ebuild-ipc: %s: %s\n" % (msg, portage.localization._('subprocess failure: %s') % \ retval), level=logging.ERROR, noiselevel=-1) return retval if not self._daemon_is_alive(): self._no_daemon_msg() return 2 pr, pw = os.pipe() pid = os.fork() if pid == 0: retval = 2 try: os.close(pr) retval = self._receive_reply(input_fd) except SystemExit: raise except: traceback.print_exc() finally: os._exit(retval) os.close(pw) retval = self._wait(pid, pr, portage.localization._('during read')) os.close(pr) os.close(input_fd) return retval def ebuild_ipc_main(args): ebuild_ipc = EbuildIpc() return ebuild_ipc.communicate(args) if __name__ == '__main__': sys.exit(ebuild_ipc_main(sys.argv[1:]))