Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / cgroups.py
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """A class for managing the Linux cgroup subsystem."""
6
7 from __future__ import print_function
8
9 import contextlib
10 import errno
11 import os
12 import signal
13 import time
14
15 from chromite.lib import cros_build_lib
16 from chromite.lib import locking
17 from chromite.lib import osutils
18 from chromite.lib import signals
19 from chromite.lib import sudo
20
21
22 # Rough hierarchy sketch:
23 # - all cgroup aware cros code should nest here.
24 # - No cros code should modify this namespace- this is user/system configurable
25 # - only.  A release_agent can be specified, although we won't use it.
26 # cros/
27 #
28 # - cbuildbot instances land here only when they're cleaning their task pool.
29 # - this root namespace is *not* auto-removed; it's left so that user/system
30 # - configuration is persistant.
31 # cros/%(process-name)s/
32 # cros/cbuildbot/
33 #
34 # - a cbuildbot job pool, owned by pid.  These are autocleaned.
35 # cros/cbuildbot/%(pid)i/
36 #
37 # - a job pool using process that was invoked by cbuildbot.
38 # - for example, cros/cbuildbot/42/cros_sdk:34
39 # - this pattern continues arbitrarily deep, and is autocleaned.
40 # cros/cbuildbot/%(pid1)i/%(basename_of_pid2)s:%(pid2)i/
41 #
42 # An example for cros_sdk (pid 552) would be:
43 # cros/cros_sdk/552/
44 # and it's children would be accessible in 552/tasks, or
45 # would create their own namespace w/in and assign themselves to it.
46
47
48 class _GroupWasRemoved(Exception):
49   """Exception representing when a group was unexpectedly removed.
50
51   Via design, this should only be possible when instantiating a new
52   pool, but the parent pool has been removed- this means effectively that
53   we're supposed to shutdown (either we've been sigterm'd and ignored it,
54   or it's imminent).
55   """
56
57
58 def _FileContains(filename, strings):
59   """Greps a group of expressions, returns whether all were found."""
60   contents = osutils.ReadFile(filename)
61   return all(s in contents for s in strings)
62
63
64 def EnsureInitialized(functor):
65   """Decorator for Cgroup methods to ensure the method is ran only if inited"""
66
67   def f(self, *args, **kwargs):
68     # pylint: disable=W0212
69     self.Instantiate()
70     return functor(self, *args, **kwargs)
71
72   # Dummy up our wrapper to make it look like what we're wrapping,
73   # and expose the underlying docstrings.
74   f.__name__ = functor.__name__
75   f.__doc__ = functor.__doc__
76   f.__module__ = functor.__module__
77   return f
78
79
80 class Cgroup(object):
81
82   """Class representing a group in cgroups hierarchy.
83
84   Note the instance may not exist on disk; it will be created as necessary.
85   Additionally, because cgroups is kernel maintained (and mutated on the fly
86   by processes using it), chunks of this class are /explicitly/ designed to
87   always go back to disk and recalculate values.
88
89   Attributes:
90     path: Absolute on disk pathway to the cgroup directory.
91     tasks: Pids contained in this immediate cgroup, and the owning pids of
92       any first level groups nested w/in us.
93     all_tasks: All Pids, and owners of nested groups w/in this point in
94       the hierarchy.
95     nested_groups: The immediate cgroups nested w/in this one.  If this
96       cgroup is 'cbuildbot/buildbot', 'cbuildbot' would have a nested_groups
97       of [Cgroup('cbuildbot/buildbot')] for example.
98     all_nested_groups: All cgroups nested w/in this one, regardless of depth.
99     pid_owner: Which pid owns this cgroup, if the cgroup is following cros
100       conventions for group naming.
101   """
102
103   NEEDED_SUBSYSTEMS = ('cpuset',)
104   PROC_PATH = '/proc/cgroups'
105   _MOUNT_ROOT_POTENTIALS = ('/sys/fs/cgroup',)
106   _MOUNT_ROOT_FALLBACK = '/dev/cgroup'
107   CGROUP_ROOT = None
108   MOUNT_ROOT = None
109   # Whether or not the cgroup implementation does auto inheritance via
110   # cgroup.clone_children
111   _SUPPORTS_AUTOINHERIT = False
112
113   @classmethod
114   @cros_build_lib.MemoizedSingleCall
115   def InitSystem(cls):
116     """If cgroups are supported, initialize the system state"""
117     if not cls.IsSupported():
118       return False
119
120     def _EnsureMounted(mnt, args):
121       if _FileContains('/proc/mounts', [mnt]):
122         return True
123
124       # Grab a lock so in the off chance we have multiple programs (like two
125       # cros_sdk launched in parallel) running this init logic, we don't end
126       # up mounting multiple times.
127       lock_path = '/tmp/.chromite.cgroups.lock'
128       with locking.FileLock(lock_path, 'cgroup lock') as lock:
129         lock.write_lock()
130         if _FileContains('/proc/mounts', [mnt]):
131           return True
132
133         # Not all distros mount cgroup_root to sysfs.
134         osutils.SafeMakedirs(mnt, sudo=True)
135         cros_build_lib.SudoRunCommand(['mount'] + args + [mnt], print_cmd=False)
136
137       return True
138
139     mount_root_args = ['-t', 'tmpfs', 'cgroup_root']
140
141     opts = ','.join(cls.NEEDED_SUBSYSTEMS)
142     cgroup_root_args = ['-t', 'cgroup', '-o', opts, 'cros']
143
144     return _EnsureMounted(cls.MOUNT_ROOT, mount_root_args) and \
145         _EnsureMounted(cls.CGROUP_ROOT, cgroup_root_args)
146
147   @classmethod
148   @cros_build_lib.MemoizedSingleCall
149   def IsUsable(cls):
150     """Function to sanity check if everything is setup to use cgroups"""
151     if not cls.InitSystem():
152       return False
153     cls._SUPPORTS_AUTOINHERIT = os.path.exists(
154         os.path.join(cls.CGROUP_ROOT, 'cgroup.clone_children'))
155     return True
156
157   @classmethod
158   @cros_build_lib.MemoizedSingleCall
159   def IsSupported(cls):
160     """Sanity check as to whether or not cgroups are supported."""
161     # Is the cgroup subsystem even enabled?
162
163     if not os.path.exists(cls.PROC_PATH):
164       return False
165
166     # Does it support the subsystems we want?
167     if not _FileContains(cls.PROC_PATH, cls.NEEDED_SUBSYSTEMS):
168       return False
169
170     for potential in cls._MOUNT_ROOT_POTENTIALS:
171       if os.path.exists(potential):
172         cls.MOUNT_ROOT = potential
173         break
174     else:
175       cls.MOUNT_ROOT = cls._MOUNT_ROOT_FALLBACK
176
177     cls.CGROUP_ROOT = os.path.join(cls.MOUNT_ROOT, 'cros')
178     return True
179
180   def __init__(self, namespace, autoclean=True, lazy_init=False, parent=None,
181                _is_root=False, _overwrite=True):
182     """Initalize a cgroup instance.
183
184     Args:
185       namespace: What cgroup namespace is this in?  cbuildbot/1823 for example.
186       autoclean: Should this cgroup be removed once unused?
187       lazy_init: Should we create the cgroup immediately, or when needed?
188       parent: A Cgroup instance; if the namespace is cbuildbot/1823, then the
189         parent *must* be the cgroup instance for namespace cbuildbot.
190       _is_root:  Internal option, shouldn't be used by consuming code.
191       _overwrite: Internal option, shouldn't be used by consuming code.
192     """
193     self._inited = None
194     self._overwrite = bool(_overwrite)
195     if _is_root:
196       namespace = '.'
197       self._inited = True
198     else:
199       namespace = os.path.normpath(namespace)
200       if parent is None:
201         raise ValueError("Either _is_root must be set to True, or parent must "
202                          "be non null")
203       if namespace in ('.', ''):
204         raise ValueError("Invalid namespace %r was given" % (namespace,))
205
206     self.namespace = namespace
207     self.autoclean = autoclean
208     self.parent = parent
209
210     if not lazy_init:
211       self.Instantiate()
212
213   def _LimitName(self, name, for_path=False, multilevel=False):
214     """Translation function doing sanity checks on derivative namespaces
215
216     If you're extending this class, you should be using this for any namespace
217     operations that pass through a nested group.
218     """
219     # We use a fake pathway here, and this code must do so.  To calculate the
220     # real pathway requires knowing CGROUP_ROOT, which requires sudo
221     # potentially.  Since this code may be invoked just by loading the module,
222     # no execution/sudo should occur.  However, if for_path is set, we *do*
223     # require CGROUP_ROOT- which is fine, since we sort that on the way out.
224     fake_path = os.path.normpath(os.path.join('/fake-path', self.namespace))
225     path = os.path.normpath(os.path.join(fake_path, name))
226
227     # Ensure that the requested pathway isn't trying to sidestep what we
228     # expect, and in the process it does internal validation checks.
229     if not path.startswith(fake_path + '/'):
230       raise ValueError("Name %s tried descending through this namespace into"
231                        " another; this isn't allowed." % (name,))
232     elif path == self.namespace:
233       raise ValueError("Empty name %s" % (name,))
234     elif os.path.dirname(path) != fake_path and not multilevel:
235       raise ValueError("Name %s is multilevel, but disallowed." % (name,))
236
237     # Get the validated/normalized name.
238     name = path[len(fake_path):].strip('/')
239     if for_path:
240       return os.path.join(self.path, name)
241     return name
242
243   @property
244   def path(self):
245     return os.path.abspath(os.path.join(self.CGROUP_ROOT, self.namespace))
246
247   @property
248   def tasks(self):
249     s = set(x.strip() for x in self.GetValue('tasks', '').splitlines())
250     s.update(x.pid_owner for x in self.nested_groups)
251     s.discard(None)
252     return s
253
254   @property
255   def all_tasks(self):
256     s = self.tasks
257     for group in self.all_nested_groups:
258       s.update(group.tasks)
259     return s
260
261   @property
262   def nested_groups(self):
263     targets = []
264     path = self.path
265     try:
266       targets = [x for x in os.listdir(path)
267                  if os.path.isdir(os.path.join(path, x))]
268     except EnvironmentError as e:
269       if e.errno != errno.ENOENT:
270         raise
271
272     targets = [self.AddGroup(x, lazy_init=True, _overwrite=False)
273                for x in targets]
274
275     # Suppress initialization checks- if it exists on disk, we know it
276     # is already initialized.
277     for x in targets:
278       x._inited = True
279     return targets
280
281   @property
282   def all_nested_groups(self):
283     # Do a depth first traversal.
284     def walk(groups):
285       for group in groups:
286         for subgroup in walk(group.nested_groups):
287           yield subgroup
288         yield group
289     return list(walk(self.nested_groups))
290
291   @property
292   @cros_build_lib.MemoizedSingleCall
293   def pid_owner(self):
294     # Ensure it's in cros namespace- if it is outside of the cros namespace,
295     # we shouldn't make assumptions about the naming convention used.
296     if not self.GroupIsAParent(_cros_node):
297       return None
298     # See documentation at the top of the file for the naming scheme.
299     # It's basically "%(program_name)s:%(owning_pid)i" if the group
300     # is nested.
301     return os.path.basename(self.namespace).rsplit(':', 1)[-1]
302
303   def GroupIsAParent(self, node):
304     """Is the given node a parent of us?"""
305     parent_path = node.path + '/'
306     return self.path.startswith(parent_path)
307
308   def GetValue(self, key, default=None):
309     """Query a cgroup configuration key from disk.
310
311     If the file doesn't exist, return the given default.
312     """
313     try:
314       return osutils.ReadFile(os.path.join(self.path, key))
315     except EnvironmentError as e:
316       if e.errno != errno.ENOENT:
317         raise
318       return default
319
320   def _AddSingleGroup(self, name, **kwargs):
321     """Method for creating a node nested within this one.
322
323     Derivative classes should override this method rather than AddGroup;
324     see __init__ for the supported keywords.
325     """
326     return self.__class__(os.path.join(self.namespace, name), **kwargs)
327
328   def AddGroup(self, name, **kwargs):
329     """Add and return a cgroup nested in this one.
330
331     See __init__ for the supported keywords.  If this isn't a direct child
332     (for example this instance is cbuildbot, and the name is 1823/x), it'll
333     create the intermediate groups as lazy_init=True, setting autoclean to
334     via the logic described for autoclean_parents below.
335
336     Args:
337       name: Name of group to add.
338       autoclean_parents: Optional keyword argument; if unspecified, it takes
339         the value of autoclean (or True if autoclean isn't specified).  This
340         controls whether any intermediate nodes that must be created for
341         multilevel groups are autocleaned.
342     """
343     name = self._LimitName(name, multilevel=True)
344
345     autoclean = kwargs.pop('autoclean', True)
346     autoclean_parents = kwargs.pop('autoclean_parents', autoclean)
347     chunks = name.split('/', 1)
348     node = self
349     # pylint: disable=W0212
350     for chunk in chunks[:-1]:
351       node = node._AddSingleGroup(chunk, parent=node,
352                                   autoclean=autoclean_parents, **kwargs)
353     return node._AddSingleGroup(chunks[-1], parent=node,
354                                 autoclean=autoclean, **kwargs)
355
356   @cros_build_lib.MemoizedSingleCall
357   def Instantiate(self):
358     """Ensure this group exists on disk in the cgroup hierarchy"""
359
360     if self.namespace == '.':
361       # If it's the root of the hierarchy, leave it alone.
362       return True
363
364     if self.parent is not None:
365       self.parent.Instantiate()
366     osutils.SafeMakedirs(self.path, sudo=True)
367
368     force_inheritance = True
369     if self.parent.GetValue('cgroup.clone_children', '').strip() == '1':
370       force_inheritance = False
371
372     if force_inheritance:
373       if self._SUPPORTS_AUTOINHERIT:
374         # If the cgroup version supports it, flip the auto-inheritance setting
375         # on so that cgroups nested here don't have to manually transfer
376         # settings
377         self._SudoSet('cgroup.clone_children', '1')
378
379       try:
380         # TODO(ferringb): sort out an appropriate filter/list for using:
381         # for name in os.listdir(parent):
382         # rather than just transfering these two values.
383         for name in ('cpuset.cpus', 'cpuset.mems'):
384           if not self._overwrite:
385             # Top level nodes like cros/cbuildbot we don't want to overwrite-
386             # users/system may've leveled configuration.  If it's empty,
387             # overwrite it in those cases.
388             val = self.GetValue(name, '').strip()
389             if val:
390               continue
391           self._SudoSet(name, self.parent.GetValue(name, ''))
392       except (EnvironmentError, cros_build_lib.RunCommandError):
393         # Do not leave half created cgroups hanging around-
394         # it makes compatibility a pain since we have to rewrite
395         # the cgroup each time.  If instantiation fails, we know
396         # the group is screwed up, or the instantiaton code is-
397         # either way, no reason to leave it alive.
398         self.RemoveThisGroup()
399         raise
400
401     return True
402
403   # Since some of this code needs to check/reset this function to be ran,
404   # we use a more developer friendly variable name.
405   Instantiate._cache_key = '_inited'
406
407   def _SudoSet(self, key, value):
408     """Set a cgroup file in this namespace to a specific value"""
409     name = self._LimitName(key, True)
410     try:
411       return sudo.SetFileContents(name, value, cwd=os.path.dirname(name))
412     except cros_build_lib.RunCommandError as e:
413       if e.exception is not None:
414         # Command failed before the exec itself; convert ENOENT
415         # appropriately.
416         exc = e.exception
417         if isinstance(exc, EnvironmentError) and exc.errno == errno.ENOENT:
418           raise _GroupWasRemoved(self.namespace, e)
419       raise
420
421   def RemoveThisGroup(self, strict=False):
422     """Remove this specific cgroup
423
424     If strict is True, then we must be removed.
425     """
426     if self._RemoveGroupOnDisk(self.path, strict=strict):
427       self._inited = None
428       return True
429     return False
430
431   def RemoveGroup(self, name, strict=False):
432     """Removes a nested cgroup of ours
433
434     Args:
435       name: the namespace to remove.
436       strict: if False, remove it if possible.  If True, its an error if it
437               cannot be removed.
438     """
439     return self._RemoveGroupOnDisk(self._LimitName(name, for_path=True),
440                                    strict)
441
442   @classmethod
443   def _RemoveGroupOnDisk(cls, path, strict, sudo_strict=True):
444     """Perform the actual group removal.
445
446     Args:
447       path: The cgroup's location on disk.
448       strict: Boolean; if true, then it's an error if the group can't be
449         removed.  This can occur if there are still processes in it, or in
450         a nested group.
451       sudo_strict: See SudoRunCommand's strict option.
452     """
453     # Depth first recursively remove our children cgroups, then ourselves.
454     # Allow this to fail since currently it's possible for the cleanup code
455     # to not fully kill the hierarchy.  Note that we must do just rmdirs,
456     # rm -rf cannot be used- it tries to remove files which are unlinkable
457     # in cgroup (only namespaces can be removed via rmdir).
458     # See Documentation/cgroups/ for further details.
459     path = os.path.normpath(path) + '/'
460     # Do a sanity check to ensure that we're not touching anything we
461     # shouldn't.
462     if not path.startswith(cls.CGROUP_ROOT):
463       raise RuntimeError("cgroups.py: Was asked to wipe path %s, refusing. "
464                          "strict was %r, sudo_strict was %r"
465                          % (path, strict, sudo_strict))
466
467     result = cros_build_lib.SudoRunCommand(
468         ['find', path, '-depth', '-type', 'd', '-exec', 'rmdir', '{}', '+'],
469         redirect_stderr=True, error_code_ok=not strict,
470         print_cmd=False, strict=sudo_strict)
471     if result.returncode == 0:
472       return True
473     elif not os.path.isdir(path):
474       # We were invoked against a nonexistant path.
475       return True
476     return False
477
478   def TransferCurrentProcess(self, threads=True):
479     """Move the current process into this cgroup.
480
481     If threads is True, we move our threads into the group in addition.
482     Note this must be called in a threadsafe manner; it primarily exists
483     as a helpful default since python stdlib generates some background
484     threads (even when the code is operated synchronously).  While we
485     try to handle that scenario, it's implicitly racy since python
486     gives no clean/sane way to control/stop thread creation; thus it's
487     on the invokers head to ensure no new threads are being generated
488     while this is ran.
489     """
490     if not threads:
491       return self.TransferPid(os.getpid())
492
493     seen = set()
494     while True:
495       force_run = False
496       threads = set(self._GetCurrentProcessThreads())
497       for tid in threads:
498         # Track any failures; a failure means the thread died under
499         # feet, implying we shouldn't trust the current state.
500         force_run |= not self.TransferPid(tid, True)
501       if not force_run and threads == seen:
502         # We got two runs of this code seeing the same threads; assume
503         # we got them all since the first run moved those threads into
504         # our cgroup, and the second didn't see any new threads.  While
505         # there may have been new threads between run1/run2, we do run2
506         # purely to snag threads we missed in run1; anything split by
507         # a thread from run1 would auto inherit our cgroup.
508         return
509       seen = threads
510
511   def _GetCurrentProcessThreads(self):
512     """Lookup the given tasks (pids fundamentally) for our process."""
513     # Note that while we could try doing tricks like threading.enumerate,
514     # that's not guranteed to pick up background c/ffi threads; generally
515     # that's ultra rare, but the potential exists thus we ask the kernel
516     # instead.  What sucks however is that python releases the GIL; thus
517     # consuming code has to know of this, and protect against it.
518     return map(int, os.listdir('/proc/self/task'))
519
520   @EnsureInitialized
521   def TransferPid(self, pid, allow_missing=False):
522     """Assigns a given process to this cgroup."""
523     # Assign this root process to the new cgroup.
524     try:
525       self._SudoSet('tasks', '%d' % int(pid))
526       return True
527     except cros_build_lib.RunCommandError:
528       if not allow_missing:
529         raise
530       return False
531
532   # TODO(ferringb): convert to snakeoil.weakref.WeakRefFinalizer
533   def __del__(self):
534     if self.autoclean and self._inited and self.CGROUP_ROOT:
535       # Suppress any sudo_strict behaviour, since we may be invoked
536       # during interpreter shutdown.
537       self._RemoveGroupOnDisk(self.path, False, sudo_strict=False)
538
539   def TemporarilySwitchToNewGroup(self, namespace, **kwargs):
540     """Context manager to create a new cgroup & temporarily switch into it."""
541     node = self.AddGroup(namespace, **kwargs)
542     return self.TemporarilySwitchToGroup(node)
543
544   @contextlib.contextmanager
545   def TemporarilySwitchToGroup(self, group):
546     """Temporarily move this process into the given group, moving back after.
547
548     Used in a context manager fashion (aka, the with statement).
549     """
550     group.TransferCurrentProcess()
551     try:
552       yield
553     finally:
554       self.TransferCurrentProcess()
555
556   def KillProcesses(self, poll_interval=0.05, remove=False, sigterm_timeout=10):
557     """Kill all processes in this namespace."""
558
559     my_pids = set(map(str, self._GetCurrentProcessThreads()))
560
561     def _SignalPids(pids, signum):
562       cros_build_lib.SudoRunCommand(
563           ['kill', '-%i' % signum] + sorted(pids),
564           print_cmd=False, error_code_ok=True, redirect_stdout=True,
565           combine_stdout_stderr=True)
566
567     # First sigterm what we can, exiting after 2 runs w/out seeing pids.
568     # Let this phase run for a max of 10 seconds; afterwards, switch to
569     # sigkilling.
570     time_end = time.time() + sigterm_timeout
571     saw_pids, pids = True, set()
572     while time.time() < time_end:
573       previous_pids = pids
574       pids = self.tasks
575
576       self_kill = my_pids.intersection(pids)
577       if self_kill:
578         raise Exception("Bad API usage: asked to kill cgroup %s, but "
579                         "current pid %s is in that group.  Effectively "
580                         "asked to kill ourselves."
581                         % (self.namespace, self_kill))
582
583       if not pids:
584         if not saw_pids:
585           break
586         saw_pids = False
587       else:
588         saw_pids = True
589         new_pids = pids.difference(previous_pids)
590         if new_pids:
591           _SignalPids(new_pids, signal.SIGTERM)
592           # As long as new pids keep popping up, skip sleeping and just keep
593           # stomping them as quickly as possible (whack-a-mole is a good visual
594           # analogy of this).  We do this to ensure that fast moving spawns
595           # are dealt with as quickly as possible.  When considering this code,
596           # it's best to think about forkbomb scenarios- shouldn't occur, but
597           # synthetic fork-bombs can occur, thus this code being aggressive.
598           continue
599
600       time.sleep(poll_interval)
601
602     # Next do a sigkill scan.  Again, exit only after no pids have been seen
603     # for two scans, and all groups are removed.
604     groups_existed = True
605     while True:
606       pids = self.all_tasks
607
608       if pids:
609         self_kill = my_pids.intersection(pids)
610         if self_kill:
611           raise Exception("Bad API usage: asked to kill cgroup %s, but "
612                           "current pid %i is in that group.  Effectively "
613                           "asked to kill ourselves."
614                           % (self.namespace, self_kill))
615
616         _SignalPids(pids, signal.SIGKILL)
617         saw_pids = True
618       elif not (saw_pids or groups_existed):
619         break
620       else:
621         saw_pids = False
622
623       time.sleep(poll_interval)
624
625       # Note this is done after the sleep; try to give the kernel time to
626       # shutdown the processes.  They may still be transitioning to defunct
627       # kernel side by when we hit this scan, but that's fine- the next will
628       # get it.
629       # This needs to be nonstrict; it's possible the kernel is currently
630       # killing the pids we've just sigkill'd, thus the group isn't removable
631       # yet.  Additionally, it's possible a child got forked we didn't see.
632       # Ultimately via our killing/removal attempts, it will be removed,
633       # just not necessarily on the first run.
634       if remove:
635         if self.RemoveThisGroup(strict=False):
636           # If we successfully removed this group, then there can be no pids,
637           # sub groups, etc, within it.  No need to scan further.
638           return True
639         groups_existed = True
640       else:
641         groups_existed = [group.RemoveThisGroup(strict=False)
642                           for group in self.nested_groups]
643         groups_existed = not all(groups_existed)
644
645
646
647   @classmethod
648   def _FindCurrentCrosGroup(cls, pid=None):
649     """Find and return the cros namespace a pid is currently in.
650
651     If no pid is given, os.getpid() is substituted.
652     """
653     if pid is None:
654       pid = 'self'
655     elif not isinstance(pid, (long, int)):
656       raise ValueError("pid must be None, or an integer/long.  Got %r" % (pid,))
657
658     cpuset = None
659     try:
660       # See the kernels Documentation/filesystems/proc.txt if you're unfamiliar
661       # w/ procfs, and keep in mind that we have to work across multiple kernel
662       # versions.
663       cpuset = osutils.ReadFile('/proc/%s/cpuset' % (pid,)).rstrip('\n')
664     except EnvironmentError as e:
665       if e.errno != errno.ENOENT:
666         raise
667       with open('/proc/%s/cgroup' % pid) as f:
668         for line in f:
669           # First digit is the hierachy index, 2nd is subsytem, 3rd is space.
670           # 2:cpuset:/
671           # 2:cpuset:/cros/cbuildbot/1234
672
673           line = line.rstrip('\n')
674           if not line:
675             continue
676           line = line.split(':', 2)
677           if line[1] == 'cpuset':
678             cpuset = line[2]
679             break
680
681     if not cpuset or not cpuset.startswith("/cros/"):
682       return None
683     return cpuset[len("/cros/"):].strip("/")
684
685   @classmethod
686   def FindStartingGroup(cls, process_name, nesting=True):
687     """Create and return the starting cgroup for ourselves nesting if allowed.
688
689     Note that the node returned is either a generic process pool (e.g.
690     cros/cbuildbot), or the parent pool we're nested within; processes
691     generated in this group are the responsibility of this process to
692     deal with- nor should this process ever try triggering a kill w/in this
693     portion of the tree since they don't truly own it.
694
695     Args:
696       process_name: See the hierarchy comments at the start of this module.
697         This should basically be the process name- cros_sdk for example,
698         cbuildbot, etc.
699       nesting: If we're invoked by another cros cgroup aware process,
700         should we nest ourselves in their hierarchy?  Generally speaking,
701         client code should never have a reason to disable nesting.
702     """
703     if not cls.IsUsable():
704       return None
705
706     target = None
707     if nesting:
708       target = cls._FindCurrentCrosGroup()
709     if target is None:
710       target = process_name
711
712     return _cros_node.AddGroup(target, autoclean=False)
713
714
715 class ContainChildren(cros_build_lib.MasterPidContextManager):
716   """Context manager for containing children processes.
717
718   This manager creates a job pool derived from the specified Cgroup |node|
719   and transfers the current process into it upon __enter__.
720
721   Any children processes created at that point will inherit our cgroup;
722   they can only escape the group if they're running as root and move
723   themselves out of this hierarchy.
724
725   Upon __exit__, transfer the current process back to this group, then
726   SIGTERM (progressing to SIGKILL) any immediate children in the pool,
727   finally removing the pool if possible. After sending SIGTERM, we wait
728   |sigterm_timeout| seconds before sending SIGKILL.
729
730   If |pool_name| is given, that name is used rather than os.getpid() for
731   the job pool created.
732
733   Finally, note that during cleanup this will suppress all signals
734   to ensure that it cleanses any children before returning.
735   """
736
737   def __init__(self, node, pool_name=None, sigterm_timeout=10):
738     super(ContainChildren, self).__init__()
739     self.node = node
740     self.child = None
741     self.pid = None
742     self.pool_name = pool_name
743     self.sigterm_timeout = sigterm_timeout
744     self.run_kill = False
745
746   def _enter(self):
747     self.pid = os.getpid()
748
749     # Note: We use lazy init here so that we cannot trigger a
750     # _GroupWasRemoved -- we want that to be contained.
751     pool_name = str(self.pid) if self.pool_name is None else self.pool_name
752     self.child = self.node.AddGroup(pool_name, autoclean=True, lazy_init=True)
753     try:
754       self.child.TransferCurrentProcess()
755     except _GroupWasRemoved:
756       raise SystemExit(
757           "Group %s was removed under our feet; pool shutdown is underway"
758           % self.child.namespace)
759     self.run_kill = True
760
761   def _exit(self, *_args, **_kwargs):
762     with signals.DeferSignals():
763       self.node.TransferCurrentProcess()
764       if self.run_kill:
765         self.child.KillProcesses(remove=True,
766                                  sigterm_timeout=self.sigterm_timeout)
767       else:
768         # Non-strict since the group may have failed to be created.
769         self.child.RemoveThisGroup(strict=False)
770
771
772 def SimpleContainChildren(process_name, nesting=True, pid=None, **kwargs):
773   """Convenience context manager to create a cgroup for children containment
774
775   See Cgroup.FindStartingGroup and Cgroup.ContainChildren for specifics.
776   If Cgroups aren't supported on this system, this is a noop context manager.
777   """
778   node = Cgroup.FindStartingGroup(process_name, nesting=nesting)
779   if node is None:
780     return cros_build_lib.NoOpContextManager()
781   if pid is None:
782     pid = os.getpid()
783   name = '%s:%i' % (process_name, pid)
784   return ContainChildren(node, name, **kwargs)
785
786 # This is a generic group, not associated with any specific process id, so
787 # we shouldn't autoclean it on exit; doing so would delete the group from
788 # under the feet of any other processes interested in using the group.
789 _root_node = Cgroup(None, _is_root=True, autoclean=False, lazy_init=True)
790 _cros_node = _root_node.AddGroup('cros', autoclean=False, lazy_init=True,
791                                  _overwrite=False)