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 
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 PID `p` 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     /// Remove all PIDs owned by the user `uid`.
138     ref PidMap removeUser(uid_t uid) return nothrow {
139         auto removePids = appender!(RawPid[])();
140         foreach (a; stat.byKeyValue.filter!(a => a.value.uid == uid)) {
141             removePids.put(a.key);
142         }
143         foreach (k; removePids.data) {
144             this.remove(k);
145         }
146 
147         return this;
148     }
149 
150     RawPid[] getChildren(RawPid p) nothrow {
151         if (auto v = p in children) {
152             return *v;
153         }
154         return null;
155     }
156 
157     string getProc(RawPid p) nothrow {
158         if (auto v = p in proc) {
159             return *v;
160         }
161         return null;
162     }
163 
164     /// Returns: a `PidMap` that is a subtree with `p` as its root.
165     PidMap getSubMap(const RawPid p) nothrow {
166         PidMap rval;
167         RawPid[] s;
168         {
169             auto g = get(p);
170             g.parent = p;
171             rval.put(g);
172             s = g.children;
173         }
174         while (!s.empty) {
175             auto f = s[0];
176             s = s[1 .. $];
177 
178             auto g = get(f);
179             rval.put(g);
180             s ~= g.children;
181         }
182 
183         return rval;
184     }
185 
186     import std.range : isOutputRange;
187 
188     string toString() @safe {
189         import std.array : appender;
190 
191         auto buf = appender!string;
192         toString(buf);
193         return buf.data;
194     }
195 
196     void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, char)) {
197         import std.format : formattedWrite;
198         import std.range : put;
199 
200         formattedWrite(w, "PidMap(\n");
201         foreach (n; pids) {
202             formattedWrite(w, `Pid(%s, stat:%s, parent:%s, "%s", %s)`, n,
203                     stat[n], parent[n], getProc(n), getChildren(n));
204             put(w, "\n");
205         }
206         put(w, ")");
207     }
208 }
209 
210 /** Kill all pids in the map.
211  *
212  * Repeats until all pids are killed. It will continiue until all processes
213  * are killed by generating an updated `PidMap` and inspecting it to see that
214  * no new processes have been started.
215  *
216  * Returns: a pid list of the killed pids that may need to be called wait on.
217  *
218  * TODO: remove @trusted when upgrading the minimum compiler >2.091.0
219  */
220 RawPid[] kill(PidMap pmap, Flag!"onlyCurrentUser" user) @trusted nothrow {
221     static import core.sys.posix.signal;
222 
223     static void killMap(RawPid[] pids) @trusted nothrow {
224         foreach (const c; pids) {
225             core.sys.posix.signal.kill(c, core.sys.posix.signal.SIGKILL);
226         }
227     }
228 
229     auto rval = appender!(RawPid[])();
230     auto toKill = [pmap.filterByCurrentUser];
231     while (!toKill.empty) {
232         auto f = toKill[0];
233         toKill = toKill[1 .. $];
234 
235         auto pids = f.pids;
236         killMap(pids);
237         rval.put(pids);
238 
239         pmap = () {
240             if (user)
241                 return makePidMap.filterByCurrentUser;
242             return makePidMap;
243         }();
244 
245         foreach (s; pids.map!(a => tuple(a, pmap.getSubMap(a)))
246                 .map!(a => a[1].remove(a[0]))
247                 .filter!(a => !a.empty)) {
248             toKill ~= s;
249         }
250     }
251 
252     return rval.data;
253 }
254 
255 /// Reap all pids by calling wait on them.
256 void reap(RawPid[] pids) @trusted nothrow {
257     import core.sys.posix.sys.wait : waitpid, WNOHANG;
258 
259     foreach (c; pids) {
260         waitpid(c, null, WNOHANG);
261     }
262 }
263 
264 /// Split a `PidMap` so each map have one top pid as the `root`.
265 Tuple!(PidMap, "map", RawPid, "root")[] splitToSubMaps(PidMap pmap) {
266     import std.range : ElementType;
267 
268     RawPid[][RawPid] trees;
269     RawPid[RawPid] parent;
270 
271     void migrate(RawPid from, RawPid to) {
272         auto p = parent[to];
273         if (auto v = from in trees) {
274             trees[p] ~= *v;
275             trees.remove(from);
276         }
277 
278         foreach (k; parent.byKeyValue
279                 .filter!(a => a.value == from)
280                 .map!(a => a.key)
281                 .array) {
282             parent[k] = p;
283         }
284     }
285 
286     // populate, simplifies the migration if all nodes exists with an
287     // individual tree.
288     foreach (n; pmap.pids) {
289         parent[n] = n;
290         trees[n] = [n];
291     }
292 
293     foreach (n; pmap.pids) {
294         foreach (c; pmap.getChildren(n)) {
295             migrate(c, n);
296         }
297     }
298 
299     alias RT = ElementType!(typeof(return));
300     auto app = appender!(RT[])();
301 
302     foreach (tree; trees.byKeyValue) {
303         RT m;
304         m.root = tree.key;
305         foreach (n; tree.value) {
306             m.map.put(pmap.get(n));
307         }
308         app.put(m);
309     }
310 
311     return app.data;
312 }
313 
314 PidMap makePidMap() @trusted nothrow {
315     import std.algorithm : startsWith;
316     import std.conv : to;
317     import std.path : buildPath, baseName;
318     import std.stdio : File;
319     import std.string : strip;
320 
321     static RawPid parsePpid(string fname) nothrow {
322         try {
323             static immutable prefix = "PPid:";
324             foreach (l; File(fname).byLine.filter!(a => a.startsWith(prefix))) {
325                 return l[prefix.length .. $].strip.to!int.RawPid;
326             }
327         } catch (Exception e) {
328         }
329         return 0.to!int.RawPid;
330     }
331 
332     static string[] procDirs() nothrow {
333         auto app = appender!(string[])();
334         try {
335             foreach (p; dirEntries("/proc", SpanMode.shallow)) {
336                 try {
337                     if (p.isDir) {
338                         app.put(p.name);
339                     }
340                 } catch (Exception e) {
341                 }
342             }
343         } catch (Exception e) {
344         }
345         return app.data;
346     }
347 
348     PidMap rval;
349     foreach (const p; procDirs) {
350         try {
351             const pid = RawPid(p.baseName.to!int);
352             const uid = readText(buildPath(p, "loginuid")).to!uid_t.ifThrown(cast(uid_t) 0);
353             const parent = parsePpid(buildPath(p, "status"));
354 
355             rval.put(PidMap.Pid(pid, PidMap.Stat(uid), null, parent, null));
356             rval.putChild(parent, pid);
357         } catch (ConvException e) {
358         } catch (Exception e) {
359             logger.trace(e.msg).collectException;
360         }
361     }
362 
363     return rval;
364 }
365 
366 /// Returns: a `PidMap` that only contains those processes that are owned by `uid`.
367 PidMap filterBy(PidMap pmap, const uid_t uid) nothrow {
368     if (pmap.empty)
369         return pmap;
370 
371     auto rval = pmap;
372     foreach (k; pmap.stat
373             .byKeyValue
374             .filter!(a => a.value.uid != uid)
375             .map!(a => a.key)
376             .array) {
377         rval.remove(k);
378     }
379 
380     return rval;
381 }
382 
383 PidMap filterByCurrentUser(PidMap pmap) nothrow {
384     import core.sys.posix.unistd : getuid;
385 
386     return filterBy(pmap, getuid());
387 }
388 
389 /// Update the executable of all pids in the map
390 void updateProc(ref PidMap pmap) @trusted nothrow {
391     static string parseCmdline(string pid) @trusted {
392         import std.utf : byUTF;
393 
394         try {
395             return readLink(buildPath("/proc", pid, "exe"));
396         } catch (Exception e) {
397         }
398 
399         auto s = appender!(const(char)[])();
400         foreach (c; File(buildPath("/proc", pid, "cmdline")).byChunk(4096).joiner) {
401             if (c == '\0')
402                 break;
403             s.put(c);
404         }
405         return cast(immutable) s.data.byUTF!char.array;
406     }
407 
408     foreach (candidatePid; pmap.pids) {
409         try {
410             auto cmd = parseCmdline(candidatePid.to!string);
411             pmap.proc[candidatePid] = cmd;
412         } catch (Exception e) {
413             logger.trace(e.msg).collectException;
414         }
415     }
416 }
417 
418 version (unittest) {
419     auto makeTestPidMap(int nodes) {
420         PidMap rval;
421         foreach (n; iota(1, nodes + 1)) {
422             rval.put(PidMap.Pid(RawPid(n), PidMap.Stat(n), null, RawPid(n), null));
423         }
424         return rval;
425     }
426 }
427 
428 @("shall produce a tree")
429 unittest {
430     auto t = makeTestPidMap(10).pids;
431     assert(t.length == 10);
432     assert(canFind(t, RawPid(1)));
433     assert(canFind(t, RawPid(10)));
434 }
435 
436 @("shall produce as many subtrees as there are nodes when no node have a child")
437 unittest {
438     auto t = makeTestPidMap(10);
439     auto s = splitToSubMaps(t);
440     assert(s.length == 10);
441 }
442 
443 @("shall produce one subtree because a node have all the others as children")
444 unittest {
445     auto t = makeTestPidMap(3);
446     t.put(PidMap.Pid(RawPid(20), PidMap.Stat(20), [
447                 RawPid(1), RawPid(2), RawPid(3)
448             ], RawPid(20), "top"));
449     auto s = splitToSubMaps(t);
450     assert(s.length == 1);
451 }