| # Copyright 1999-2012 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import errno |
| import io |
| import platform |
| import stat |
| import subprocess |
| import tempfile |
| import textwrap |
| from _emerge.SpawnProcess import SpawnProcess |
| from _emerge.EbuildBuildDir import EbuildBuildDir |
| from _emerge.EbuildIpcDaemon import EbuildIpcDaemon |
| import portage |
| from portage.elog import messages as elog_messages |
| from portage.localization import _ |
| from portage.package.ebuild._ipc.ExitCommand import ExitCommand |
| from portage.package.ebuild._ipc.QueryCommand import QueryCommand |
| from portage import shutil, os |
| from portage.util._pty import _create_pty_or_pipe |
| from portage.util import apply_secpass_permissions |
| |
| class AbstractEbuildProcess(SpawnProcess): |
| |
| __slots__ = ('phase', 'settings',) + \ |
| ('_build_dir', '_ipc_daemon', '_exit_command', '_exit_timeout_id') |
| |
| _phases_without_builddir = ('clean', 'cleanrm', 'depend', 'help',) |
| _phases_interactive_whitelist = ('config',) |
| _phases_without_cgroup = ('preinst', 'postinst', 'prerm', 'postrm', 'config') |
| |
| # Number of milliseconds to allow natural exit of the ebuild |
| # process after it has called the exit command via IPC. It |
| # doesn't hurt to be generous here since the scheduler |
| # continues to process events during this period, and it can |
| # return long before the timeout expires. |
| _exit_timeout = 10000 # 10 seconds |
| |
| # The EbuildIpcDaemon support is well tested, but this variable |
| # is left so we can temporarily disable it if any issues arise. |
| _enable_ipc_daemon = True |
| |
| def __init__(self, **kwargs): |
| SpawnProcess.__init__(self, **kwargs) |
| if self.phase is None: |
| phase = self.settings.get("EBUILD_PHASE") |
| if not phase: |
| phase = 'other' |
| self.phase = phase |
| |
| def _start(self): |
| |
| need_builddir = self.phase not in self._phases_without_builddir |
| |
| # This can happen if the pre-clean phase triggers |
| # die_hooks for some reason, and PORTAGE_BUILDDIR |
| # doesn't exist yet. |
| if need_builddir and \ |
| not os.path.isdir(self.settings['PORTAGE_BUILDDIR']): |
| msg = _("The ebuild phase '%s' has been aborted " |
| "since PORTAGE_BUILDDIR does not exist: '%s'") % \ |
| (self.phase, self.settings['PORTAGE_BUILDDIR']) |
| self._eerror(textwrap.wrap(msg, 72)) |
| self._set_returncode((self.pid, 1 << 8)) |
| self._async_wait() |
| return |
| |
| # Check if the cgroup hierarchy is in place. If it's not, mount it. |
| if (os.geteuid() == 0 and platform.system() == 'Linux' |
| and 'cgroup' in self.settings.features |
| and self.phase not in self._phases_without_cgroup): |
| cgroup_root = '/sys/fs/cgroup' |
| cgroup_portage = os.path.join(cgroup_root, 'portage') |
| |
| try: |
| # cgroup tmpfs |
| if not os.path.ismount(cgroup_root): |
| # we expect /sys/fs to be there already |
| if not os.path.isdir(cgroup_root): |
| os.mkdir(cgroup_root, 0o755) |
| subprocess.check_call(['mount', '-t', 'tmpfs', |
| '-o', 'rw,nosuid,nodev,noexec,mode=0755', |
| 'tmpfs', cgroup_root]) |
| |
| # portage subsystem |
| if not os.path.ismount(cgroup_portage): |
| if not os.path.isdir(cgroup_portage): |
| os.mkdir(cgroup_portage, 0o755) |
| subprocess.check_call(['mount', '-t', 'cgroup', |
| '-o', 'rw,nosuid,nodev,noexec,none,name=portage', |
| 'tmpfs', cgroup_portage]) |
| with open(os.path.join( |
| cgroup_portage, 'release_agent'), 'w') as f: |
| f.write(os.path.join(self.settings['PORTAGE_BIN_PATH'], |
| 'cgroup-release-agent')) |
| with open(os.path.join( |
| cgroup_portage, 'notify_on_release'), 'w') as f: |
| f.write('1') |
| else: |
| # Update release_agent if it no longer exists, because |
| # it refers to a temporary path when portage is updating |
| # itself. |
| release_agent = os.path.join( |
| cgroup_portage, 'release_agent') |
| try: |
| with open(release_agent) as f: |
| release_agent_path = f.readline().rstrip('\n') |
| except EnvironmentError: |
| release_agent_path = None |
| |
| if (release_agent_path is None or |
| not os.path.exists(release_agent_path)): |
| with open(release_agent, 'w') as f: |
| f.write(os.path.join( |
| self.settings['PORTAGE_BIN_PATH'], |
| 'cgroup-release-agent')) |
| |
| cgroup_path = tempfile.mkdtemp(dir=cgroup_portage, |
| prefix='%s:%s.' % (self.settings["CATEGORY"], |
| self.settings["PF"])) |
| except (subprocess.CalledProcessError, OSError): |
| pass |
| else: |
| self.cgroup = cgroup_path |
| |
| if self.background: |
| # Automatically prevent color codes from showing up in logs, |
| # since we're not displaying to a terminal anyway. |
| self.settings['NOCOLOR'] = 'true' |
| |
| if self._enable_ipc_daemon: |
| self.settings.pop('PORTAGE_EBUILD_EXIT_FILE', None) |
| if self.phase not in self._phases_without_builddir: |
| if 'PORTAGE_BUILDDIR_LOCKED' not in self.settings: |
| self._build_dir = EbuildBuildDir( |
| scheduler=self.scheduler, settings=self.settings) |
| self._build_dir.lock() |
| self.settings['PORTAGE_IPC_DAEMON'] = "1" |
| self._start_ipc_daemon() |
| else: |
| self.settings.pop('PORTAGE_IPC_DAEMON', None) |
| else: |
| # Since the IPC daemon is disabled, use a simple tempfile based |
| # approach to detect unexpected exit like in bug #190128. |
| self.settings.pop('PORTAGE_IPC_DAEMON', None) |
| if self.phase not in self._phases_without_builddir: |
| exit_file = os.path.join( |
| self.settings['PORTAGE_BUILDDIR'], |
| '.exit_status') |
| self.settings['PORTAGE_EBUILD_EXIT_FILE'] = exit_file |
| try: |
| os.unlink(exit_file) |
| except OSError: |
| if os.path.exists(exit_file): |
| # make sure it doesn't exist |
| raise |
| else: |
| self.settings.pop('PORTAGE_EBUILD_EXIT_FILE', None) |
| |
| if self.fd_pipes is None: |
| self.fd_pipes = {} |
| null_fd = None |
| if 0 not in self.fd_pipes and \ |
| self.phase not in self._phases_interactive_whitelist and \ |
| "interactive" not in self.settings.get("PROPERTIES", "").split(): |
| null_fd = os.open('/dev/null', os.O_RDONLY) |
| self.fd_pipes[0] = null_fd |
| |
| try: |
| SpawnProcess._start(self) |
| finally: |
| if null_fd is not None: |
| os.close(null_fd) |
| |
| def _init_ipc_fifos(self): |
| |
| input_fifo = os.path.join( |
| self.settings['PORTAGE_BUILDDIR'], '.ipc_in') |
| output_fifo = os.path.join( |
| self.settings['PORTAGE_BUILDDIR'], '.ipc_out') |
| |
| for p in (input_fifo, output_fifo): |
| |
| st = None |
| try: |
| st = os.lstat(p) |
| except OSError: |
| os.mkfifo(p) |
| else: |
| if not stat.S_ISFIFO(st.st_mode): |
| st = None |
| try: |
| os.unlink(p) |
| except OSError: |
| pass |
| os.mkfifo(p) |
| |
| apply_secpass_permissions(p, |
| uid=os.getuid(), |
| gid=portage.data.portage_gid, |
| mode=0o770, stat_cached=st) |
| |
| return (input_fifo, output_fifo) |
| |
| def _start_ipc_daemon(self): |
| self._exit_command = ExitCommand() |
| self._exit_command.reply_hook = self._exit_command_callback |
| query_command = QueryCommand(self.settings, self.phase) |
| commands = { |
| 'available_eclasses' : query_command, |
| 'best_version' : query_command, |
| 'eclass_path' : query_command, |
| 'exit' : self._exit_command, |
| 'has_version' : query_command, |
| 'license_path' : query_command, |
| 'master_repositories' : query_command, |
| 'repository_path' : query_command, |
| } |
| input_fifo, output_fifo = self._init_ipc_fifos() |
| self._ipc_daemon = EbuildIpcDaemon(commands=commands, |
| input_fifo=input_fifo, |
| output_fifo=output_fifo, |
| scheduler=self.scheduler) |
| self._ipc_daemon.start() |
| |
| def _exit_command_callback(self): |
| if self._registered: |
| # Let the process exit naturally, if possible. |
| self._exit_timeout_id = \ |
| self.scheduler.timeout_add(self._exit_timeout, |
| self._exit_command_timeout_cb) |
| |
| def _exit_command_timeout_cb(self): |
| if self._registered: |
| # If it doesn't exit naturally in a reasonable amount |
| # of time, kill it (solves bug #278895). We try to avoid |
| # this when possible since it makes sandbox complain about |
| # being killed by a signal. |
| self.cancel() |
| self._exit_timeout_id = \ |
| self.scheduler.timeout_add(self._cancel_timeout, |
| self._cancel_timeout_cb) |
| else: |
| self._exit_timeout_id = None |
| |
| return False # only run once |
| |
| def _cancel_timeout_cb(self): |
| self._exit_timeout_id = None |
| self.wait() |
| return False # only run once |
| |
| def _orphan_process_warn(self): |
| phase = self.phase |
| |
| msg = _("The ebuild phase '%s' with pid %s appears " |
| "to have left an orphan process running in the " |
| "background.") % (phase, self.pid) |
| |
| self._eerror(textwrap.wrap(msg, 72)) |
| |
| def _pipe(self, fd_pipes): |
| stdout_pipe = None |
| if not self.background: |
| stdout_pipe = fd_pipes.get(1) |
| got_pty, master_fd, slave_fd = \ |
| _create_pty_or_pipe(copy_term_size=stdout_pipe) |
| return (master_fd, slave_fd) |
| |
| def _can_log(self, slave_fd): |
| # With sesandbox, logging works through a pty but not through a |
| # normal pipe. So, disable logging if ptys are broken. |
| # See Bug #162404. |
| # TODO: Add support for logging via named pipe (fifo) with |
| # sesandbox, since EbuildIpcDaemon uses a fifo and it's known |
| # to be compatible with sesandbox. |
| return not ('sesandbox' in self.settings.features \ |
| and self.settings.selinux_enabled()) or os.isatty(slave_fd) |
| |
| def _killed_by_signal(self, signum): |
| msg = _("The ebuild phase '%s' has been " |
| "killed by signal %s.") % (self.phase, signum) |
| self._eerror(textwrap.wrap(msg, 72)) |
| |
| def _unexpected_exit(self): |
| |
| phase = self.phase |
| |
| msg = _("The ebuild phase '%s' has exited " |
| "unexpectedly. This type of behavior " |
| "is known to be triggered " |
| "by things such as failed variable " |
| "assignments (bug #190128) or bad substitution " |
| "errors (bug #200313). Normally, before exiting, bash should " |
| "have displayed an error message above. If bash did not " |
| "produce an error message above, it's possible " |
| "that the ebuild has called `exit` when it " |
| "should have called `die` instead. This behavior may also " |
| "be triggered by a corrupt bash binary or a hardware " |
| "problem such as memory or cpu malfunction. If the problem is not " |
| "reproducible or it appears to occur randomly, then it is likely " |
| "to be triggered by a hardware problem. " |
| "If you suspect a hardware problem then you should " |
| "try some basic hardware diagnostics such as memtest. " |
| "Please do not report this as a bug unless it is consistently " |
| "reproducible and you are sure that your bash binary and hardware " |
| "are functioning properly.") % phase |
| |
| self._eerror(textwrap.wrap(msg, 72)) |
| |
| def _eerror(self, lines): |
| self._elog('eerror', lines) |
| |
| def _elog(self, elog_funcname, lines): |
| out = io.StringIO() |
| phase = self.phase |
| elog_func = getattr(elog_messages, elog_funcname) |
| global_havecolor = portage.output.havecolor |
| try: |
| portage.output.havecolor = \ |
| self.settings.get('NOCOLOR', 'false').lower() in ('no', 'false') |
| for line in lines: |
| elog_func(line, phase=phase, key=self.settings.mycpv, out=out) |
| finally: |
| portage.output.havecolor = global_havecolor |
| msg = out.getvalue() |
| if msg: |
| log_path = None |
| if self.settings.get("PORTAGE_BACKGROUND") != "subprocess": |
| log_path = self.settings.get("PORTAGE_LOG_FILE") |
| self.scheduler.output(msg, log_path=log_path) |
| |
| def _log_poll_exception(self, event): |
| self._elog("eerror", |
| ["%s received strange poll event: %s\n" % \ |
| (self.__class__.__name__, event,)]) |
| |
| def _set_returncode(self, wait_retval): |
| SpawnProcess._set_returncode(self, wait_retval) |
| |
| if self._exit_timeout_id is not None: |
| self.scheduler.source_remove(self._exit_timeout_id) |
| self._exit_timeout_id = None |
| |
| if self._ipc_daemon is not None: |
| self._ipc_daemon.cancel() |
| if self._exit_command.exitcode is not None: |
| self.returncode = self._exit_command.exitcode |
| else: |
| if self.returncode < 0: |
| if not self.cancelled: |
| self._killed_by_signal(-self.returncode) |
| else: |
| self.returncode = 1 |
| if not self.cancelled: |
| self._unexpected_exit() |
| if self._build_dir is not None: |
| self._build_dir.unlock() |
| self._build_dir = None |
| elif not self.cancelled: |
| exit_file = self.settings.get('PORTAGE_EBUILD_EXIT_FILE') |
| if exit_file and not os.path.exists(exit_file): |
| if self.returncode < 0: |
| if not self.cancelled: |
| self._killed_by_signal(-self.returncode) |
| else: |
| self.returncode = 1 |
| if not self.cancelled: |
| self._unexpected_exit() |