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