summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2013-08-19 10:45:52 +0200
committerZac Medico <zmedico@gentoo.org>2013-08-19 02:15:55 -0700
commitb01a1b90d8c5c9ebc9b2c956520f3096cc07342d (patch)
treed6628910007e7330bfad0cfdef9e2f7230fb0544
parentAdd FEATURES=ipc-sandbox to isolate IPC from host. (diff)
downloadportage-b01a1b90d8c5c9ebc9b2c956520f3096cc07342d.tar.gz
portage-b01a1b90d8c5c9ebc9b2c956520f3096cc07342d.tar.bz2
portage-b01a1b90d8c5c9ebc9b2c956520f3096cc07342d.zip
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.
-rw-r--r--man/make.conf.54
-rw-r--r--pym/_emerge/AbstractEbuildProcess.py39
-rw-r--r--pym/_emerge/SpawnProcess.py37
-rw-r--r--pym/portage/const.py2
-rw-r--r--pym/portage/process.py17
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")