1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module proc.pid;
7 
8 import core.time : Duration;
9 import logger = std.experimental.logger;
10 import std.algorithm : splitter, map, filter, joiner, sort;
11 import std.array : array, appender, empty;
12 import std.conv;
13 import std.exception : collectException, ifThrown;
14 import std.file;
15 import std.path;
16 import std.range : iota;
17 import std.stdio : File, writeln, writefln;
18 import std.typecons : Nullable, NullableRef, Tuple, tuple, Flag;
19 
20 import core.sys.posix.sys.types : uid_t;
21 
22 @safe:
23 
24 struct RawPid {
25     import core.sys.posix.unistd : pid_t;
26 
27     pid_t value;
28     alias value this;
29 }
30 
31 struct PidMap {
32     static struct Stat {
33         uid_t uid;
34     }
35 
36     static struct Pid {
37         RawPid self;
38         Stat stat;
39         RawPid[] children;
40         RawPid parent;
41         string proc;
42     }
43 
44     Stat[RawPid] stat;
45     /// The children a process has
46     RawPid[][RawPid] children;
47     /// the parent of a process
48     RawPid[RawPid] parent;
49     /// The executable of a pid
50     string[RawPid] proc;
51 
52     size_t length() nothrow {
53         return stat.length;
54     }
55 
56     auto pids() nothrow {
57         return stat.byKey.array;
58     }
59 
60     Pid get(RawPid p) nothrow {
61         typeof(return) rval;
62         rval.self = p;
63 
64         if (auto v = p in stat) {
65             rval.stat = *v;
66         }
67         if (auto v = p in children) {
68             rval.children = *v;
69         }
70         if (auto v = p in proc) {
71             rval.proc = *v;
72         }
73 
74         if (auto v = p in parent) {
75             rval.parent = *v;
76         } else {
77             rval.parent = p;
78         }
79 
80         return rval;
81     }
82 
83     void put(Pid p) nothrow {
84         stat[p.self] = p.stat;
85         this.parent[p.self] = p.parent;
86         if (p.parent !in stat) {
87             stat[p.parent] = Stat.init;
88             parent[p.parent] = p.parent;
89         }
90         if (!p.children.empty) {
91             this.children[p.self] = p.children;
92         }
93         if (!p.proc.empty) {
94             this.proc[p.self] = p.proc;
95         }
96     }
97 
98     void putChild(RawPid parent, RawPid child) {
99         if (auto v = parent in children) {
100             (*v) ~= child;
101         } else {
102             children[parent] = [child];
103         }
104     }
105 
106     bool empty() nothrow {
107         return stat.empty;
108     }
109 
110     /** Remove a pid from the map.
111      *
112      * An existing pid that have `p` as its parent will be rewritten such that
113      * it is it's own parent.
114      *
115      * The pid that had `p` as a child will be rewritten such that `p` is
116      * removed as a child.
117      */
118     ref PidMap remove(RawPid p) return nothrow {
119         stat.remove(p);
120         proc.remove(p);
121 
122         if (auto children_ = p in children) {
123             foreach (c; *children_) {
124                 parent[c] = c;
125             }
126         }
127         children.remove(p);
128 
129         if (auto children_ = parent[p] in children) {
130             (*children_) = (*children_).filter!(a => a != p).array;
131         }
132         parent.remove(p);
133 
134         return this;
135     }
136 
137     ref PidMap removeUser(uid_t uid) return nothrow {
138         auto removePids = appender!(RawPid[])();
139         foreach (a; stat.byKeyValue.filter!(a => a.value.uid == uid)) {
140             removePids.put(a.key);
141         }
142         foreach (k; removePids.data) {
143             this.remove(k);
144         }
145 
146         return this;
147     }
148 
149     RawPid[] getChildren(RawPid p) nothrow {
150         if (auto v = p in children) {
151             return *v;
152         }
153         return null;
154     }
155 
156     string getProc(RawPid p) nothrow {
157         if (auto v = p in proc) {
158             return *v;
159         }
160         return null;
161     }
162 
163     /// Returns: a `PidMap` that is a subtree with `p` as its root.
164     PidMap getSubMap(const RawPid p) nothrow {
165         PidMap rval;
166         RawPid[] s;
167         {
168             auto g = get(p);
169             g.parent = p;
170             rval.put(g);
171             s = g.children;
172         }
173         while (!s.empty) {
174             auto f = s[0];
175             s = s[1 .. $];
176 
177             auto g = get(f);
178             rval.put(g);
179             s ~= g.children;
180         }
181 
182         return rval;
183     }
184 
185     import std.range : isOutputRange;
186 
187     string toString() @safe {
188         import std.array : appender;
189 
190         auto buf = appender!string;
191         toString(buf);
192         return buf.data;
193     }
194 
195     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
196         import std.format : formattedWrite;
197         import std.range : put;
198 
199         formattedWrite(w, "PidMap(\n");
200         foreach (n; pids) {
201             formattedWrite(w, `Pid(%s, stat:%s, parent:%s, "%s", %s)`, n,
202                     stat[n], parent[n], getProc(n), getChildren(n));
203             put(w, "\n");
204         }
205         put(w, ")");
206     }
207 }
208 
209 /** Kill all pids in the map.
210  *
211  * Repeats until all pids are killed. It will continiue until all processes
212  * are killed by generating an updated `PidMap` and inspecting it to see that
213  * no new processes have been started.
214  *
215  * Returns: a pid list of the killed pids that may need to be called wait on.
216  *
217  * TODO: remove @trusted when upgrading the minimum compiler >2.091.0
218  */
219 RawPid[] kill(PidMap pmap, Flag!"onlyCurrentUser" user) @trusted nothrow {
220     static import core.sys.posix.signal;
221 
222     static void killMap(RawPid[] pids) @trusted nothrow {
223         foreach (const c; pids) {
224             core.sys.posix.signal.kill(c, core.sys.posix.signal.SIGKILL);
225         }
226     }
227 
228     auto rval = appender!(RawPid[])();
229     auto toKill = [pmap.filterByCurrentUser];
230     while (!toKill.empty) {
231         auto f = toKill[0];
232         toKill = toKill[1 .. $];
233 
234         auto pids = f.pids;
235         killMap(pids);
236         rval.put(pids);
237 
238         pmap = () {
239             if (user)
240                 return makePidMap.filterByCurrentUser;
241             return makePidMap;
242         }();
243 
244         foreach (s; pids.map!(a => tuple(a, pmap.getSubMap(a)))
245                 .map!(a => a[1].remove(a[0]))
246                 .filter!(a => !a.empty)) {
247             toKill ~= s;
248         }
249     }
250 
251     return rval.data;
252 }
253 
254 /// Reap all pids by calling wait on them.
255 void reap(RawPid[] pids) @trusted nothrow {
256     import core.sys.posix.sys.wait : waitpid, WNOHANG;
257 
258     foreach (c; pids) {
259         waitpid(c, null, WNOHANG);
260     }
261 }
262 
263 /// Split a `PidMap` so each map have one top pid as the `root`.
264 Tuple!(PidMap, "map", RawPid, "root")[] splitToSubMaps(PidMap pmap) {
265     import std.range : ElementType;
266 
267     RawPid[][RawPid] trees;
268     RawPid[RawPid] parent;
269 
270     void migrate(RawPid from, RawPid to) {
271         auto p = parent[to];
272         if (auto v = from in trees) {
273             trees[p] ~= *v;
274             trees.remove(from);
275         }
276 
277         foreach (k; parent.byKeyValue
278                 .filter!(a => a.value == from)
279                 .map!(a => a.key)
280                 .array) {
281             parent[k] = p;
282         }
283     }
284 
285     // populate, simplifies the migration if all nodes exists with an
286     // individual tree.
287     foreach (n; pmap.pids) {
288         parent[n] = n;
289         trees[n] = [n];
290     }
291 
292     foreach (n; pmap.pids) {
293         foreach (c; pmap.getChildren(n)) {
294             migrate(c, n);
295         }
296     }
297 
298     alias RT = ElementType!(typeof(return));
299     auto app = appender!(RT[])();
300 
301     foreach (tree; trees.byKeyValue) {
302         RT m;
303         m.root = tree.key;
304         foreach (n; tree.value) {
305             m.map.put(pmap.get(n));
306         }
307         app.put(m);
308     }
309 
310     return app.data;
311 }
312 
313 PidMap makePidMap() @trusted nothrow {
314     import std.algorithm : startsWith;
315     import std.conv : to;
316     import std.path : buildPath, baseName;
317     import std.stdio : File;
318     import std..string : strip;
319 
320     static RawPid parsePpid(string fname) nothrow {
321         try {
322             static immutable prefix = "PPid:";
323             foreach (l; File(fname).byLine.filter!(a => a.startsWith(prefix))) {
324                 return l[prefix.length .. $].strip.to!int.RawPid;
325             }
326         } catch (Exception e) {
327         }
328         return 0.to!int.RawPid;
329     }
330 
331     static string[] procDirs() nothrow {
332         auto app = appender!(string[])();
333         try {
334             foreach (p; dirEntries("/proc", SpanMode.shallow)) {
335                 try {
336                     if (p.isDir) {
337                         app.put(p.name);
338                     }
339                 } catch (Exception e) {
340                 }
341             }
342         } catch (Exception e) {
343         }
344         return app.data;
345     }
346 
347     PidMap rval;
348     foreach (const p; procDirs) {
349         try {
350             const pid = RawPid(p.baseName.to!int);
351             const uid = readText(buildPath(p, "loginuid")).to!uid_t.ifThrown(cast(uid_t) 0);
352             const parent = parsePpid(buildPath(p, "status"));
353 
354             rval.put(PidMap.Pid(pid, PidMap.Stat(uid), null, parent, null));
355             rval.putChild(parent, pid);
356         } catch (ConvException e) {
357         } catch (Exception e) {
358             logger.trace(e.msg).collectException;
359         }
360     }
361 
362     return rval;
363 }
364 
365 /// Returns: a `PidMap` that only contains those processes that are owned by `uid`.
366 PidMap filterBy(PidMap pmap, const uid_t uid) nothrow {
367     if (pmap.empty)
368         return pmap;
369 
370     auto rval = pmap;
371     foreach (k; pmap.stat
372             .byKeyValue
373             .filter!(a => a.value.uid != uid)
374             .map!(a => a.key)
375             .array) {
376         rval.remove(k);
377     }
378 
379     return rval;
380 }
381 
382 PidMap filterByCurrentUser(PidMap pmap) nothrow {
383     import core.sys.posix.unistd : getuid;
384 
385     return filterBy(pmap, getuid());
386 }
387 
388 /// Update the executable of all pids in the map
389 void updateProc(ref PidMap pmap) @trusted nothrow {
390     static string parseCmdline(string pid) @trusted {
391         import std.utf : byUTF;
392 
393         try {
394             return readLink(buildPath("/proc", pid, "exe"));
395         } catch (Exception e) {
396         }
397 
398         auto s = appender!(const(char)[])();
399         foreach (c; File(buildPath("/proc", pid, "cmdline")).byChunk(4096).joiner) {
400             if (c == '\0')
401                 break;
402             s.put(c);
403         }
404         return cast(immutable) s.data.byUTF!char.array;
405     }
406 
407     foreach (candidatePid; pmap.pids) {
408         try {
409             auto cmd = parseCmdline(candidatePid.to!string);
410             pmap.proc[candidatePid] = cmd;
411         } catch (Exception e) {
412             logger.trace(e.msg).collectException;
413         }
414     }
415 }
416 
417 version (unittest) {
418     import unit_threaded.assertions;
419 
420     auto makeTestPidMap(int nodes) {
421         PidMap rval;
422         foreach (n; iota(1, nodes + 1)) {
423             rval.put(PidMap.Pid(RawPid(n), PidMap.Stat(n), null, RawPid(n), null));
424         }
425         return rval;
426     }
427 }
428 
429 @("shall produce a tree")
430 unittest {
431     auto t = makeTestPidMap(10).pids;
432     t.length.shouldEqual(10);
433     RawPid(1).shouldBeIn(t);
434     RawPid(10).shouldBeIn(t);
435 }
436 
437 @("shall produce as many subtrees as there are nodes when no node have a child")
438 unittest {
439     auto t = makeTestPidMap(10);
440     auto s = splitToSubMaps(t);
441     s.length.shouldEqual(10);
442 }
443 
444 @("shall produce one subtree because a node have all the others as children")
445 unittest {
446     auto t = makeTestPidMap(3);
447     t.put(PidMap.Pid(RawPid(20), PidMap.Stat(20), [
448                 RawPid(1), RawPid(2), RawPid(3)
449             ], RawPid(20), "top"));
450     auto s = splitToSubMaps(t);
451     s.length.shouldEqual(1);
452 }