Add FEATURES=cgroup to isolate phase processes.
authorMichał Górny <mgorny@gentoo.org>
Mon, 19 Aug 2013 08:45:52 +0000 (10:45 +0200)
committerZac Medico <zmedico@gentoo.org>
Mon, 19 Aug 2013 09:15:55 +0000 (02:15 -0700)
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
pym/_emerge/AbstractEbuildProcess.py
pym/_emerge/SpawnProcess.py
pym/portage/const.py
pym/portage/process.py

index 91817aec5027b67aab14cf549b0fe02e0769b475..ab9b44e644b27ced563ed10af516a206cbc2f922 100644 (file)
@@ -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
index 6d12cd9990124c2487fcee94317807ecd2750d49..31127f474305815d6c7a94fdaf8ed32a4c1f6c68 100644 (file)
@@ -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.
index 3b363a2a862de69da4ea9c8c8499ca3138938c9a..60bd3f1c95fa9832c89fb739dfc7f256271920ba 100644 (file)
@@ -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
index 88c199b2ce8c9d3097b480e0626dea52e632d545..214ede41453eb04b9f13a82c2f0cbb4cb6d4daaf 100644 (file)
@@ -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",
index 22c6a882329c8f0dc08fc9f3f5ffca903a361624..20ef97d4d6c5549a2bc54ccdf3c9b6bb31fe0961 100644 (file)
@@ -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")