]> git.ipfire.org Git - pakfire.git/blame - python/pakfire/cgroup.py
builder: Use cgroups if the system supports it.
[pakfire.git] / python / pakfire / cgroup.py
CommitLineData
2b6cc06d
MT
1#!/usr/bin/python
2
3import os
4import shutil
5import signal
6import time
7
8import logging
9log = logging.getLogger("pakfire.cgroups")
10
904c8f11 11CGROUP_MOUNTPOINT = "/sys/fs/cgroup/systemd"
2b6cc06d
MT
12
13class CGroup(object):
14 def __init__(self, name):
15 assert supported(), "cgroups are not supported by this kernel"
16
17 self.name = name
904c8f11 18 self.path = os.path.join(CGROUP_MOUNTPOINT, name)
2b6cc06d
MT
19 self.path = os.path.abspath(self.path)
20
21 # The parent cgroup.
22 self._parent = None
23
24 # Initialize the cgroup.
25 self.create()
26
27 log.debug("cgroup '%s' has been successfully initialized." % self.name)
28
29 def __repr__(self):
30 return "<%s %s>" % (self.__class__.__name__, self.name)
31
32 def __cmp__(self, other):
33 return cmp(self.path, other.path)
34
904c8f11
MT
35 @classmethod
36 def find_by_pid(cls, pid):
37 """
38 Returns the cgroup of the process with the given PID.
39
40 If no cgroup can be found, None is returned.
41 """
42 if not cls.supported:
43 return
44
45 for d, subdirs, files in os.walk(CGROUP_MOUNTPOINT):
46 if not "tasks" in files:
47 continue
48
49 cgroup = cls(d)
50 if pid in cgroup.tasks:
51 return cgroup
52
53 @staticmethod
54 def supported():
55 """
56 Returns true, if this hosts supports cgroups.
57 """
58 return os.path.ismount(CGROUP_MOUNTPOINT)
59
2b6cc06d
MT
60 def create(self):
61 """
62 Creates the filesystem structure for
63 the cgroup.
64 """
65 if os.path.exists(self.path):
66 return
67
68 log.debug("cgroup '%s' has been created." % self.name)
69 os.makedirs(self.path)
70
904c8f11
MT
71 def create_child_cgroup(self, name):
72 """
73 Create a child cgroup with name relative to the
74 parent cgroup.
75 """
76 return self.__class__(os.path.join(self.name, name))
77
2b6cc06d
MT
78 def attach(self):
79 """
80 Attaches this task to the cgroup.
81 """
82 pid = os.getpid()
83 self.attach_task(pid)
84
85 def destroy(self):
86 """
87 Deletes the cgroup.
88
89 All running tasks will be migrated to the parent cgroup.
90 """
91 # Don't delete the root cgroup.
92 if self == self.root:
93 return
94
95 # Move all tasks to the parent.
96 self.migrate(self.parent)
97
98 # Just make sure the statement above worked.
99 assert self.is_empty(recursive=True), "cgroup must be empty to be destroyed"
100 assert not self.processes
101
102 # Remove the file tree.
103 try:
104 os.rmdir(self.path)
105 except OSError, e:
106 # Ignore "Device or resource busy".
107 if e.errno == 16:
108 return
109
110 raise
111
112 def _read(self, file):
113 """
114 Reads the contect of file in the cgroup directory.
115 """
116 file = os.path.join(self.path, file)
117
118 with open(file) as f:
119 return f.read()
120
121 def _read_pids(self, file):
122 """
123 Reads file and interprets the lines as a sorted list.
124 """
125 _pids = self._read(file)
126
127 pids = []
128
129 for pid in _pids.splitlines():
130 try:
131 pid = int(pid)
132 except ValueError:
133 continue
134
135 if pid in pids:
136 continue
137
138 pids.append(pid)
139
140 return sorted(pids)
141
142 def _write(self, file, what):
143 """
144 Writes what to file in the cgroup directory.
145 """
146 file = os.path.join(self.path, file)
147
148 f = open(file, "w")
149 f.write("%s" % what)
150 f.close()
151
152 @property
153 def root(self):
154 if self.parent:
155 return self.parent.root
156
157 return self
158
159 @property
160 def parent(self):
904c8f11
MT
161 # Cannot go above CGROUP_MOUNTPOINT.
162 if self.path == CGROUP_MOUNTPOINT:
2b6cc06d
MT
163 return
164
165 if self._parent is None:
166 parent_name = os.path.dirname(self.name)
167 self._parent = CGroup(parent_name)
168
169 return self._parent
170
171 @property
172 def subgroups(self):
173 subgroups = []
174
175 for name in os.listdir(self.path):
176 path = os.path.join(self.path, name)
177 if not os.path.isdir(path):
178 continue
179
180 name = os.path.join(self.name, name)
181 group = CGroup(name)
182
183 subgroups.append(group)
184
185 return subgroups
186
187 def is_empty(self, recursive=False):
188 """
189 Returns True if the cgroup is empty.
190
191 Otherwise returns False.
192 """
193 if self.tasks:
194 return False
195
196 if recursive:
197 for subgroup in self.subgroups:
198 if subgroup.is_empty(recursive=recursive):
199 continue
200
201 return False
202
203 return True
204
205 @property
206 def tasks(self):
207 """
208 Returns a list of pids of all tasks
209 in this process group.
210 """
211 return self._read_pids("tasks")
212
213 @property
214 def processes(self):
215 """
216 Returns a list of pids of all processes
217 that are currently running within the cgroup.
218 """
219 return self._read_pids("cgroup.procs")
220
221 def attach_task(self, pid):
222 """
223 Attaches the task with the given PID to
224 the cgroup.
225 """
226 self._write("tasks", pid)
227
228 def migrate_task(self, other, pid):
229 """
230 Migrates a single task to another cgroup.
231 """
232 other.attach_task(pid)
233
234 def migrate(self, other):
235 if self.is_empty(recursive=True):
236 return
237
238 log.info("Migrating all tasks from '%s' to '%s'." \
239 % (self.name, other.name))
240
241 while True:
242 # Migrate all tasks to the new cgroup.
243 for task in self.tasks:
244 self.migrate_task(other, task)
245
246 # Also do that for all subgroups.
247 for subgroup in self.subgroups:
248 subgroup.migrate(other)
249
250 if self.is_empty():
251 break
252
253 def kill(self, sig=signal.SIGTERM, recursive=True):
254 killed_processes = []
255
256 mypid = os.getpid()
257
258 while True:
259 for proc in self.processes:
260 # Don't kill myself.
261 if proc == mypid:
262 continue
263
264 # Skip all processes that have already been killed.
265 if proc in killed_processes:
266 continue
267
268 # If we haven't killed the process yet, we kill it.
269 log.debug("Sending signal %s to process %s..." % (sig, proc))
270
271 try:
272 os.kill(proc, sig)
273 except OSError, e:
be230c03
MT
274 # Skip "No such process" error
275 if e.errno == 3:
276 pass
277 else:
278 raise
2b6cc06d
MT
279
280 # Save all killed processes to a list.
281 killed_processes.append(proc)
282
283 else:
284 # If no processes are left to be killed, we end the loop.
285 break
286
287 # Nothing more to do if not in recursive mode.
288 if not recursive:
289 return
290
291 # Kill all processes in subgroups as well.
292 for subgroup in self.subgroups:
293 subgroup.kill(sig=sig, recursive=recursive)
294
295 def kill_and_wait(self):
296 # Safely kill all processes in the cgroup.
297 # This first sends SIGTERM and then checks 8 times
298 # after 200ms whether the group is empty. If not,
299 # everything what's still in there gets SIGKILL
300 # and it is five more times checked if everything
301 # went away.
302
303 sig = None
304 for i in range(15):
305 if i == 0:
306 sig = signal.SIGTERM
307 elif i == 9:
308 sig = signal.SIGKILL
309 else:
310 sig = None
311
312 # If no signal is given and there are no processes
313 # left, our job is done and we can exit.
314 if not self.processes:
315 break
316
317 if sig:
318 # Send sig to all processes in the cgroup.
319 log.info("Sending signal %s to all processes in '%s'." % (sig, self.name))
320 self.kill(sig=sig, recursive=True)
321
322 # Sleep for 200ms.
323 time.sleep(0.2)
324
325 return self.is_empty()
904c8f11
MT
326
327
328# Alias for simple access to check if this host supports cgroups.
329supported = CGroup.supported
330
331# Alias for simple access to find the cgroup of a certain process.
332find_by_pid = CGroup.find_by_pid