#!/usr/bin/python3
+###############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2021 Pakfire development team #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 3 of the License, or #
+# (at your option) any later version. #
+# #
+# This program is distributed in the hope that it will be useful, #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
+# GNU General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###############################################################################
+import logging
import os
-import shutil
import signal
import time
-import logging
log = logging.getLogger("pakfire.cgroups")
-CGROUP_MOUNTPOINT = "/sys/fs/cgroup/systemd"
-
-class CGroup(object):
- def __init__(self, name):
- assert supported(), "cgroups are not supported by this kernel"
+def find_group_by_pid(pid):
+ """
+ Returns the cgroup of the process currently running with pid
+ """
+ with open("/proc/%s/cgroup" % pid) as f:
+ for line in f:
+ if not line.startswith("0::"):
+ continue
- self.name = name
- self.path = os.path.join(CGROUP_MOUNTPOINT, name)
- self.path = os.path.abspath(self.path)
+ # Clean up path
+ path = line[3:].rstrip()
- # The parent cgroup.
- self._parent = None
+ return CGroup(path)
- # Initialize the cgroup.
- self.create()
+def get_own_group():
+ """
+ Returns the cgroup of the process we are currently in
+ """
+ pid = os.getpid()
- log.debug("cgroup '%s' has been successfully initialized." % self.name)
+ return find_group_by_pid(pid)
- def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, self.name)
+class CGroup(object):
+ """
+ cgroup controller
+ """
+ root = "/sys/fs/cgroup/unified"
- def __cmp__(self, other):
- return cmp(self.path, other.path)
+ def __init__(self, path):
+ if not path.startswith("/"):
+ raise ValueError("Invalid cgroup path")
- @classmethod
- def find_by_pid(cls, pid):
- """
- Returns the cgroup of the process with the given PID.
+ # Store path
+ self.path = path
- If no cgroup can be found, None is returned.
- """
- if not cls.supported:
- return
+ # Make absolute path
+ self.abspath = "%s%s" % (self.root, self.path)
- for d, subdirs, files in os.walk(CGROUP_MOUNTPOINT):
- if not "tasks" in files:
- continue
+ if not os.path.isdir(self.abspath):
+ raise ValueError("Non-existant cgroup")
- cgroup = cls(d)
- if pid in cgroup.tasks:
- return cgroup
+ def __repr__(self):
+ return "<%s path=%s>" % (self.__class__.__name__, self.path)
- @staticmethod
- def supported():
+ def _open(self, path, mode="r"):
"""
- Returns true, if this hosts supports cgroups.
+ Opens a file in this cgroup for reading of writing
"""
- return os.path.ismount(CGROUP_MOUNTPOINT)
+ # Make full path
+ path = os.path.join(self.abspath, path)
- def create(self):
+ return open(path, mode)
+
+ @property
+ def parent(self):
"""
- Creates the filesystem structure for
- the cgroup.
+ Returns the parent group
"""
- if os.path.exists(self.path):
- return
+ return self.__class__(os.path.dirname(self.path))
- log.debug("cgroup '%s' has been created." % self.name)
- os.makedirs(self.path)
+ def create_subgroup(self, name):
+ path = os.path.join(self.path, name)
- def create_child_cgroup(self, name):
- """
- Create a child cgroup with name relative to the
- parent cgroup.
- """
- return self.__class__(os.path.join(self.name, name))
+ # Create directory
+ try:
+ os.mkdir("%s%s" % (self.root, path))
- def attach(self):
- """
- Attaches this task to the cgroup.
- """
- pid = os.getpid()
- self.attach_task(pid)
+ log.debug("New cgroup '%s' created" % path)
+
+ # Silently continue if groups already exists
+ except FileExistsError:
+ pass
+
+ # Return new instance
+ return self.__class__(path)
def destroy(self):
"""
- Deletes the cgroup.
-
- All running tasks will be migrated to the parent cgroup.
+ Destroys this cgroup
"""
- # Don't delete the root cgroup.
- if self == self.root:
- return
+ log.debug("Destroying cgroup %s" % self.path)
- # Move all tasks to the parent.
+ # Move whatever is left to the parent group
self.migrate(self.parent)
- # Just make sure the statement above worked.
- assert self.is_empty(recursive=True), "cgroup must be empty to be destroyed"
- assert not self.processes
-
- # Remove the file tree.
+ # Remove the file tree
try:
- os.rmdir(self.path)
+ os.rmdir(self.abspath)
except OSError as e:
# Ignore "Device or resource busy".
if e.errno == 16:
raise
- def _read(self, file):
- """
- Reads the contect of file in the cgroup directory.
- """
- file = os.path.join(self.path, file)
-
- with open(file) as f:
- return f.read()
-
- def _read_pids(self, file):
+ @property
+ def pids(self):
"""
- Reads file and interprets the lines as a sorted list.
+ Returns the PIDs of all currently in this group running processes
"""
- _pids = self._read(file)
-
pids = []
- for pid in _pids.splitlines():
- try:
- pid = int(pid)
- except ValueError:
- continue
-
- if pid in pids:
- continue
+ with self._open("cgroup.procs") as f:
+ for line in f:
+ try:
+ pid = int(line)
+ except (TypeError, ValueError):
+ pass
- pids.append(pid)
+ pids.append(pid)
- return sorted(pids)
+ return pids
- def _write(self, file, what):
+ def attach_process(self, pid):
"""
- Writes what to file in the cgroup directory.
+ Attaches the process PID to this group
"""
- file = os.path.join(self.path, file)
-
- f = open(file, "w")
- f.write("%s" % what)
- f.close()
+ log.debug("Attaching process %s to group %s" % (pid, self.path))
- @property
- def root(self):
- if self.parent:
- return self.parent.root
-
- return self
-
- @property
- def parent(self):
- # Cannot go above CGROUP_MOUNTPOINT.
- if self.path == CGROUP_MOUNTPOINT:
- return
+ with self._open("cgroup.procs", "w") as f:
+ f.write("%s\n" % pid)
- if self._parent is None:
- parent_name = os.path.dirname(self.name)
- self._parent = CGroup(parent_name)
-
- return self._parent
-
- @property
- def subgroups(self):
- subgroups = []
-
- for name in os.listdir(self.path):
- path = os.path.join(self.path, name)
- if not os.path.isdir(path):
- continue
-
- name = os.path.join(self.name, name)
- group = CGroup(name)
-
- subgroups.append(group)
-
- return subgroups
-
- def is_empty(self, recursive=False):
+ def attach_self(self):
"""
- Returns True if the cgroup is empty.
-
- Otherwise returns False.
+ Attaches this process to the group
"""
- if self.tasks:
- return False
+ return self.attach_process(os.getpid())
- if recursive:
- for subgroup in self.subgroups:
- if subgroup.is_empty(recursive=recursive):
- continue
-
- return False
+ def detach_self(self):
+ pid = os.getpid()
- return True
+ # Move process to parent
+ if pid in self.pids:
+ self.parent.attach_process(pid)
- @property
- def tasks(self):
+ def migrate(self, group):
"""
- Returns a list of pids of all tasks
- in this process group.
+ Migrates all processes to the given group
"""
- return self._read_pids("tasks")
+ for pid in self.pids:
+ group.attach_process(pid)
- @property
- def processes(self):
+ def _kill(self, signal=signal.SIGTERM):
"""
- Returns a list of pids of all processes
- that are currently running within the cgroup.
+ Sends signal to all processes in this cgroup
"""
- return self._read_pids("cgroup.procs")
+ for pid in self.pids:
+ log.debug("Sending signal %s to process %s" % (signal, pid))
- def attach_task(self, pid):
- """
- Attaches the task with the given PID to
- the cgroup.
- """
- self._write("tasks", pid)
+ try:
+ os.kill(pid, signal)
+ except OSError as e:
+ # Skip "No such process" error
+ if e.errno == 3:
+ pass
+ else:
+ raise
+
+ # Return True if there are any processes left
+ return not self.pids
- def migrate_task(self, other, pid):
+ def killall(self, timeout=10):
"""
- Migrates a single task to another cgroup.
+ Kills all processes
"""
- other.attach_task(pid)
-
- def migrate(self, other):
- if self.is_empty(recursive=True):
- return
-
- log.info("Migrating all tasks from '%s' to '%s'." \
- % (self.name, other.name))
+ self.detach_self()
- while True:
- # Migrate all tasks to the new cgroup.
- for task in self.tasks:
- self.migrate_task(other, task)
-
- # Also do that for all subgroups.
- for subgroup in self.subgroups:
- subgroup.migrate(other)
-
- if self.is_empty():
- break
-
- def kill(self, sig=signal.SIGTERM, recursive=True):
- killed_processes = []
-
- mypid = os.getpid()
-
- while True:
- for proc in self.processes:
- # Don't kill myself.
- if proc == mypid:
- continue
-
- # Skip all processes that have already been killed.
- if proc in killed_processes:
- continue
-
- # If we haven't killed the process yet, we kill it.
- log.debug("Sending signal %s to process %s..." % (sig, proc))
-
- try:
- os.kill(proc, sig)
- except OSError as e:
- # Skip "No such process" error
- if e.errno == 3:
- pass
- else:
- raise
-
- # Save all killed processes to a list.
- killed_processes.append(proc)
-
- else:
- # If no processes are left to be killed, we end the loop.
- break
-
- # Nothing more to do if not in recursive mode.
- if not recursive:
- return
-
- # Kill all processes in subgroups as well.
- for subgroup in self.subgroups:
- subgroup.kill(sig=sig, recursive=recursive)
-
- def kill_and_wait(self):
- # Safely kill all processes in the cgroup.
- # This first sends SIGTERM and then checks 8 times
- # after 200ms whether the group is empty. If not,
- # everything what's still in there gets SIGKILL
- # and it is five more times checked if everything
- # went away.
-
- sig = None
- for i in range(15):
- if i == 0:
- sig = signal.SIGTERM
- elif i == 9:
- sig = signal.SIGKILL
+ for i in range(timeout * 10):
+ if i >= 10:
+ s = signal.SIGKILL
else:
- sig = None
+ s = signal.SIGTERM
- # If no signal is given and there are no processes
- # left, our job is done and we can exit.
- if not self.processes:
+ # Send signal and end loop when no processes are left
+ if self._kill(signal=s):
break
- if sig:
- # Send sig to all processes in the cgroup.
- log.debug("Sending signal %s to all processes in '%s'." % (sig, self.name))
- self.kill(sig=sig, recursive=True)
-
- # Sleep for 200ms.
- time.sleep(0.2)
-
- return self.is_empty()
-
-
-# Alias for simple access to check if this host supports cgroups.
-supported = CGroup.supported
-
-# Alias for simple access to find the cgroup of a certain process.
-find_by_pid = CGroup.find_by_pid
+ # Sleep for 100ms
+ time.sleep(0.1)