| # Copyright 1999-2020 Gentoo Authors |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import functools |
| 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 os |
| from portage.util.futures import asyncio |
| from portage.util._pty import _create_pty_or_pipe |
| from portage.util import apply_secpass_permissions |
| |
| portage.proxy.lazyimport.lazyimport( |
| globals(), |
| "portage.package.ebuild.doebuild:_global_pid_phases", |
| ) |
| |
| |
| class AbstractEbuildProcess(SpawnProcess): |
| |
| __slots__ = ("phase", "settings",) + ( |
| "_build_dir", |
| "_build_dir_unlock", |
| "_ipc_daemon", |
| "_exit_command", |
| "_exit_timeout_id", |
| "_start_future", |
| ) |
| |
| _phases_without_builddir = ( |
| "clean", |
| "cleanrm", |
| "depend", |
| "help", |
| ) |
| _phases_interactive_whitelist = ("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 = 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.returncode = 1 |
| 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 _global_pid_phases |
| ): |
| 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" |
| |
| start_ipc_daemon = False |
| if self._enable_ipc_daemon: |
| self.settings.pop("PORTAGE_EBUILD_EXIT_FILE", None) |
| if self.phase not in self._phases_without_builddir: |
| start_ipc_daemon = True |
| if "PORTAGE_BUILDDIR_LOCKED" not in self.settings: |
| self._build_dir = EbuildBuildDir( |
| scheduler=self.scheduler, settings=self.settings |
| ) |
| self._start_future = self._build_dir.async_lock() |
| self._start_future.add_done_callback( |
| functools.partial( |
| self._start_post_builddir_lock, |
| start_ipc_daemon=start_ipc_daemon, |
| ) |
| ) |
| return |
| 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) |
| |
| self._start_post_builddir_lock(start_ipc_daemon=start_ipc_daemon) |
| |
| def _start_post_builddir_lock(self, lock_future=None, start_ipc_daemon=False): |
| if lock_future is not None: |
| if lock_future is not self._start_future: |
| raise AssertionError("lock_future is not self._start_future") |
| self._start_future = None |
| if lock_future.cancelled(): |
| self._build_dir = None |
| self.cancelled = True |
| self._was_cancelled() |
| self._async_wait() |
| return |
| |
| lock_future.result() |
| |
| if start_ipc_daemon: |
| self.settings["PORTAGE_IPC_DAEMON"] = "1" |
| self._start_ipc_daemon() |
| |
| 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 |
| |
| self.log_filter_file = self.settings.get("PORTAGE_LOG_FILTER_FILE_CMD") |
| 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.call_later( |
| 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.call_later( |
| self._cancel_timeout, self._cancel_timeout_cb |
| ) |
| else: |
| self._exit_timeout_id = None |
| |
| def _cancel_timeout_cb(self): |
| self._exit_timeout_id = None |
| self._async_waitpid() |
| |
| 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 _async_waitpid_cb(self, *args, **kwargs): |
| """ |
| Override _async_waitpid_cb to perform cleanup that is |
| not necessarily idempotent. |
| """ |
| SpawnProcess._async_waitpid_cb(self, *args, **kwargs) |
| |
| if self._exit_timeout_id is not None: |
| self._exit_timeout_id.cancel() |
| 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() |
| |
| 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() |
| |
| def _async_wait(self): |
| """ |
| Override _async_wait to asynchronously unlock self._build_dir |
| when necessary. |
| """ |
| if self._build_dir is None: |
| SpawnProcess._async_wait(self) |
| elif self._build_dir_unlock is None: |
| if self.returncode is None: |
| raise asyncio.InvalidStateError("Result is not ready for %s" % (self,)) |
| self._async_unlock_builddir(returncode=self.returncode) |
| |
| def _async_unlock_builddir(self, returncode=None): |
| """ |
| Release the lock asynchronously, and if a returncode parameter |
| is given then set self.returncode and notify exit listeners. |
| """ |
| if self._build_dir_unlock is not None: |
| raise AssertionError("unlock already in progress") |
| if returncode is not None: |
| # The returncode will be set after unlock is complete. |
| self.returncode = None |
| self._build_dir_unlock = self._build_dir.async_unlock() |
| # Unlock only once. |
| self._build_dir = None |
| self._build_dir_unlock.add_done_callback( |
| functools.partial(self._unlock_builddir_exit, returncode=returncode) |
| ) |
| |
| def _unlock_builddir_exit(self, unlock_future, returncode=None): |
| # Normally, async_unlock should not raise an exception here. |
| unlock_future.cancelled() or unlock_future.result() |
| if returncode is not None: |
| if unlock_future.cancelled(): |
| self.cancelled = True |
| self._was_cancelled() |
| else: |
| self.returncode = returncode |
| SpawnProcess._async_wait(self) |