From b01a1b90d8c5c9ebc9b2c956520f3096cc07342d Mon Sep 17 00:00:00 2001 From: =?utf8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 19 Aug 2013 10:45:52 +0200 Subject: [PATCH] Add FEATURES=cgroup to isolate phase processes. We create a cgroup for each ebuild. The ebuild processes are put in that cgroup, therefore allowing us to keep track of their replication. Once ebuild phase is over, we can happily kill all orphans. --- man/make.conf.5 | 4 +++ pym/_emerge/AbstractEbuildProcess.py | 39 ++++++++++++++++++++++++++++ pym/_emerge/SpawnProcess.py | 37 +++++++++++++++++++++++++- pym/portage/const.py | 2 +- pym/portage/process.py | 17 +++++++++--- 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/man/make.conf.5 b/man/make.conf.5 index 91817aec5..ab9b44e64 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -276,6 +276,10 @@ like "File not recognized: File truncated"), try recompiling the application with ccache disabled before reporting a bug. Unless you are doing development work, do not enable ccache. .TP +.B cgroup +Use Linux control group to control processes spawned by ebuilds. This allows +emerge to safely kill all subprocesses when ebuild phase exits. +.TP .B clean\-logs Enable automatic execution of the command specified by the PORT_LOGDIR_CLEAN variable. The default PORT_LOGDIR_CLEAN setting will diff --git a/pym/_emerge/AbstractEbuildProcess.py b/pym/_emerge/AbstractEbuildProcess.py index 6d12cd999..31127f474 100644 --- a/pym/_emerge/AbstractEbuildProcess.py +++ b/pym/_emerge/AbstractEbuildProcess.py @@ -2,7 +2,9 @@ # Distributed under the terms of the GNU General Public License v2 import io +import platform import stat +import subprocess import textwrap from _emerge.SpawnProcess import SpawnProcess from _emerge.EbuildBuildDir import EbuildBuildDir @@ -20,8 +22,10 @@ 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 @@ -59,6 +63,41 @@ class AbstractEbuildProcess(SpawnProcess): 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') + cgroup_path = os.path.join(cgroup_portage, + '%s:%s' % (self.settings["CATEGORY"], + self.settings["PF"])) + 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]) + + # the ebuild cgroup + if not os.path.isdir(cgroup_path): + os.mkdir(cgroup_path) + 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. diff --git a/pym/_emerge/SpawnProcess.py b/pym/_emerge/SpawnProcess.py index 3b363a2a8..60bd3f1c9 100644 --- a/pym/_emerge/SpawnProcess.py +++ b/pym/_emerge/SpawnProcess.py @@ -7,7 +7,9 @@ except ImportError: # http://bugs.jython.org/issue1074 fcntl = None +import errno import platform +import signal import sys from _emerge.SubProcess import SubProcess @@ -29,7 +31,7 @@ class SpawnProcess(SubProcess): _spawn_kwarg_names = ("env", "opt_name", "fd_pipes", "uid", "gid", "groups", "umask", "logfile", - "path_lookup", "pre_exec", "close_fds") + "path_lookup", "pre_exec", "close_fds", "cgroup") __slots__ = ("args",) + \ _spawn_kwarg_names + ("_pipe_logger", "_selinux_type",) @@ -179,3 +181,36 @@ class SpawnProcess(SubProcess): pipe_logger.removeExitListener(self._pipe_logger_exit) pipe_logger.cancel() pipe_logger.wait() + + def _set_returncode(self, wait_retval): + SubProcess._set_returncode(self, wait_retval) + + if self.cgroup: + def get_pids(cgroup): + try: + with open(os.path.join(cgroup, 'cgroup.procs'), 'r') as f: + return f.read().split() + except OSError: + # cgroup removed already? + return [] + + def kill_all(pids, sig): + for p in pids: + try: + os.kill(int(p), sig) + except OSError as e: + if e.errno != errno.ESRCH: + raise + + # step 1: kill all orphans + pids = get_pids(self.cgroup) + if pids: + kill_all(pids, signal.SIGTERM) + + # step 2: remove the cgroup + try: + os.rmdir(self.cgroup) + except OSError: + # it may be removed already, or busy + # we can't do anything good about it + pass diff --git a/pym/portage/const.py b/pym/portage/const.py index 88c199b2c..214ede414 100644 --- a/pym/portage/const.py +++ b/pym/portage/const.py @@ -96,7 +96,7 @@ EBUILD_PHASES = ("pretend", "setup", "unpack", "prepare", "configure" "nofetch", "config", "info", "other") SUPPORTED_FEATURES = frozenset([ "assume-digests", "binpkg-logs", "buildpkg", "buildsyspkg", "candy", - "ccache", "chflags", "clean-logs", + "ccache", "cgroup", "chflags", "clean-logs", "collision-protect", "compress-build-logs", "compressdebug", "compress-index", "config-protect-if-modified", "digest", "distcc", "distcc-pump", "distlocks", diff --git a/pym/portage/process.py b/pym/portage/process.py index 22c6a8823..20ef97d4d 100644 --- a/pym/portage/process.py +++ b/pym/portage/process.py @@ -185,7 +185,7 @@ def cleanup(): def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, uid=None, gid=None, groups=None, umask=None, logfile=None, path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False, - unshare_ipc=False): + unshare_ipc=False, cgroup=None): """ Spawns a given command. @@ -222,6 +222,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, @type unshare_net: Boolean @param unshare_ipc: If True, IPC will be unshared from the spawned process @type unshare_ipc: Boolean + @param cgroup: CGroup path to bind the process to + @type cgroup: String logfile requires stdout and stderr to be assigned to this process (ie not pointed somewhere else.) @@ -300,7 +302,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, try: _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, pre_exec, close_fds, - unshare_net, unshare_ipc) + unshare_net, unshare_ipc, cgroup) except SystemExit: raise except Exception as e: @@ -370,7 +372,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, return 0 def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, - pre_exec, close_fds, unshare_net, unshare_ipc): + pre_exec, close_fds, unshare_net, unshare_ipc, cgroup): """ Execute a given binary with options @@ -399,6 +401,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, @type unshare_net: Boolean @param unshare_ipc: If True, IPC will be unshared from the spawned process @type unshare_ipc: Boolean + @param cgroup: CGroup path to bind the process to + @type cgroup: String @rtype: None @return: Never returns (calls os.execve) """ @@ -435,6 +439,13 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, _setup_pipes(fd_pipes, close_fds=close_fds) + # Add to cgroup + # it's better to do it from the child since we can guarantee + # it is done before we start forking children + if cgroup: + with open(os.path.join(cgroup, 'cgroup.procs'), 'a') as f: + f.write('%d\n' % os.getpid()) + # Unshare (while still uid==0) if unshare_net or unshare_ipc: filename = find_library("c") -- 2.26.2