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 Run in the background a continious purge of all process trees that do not have
7 a distssh client in the root.
8 */
9 module distssh.purge;
10 
11 import logger = std.experimental.logger;
12 import std.algorithm : splitter, map, filter, joiner;
13 import std.array : array, appender, empty;
14 import std.conv;
15 import std.exception : collectException;
16 import std.file;
17 import std.path;
18 import std.range : only;
19 import std.typecons : Flag;
20 
21 import core.sys.posix.sys.types : uid_t;
22 
23 import colorlog;
24 import my.set;
25 import proc;
26 
27 import distssh.config;
28 import distssh.metric;
29 import distssh.types;
30 import distssh.utility;
31 
32 @safe:
33 
34 int cli(const Config fconf, Config.LocalPurge conf) @trusted nothrow {
35     Whitelist wl;
36     try {
37         wl = Whitelist(conf.whiteList);
38     } catch (Exception e) {
39         logger.error(e.msg).collectException;
40         return 1;
41     }
42 
43     void iterate(alias fn)() {
44         auto pmap = () {
45             auto rval = makePidMap;
46             if (conf.userFilter)
47                 return rval.filterByCurrentUser;
48             // assuming that processes owned by root such as init never
49             // interesting to kill. 4294967295 is a magic number used in linux
50             // for system processes.
51             return rval.removeUser(0).removeUser(4294967295);
52         }();
53         updateProc(pmap);
54         debug logger.trace(pmap);
55 
56         foreach (ref t; pmap.splitToSubMaps) {
57             bool hasWhiteListProc;
58             foreach (c; t.map.proc) {
59                 if (wl.match(c)) {
60                     hasWhiteListProc = true;
61                     break;
62                 }
63             }
64 
65             fn(hasWhiteListProc, t.map, t.root);
66         }
67     }
68 
69     void purgeKill(bool hasWhiteListProc, ref PidMap pmap, RawPid root) {
70         if (conf.kill && !hasWhiteListProc) {
71             auto killed = proc.kill(pmap, cast(Flag!"onlyCurrentUser") conf.userFilter);
72             reap(killed);
73         }
74     }
75 
76     void purgePrint(bool hasWhiteListProc, ref PidMap pmap, RawPid root) {
77         import std.stdio : writeln;
78 
79         if (conf.print) {
80             if (hasWhiteListProc && fconf.global.verbosity >= VerboseMode.info) {
81                 logger.info("whitelist".color(Color.green), " process tree");
82                 writeln(toTreeString(pmap));
83             } else if (!hasWhiteListProc) {
84                 logger.info("terminate".color(Color.red), " process tree");
85                 writeln(toTreeString(pmap));
86             }
87         }
88     }
89 
90     try {
91         iterate!purgeKill();
92         iterate!purgePrint();
93     } catch (Exception e) {
94         logger.error(e.msg).collectException;
95         return 1;
96     }
97 
98     return 0;
99 }
100 
101 int cli(const Config fconf, Config.Purge conf) @trusted nothrow {
102     import std.algorithm : sort;
103 
104     auto hosts = RemoteHostCache.make(fconf.global.dbPath, fconf.global.cluster).allRange;
105 
106     if (hosts.empty) {
107         logger.errorf("No remote host online").collectException;
108         return 1;
109     }
110 
111     auto failed = appender!(Host[])();
112     auto econf = ExecuteOnHostConf(fconf.global.workDir, null,
113             fconf.global.importEnv, fconf.global.cloneEnv, fconf.global.noImportEnv);
114 
115     foreach (a; hosts.map!(a => a.host)) {
116         logger.info("Connecting to ", a).collectException;
117         if (purgeServer(econf, conf, a, fconf.global.verbosity) != 0) {
118             failed.put(a);
119         }
120     }
121 
122     if (!failed.data.empty) {
123         logger.warning("Failed to purge").collectException;
124         foreach (a; failed.data) {
125             logger.info(a).collectException;
126         }
127     }
128 
129     return failed.data.empty ? 0 : 1;
130 }
131 
132 int purgeServer(ExecuteOnHostConf econf, const Config.Purge pconf, Host host,
133         VerboseMode vmode = VerboseMode.init) @safe nothrow {
134     import std.file : thisExePath;
135     import std.process : escapeShellFileName;
136 
137     econf.command = () {
138         string[] r;
139         try {
140             r = [thisExePath.escapeShellFileName];
141         } catch (Exception e) {
142             r = ["distssh"];
143         }
144         return r ~ "localpurge";
145     }();
146 
147     try {
148         econf.command ~= ["-v", vmode.to!string];
149     } catch (Exception e) {
150     }
151 
152     if (pconf.print) {
153         econf.command ~= "-p";
154     }
155     if (pconf.kill) {
156         econf.command ~= "-k";
157     }
158     if (pconf.userFilter) {
159         econf.command ~= "--user-filter";
160     }
161 
162     Set!string wlist;
163     foreach (a; only(pconf.whiteList, readPurgeEnvWhiteList).joiner) {
164         wlist.add(a);
165     }
166 
167     econf.command ~= wlist.toArray.map!(a => ["--whitelist", a]).joiner.array;
168 
169     logger.trace("Purge command ", econf.command).collectException;
170 
171     return () @trusted { return executeOnHost(econf, host); }();
172 }
173 
174 string[] readPurgeEnvWhiteList() @safe nothrow {
175     import std.process : environment;
176     import std..string : strip;
177 
178     try {
179         return environment.get(globalEnvPurgeWhiteList, "").strip.splitter(";").map!(a => a.strip)
180             .filter!(a => !a.empty)
181             .array;
182     } catch (Exception e) {
183     }
184 
185     return null;
186 }
187 
188 private:
189 
190 struct Whitelist {
191     import std.regex : regex, matchFirst, Regex;
192 
193     Regex!char[] list;
194 
195     this(string[] whitelist) {
196         foreach (w; whitelist) {
197             list ~= regex(w, "i");
198         }
199     }
200 
201     bool match(string s) {
202         foreach (ref l; list) {
203             auto m = matchFirst(s, l);
204             if (!m.empty) {
205                 return true;
206             }
207         }
208         return false;
209     }
210 }