]>
Commit | Line | Data |
---|---|---|
2b6cc06d MT |
1 | #!/usr/bin/python |
2 | ||
3 | import os | |
4 | import shutil | |
5 | import signal | |
6 | import time | |
7 | ||
8 | import logging | |
9 | log = logging.getLogger("pakfire.cgroups") | |
10 | ||
904c8f11 | 11 | CGROUP_MOUNTPOINT = "/sys/fs/cgroup/systemd" |
2b6cc06d MT |
12 | |
13 | class 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. | |
329 | supported = CGroup.supported | |
330 | ||
331 | # Alias for simple access to find the cgroup of a certain process. | |
332 | find_by_pid = CGroup.find_by_pid |