blob: 1b4e7759f2c24db2494d77adab0ec87723a3594f [file] [log] [blame]
# 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)