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 }