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 }