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 }