1 module beangle.task.config;
2 
3 import std.string;
4 import dxml.dom;
5 import std.stdio;
6 import std.conv;
7 import std.file;
8 import std.path;
9 import std.datetime;
10 import std.algorithm;
11 import std.array;
12 import core.time;
13 import beangle.util;
14 
15 class Agent{
16   immutable string name;
17   immutable string secret;
18   immutable string serverURL;
19   Task[string] tasks;
20 
21   this(string name, string secret, string serverURL){
22     this.name = name;
23     this.secret = secret;
24     this.serverURL = serverURL;
25   }
26   void addTask(Task task){
27     this.tasks[task.name] = task;
28   }
29 
30   void removeTasks(Task[] removed){
31     foreach (t; removed){
32       this.tasks.remove(t.name);
33     }
34   }
35 
36   public static Agent parse(string content){
37     auto dom = parseDOM!simpleXML(content).children[0];
38     auto attrs = getAttrs(dom);
39     auto name = attrs.get("name", "agent");
40     auto secret = attrs.get("secret", name);
41     string serverURL=null;
42     foreach (se; children(dom, "server")) {
43       attrs = getAttrs(se);
44       if ("url" in attrs){
45         serverURL = attrs["url"];
46       }
47     }
48     auto agent = new Agent(name, secret, serverURL);
49     auto tasksEntry = children(dom, "task");
50     foreach (t; tasksEntry) {
51       auto task = parseTask(t);
52       agent.addTask(task);
53     }
54     return agent;
55   }
56 }
57 
58 unittest {
59   auto content = `<?xml version="1.0" encoding="UTF-8"?>
60 <agent name="agent" secret="secret">
61   <server url="ws://somehost.com/ws">
62   </server>
63   <task name="check_disk" workdir="~/tmp" host="localhost" repeat="5 seconds" stdout="${task_name}.out">
64     <commands>
65       df -h;
66     </commands>
67   </task>
68 </agent>
69 `;
70   auto agent = Agent.parse(content);
71 
72   assert(agent.name == "agent");
73   assert(agent.serverURL == "ws://somehost.com/ws");
74 
75   assert(agent.tasks.length == 1);
76   assert("check_disk" in agent.tasks);
77   auto task = agent.tasks["check_disk"];
78 }
79 
80 class Profile {
81   Task[string] tasks;
82 
83   void addTask(Task task){
84     tasks[task.name] = task;
85   }
86 
87   public static Profile parse(string content) {
88     Profile profile = new Profile();
89     auto dom = parseDOM!simpleXML(content).children[0];
90     auto tasksEntry = children(dom, "task");
91     foreach (t; tasksEntry) {
92       auto task = parseTask(t);
93       profile.addTask(task);
94     }
95     return profile;
96   }
97 }
98 
99 class Task {
100   immutable string name;
101   immutable string workdir;
102   immutable string host;
103 
104   string[string] envs;
105 
106   immutable string stdout;
107 
108   Commands commands;
109   immutable DateTime beginAt;
110   immutable DateTime endAt;
111   immutable Duration repeat;
112   string lastExecuteAt;
113 
114   this(string name, string workdir, string host, string[string] envs, string commands, DateTime beginAt,
115     DateTime endAt, Duration repeat, string stdout) {
116     this.name = name;
117     auto dir = workdir.replace("${task_name}", name);
118     if (dir.endsWith("/")) {
119       this.workdir = dir[0 .. $ - 1];
120     } else {
121       this.workdir = dir;
122     }
123     this.host = host;
124     this.envs = envs;
125     auto cmd = commands.strip();
126     foreach (k, v; envs){
127       cmd = cmd.replace("${" ~ k ~ "}", v);
128     }
129     this.commands = new Commands(cmd);
130     this.beginAt = beginAt;
131     this.endAt = endAt;
132     this.repeat = repeat;
133     this.stdout = stdout;
134   }
135 
136   int execute() {
137     lastExecuteAt = now().toISOString();
138     import std.file;
139     mkdirRecurse(this.workdir);
140     auto results = this.commands.execute(this.envs, this.workdir);
141     if (this.stdout is null){
142       writeln(results[1]);
143     }else {
144       auto f = File(this.stdout, "w");
145       f.write(results[1]);
146     }
147     return results[0];
148   }
149 
150   int execute(string command){
151     import std.process;
152     import std.file;
153     auto cmd = executeShell(command, this.envs, std.process.Config.none, size_t.max, this.workdir);
154     writeln(cmd.output);
155     return cmd.status;
156   }
157 
158   bool repeatable(){
159     return this.repeat.total!"seconds">0;
160   }
161 
162   DateTime nextExecuteAt(){
163     auto current = now();
164     if ( lastExecuteAt is null){
165       if (repeatable){
166         DateTime startAt = beginAt;
167         while(startAt < current ){
168           startAt = startAt + repeat;
169         }
170         return startAt;
171       }else {
172         auto last = current + 2.seconds;
173         lastExecuteAt = current.toISOString();
174         return last;
175       }
176     }else {
177       auto last = DateTime.fromISOString(lastExecuteAt);
178       if (repeatable){
179         auto startAt = last;
180         while(startAt < current ){
181           startAt = startAt + repeat;
182         }
183         return startAt;
184       }else {
185         return last;
186       }
187     }
188   }
189 
190 }
191 
192 class Commands{
193   const string[] lines;
194   this(string[] commands){
195     this.lines = commands;
196   }
197   this(string cmds){
198     this.lines = to!(immutable(string[])) (splitLines(cmds).map!(x=> strip(x)).filter!(x=> !x.empty && !startsWith(x, "#")).array());
199   }
200 
201   auto execute(string[string] envs, string workdir){
202     import std.process;
203     import std.typecons;
204     import std.array : appender;
205     auto buf = appender!string();
206     foreach (command; lines){
207       auto cmd = executeShell(command, envs, std.process.Config.none, size_t.max, workdir);
208       if (cmd.status != 0) {
209         return tuple(cmd.status, cmd.output);
210       } else {
211         buf.put(cmd.output);
212       }
213     }
214     return tuple(0, buf.data);
215   }
216 }
217 public static Task parseTask(string contents){
218   auto dom = parseDOM!simpleXML(`<?xml version="1.0" encoding="UTF-8"?>` ~ contents).children[0];
219   return parseTask(dom);
220 }
221 
222 unittest{
223   auto a  = parseTask(`<task name="ls" workdir="~/tmp" host="localhost" repeat="3 seconds" stdout="${task_name}.out"><commands>ls -alh;</commands></task>`);
224   assert(a.stdout == "ls.out");
225 }
226 
227 public static Task parseTask(T)(ref DOMEntity!T t){
228   auto attrs = getAttrs(t);
229   string name = attrs["name"];
230   string workdir = expandTilde(attrs.get("workdir", "~"));
231   string host = attrs.get("host", "127.0.0.1");
232 
233   auto beginAt = ("begin_at" in attrs)? DateTime.fromISOString(attrs["begin_at"]) : now();
234   DateTime endAt = DateTime.fromISOString(attrs.get("end_at", "99991231T115959"));
235   Duration repeat = 0.seconds;
236   if ( "repeat" in attrs){
237     auto interval = attrs["repeat"];
238     import std.uni : isWhite;
239     auto parts = interval.split!isWhite;
240     assert(parts.length == 2);
241     switch (parts[1]){
242       case "days":
243         repeat = (parts[0].to!int).days;
244         break;
245       case "weeks":
246         repeat = (parts[0].to!int).weeks;
247         break;
248       case "hours":
249         repeat = (parts[0].to!int).hours;
250         break;
251       case "minutes":
252         repeat = (parts[0].to!int).minutes;
253         break;
254       case "seconds":
255         repeat = (parts[0].to!int).seconds;
256         break;
257         default:
258         throw new Exception("cannot support "~parts[1]~" as duration units");
259     }
260   }
261 
262   string stdout = attrs.get("stdout", workdir ~ "/" ~ name ~ ".out");
263   stdout = expandTilde(replace(stdout, "${task_name}", name));
264   auto envElems = children(t, "env");
265   string[string] envs;
266   envs["task_name"] = name;
267   envs["begin_at"] = beginAt.toISOString;
268   foreach (e; envElems) {
269     auto a = getAttrs(e);
270     auto value = a["value"];
271     if (value.indexOf("${") > -1 ){
272       foreach (k, v; envs){
273         value = value.replace("${" ~ k ~ "}", v);
274       }
275     }
276     envs[a["name"]] = value;
277   }
278   auto cmdElems = children(t, "commands");
279   string command = null;
280   if (!cmdElems.empty){
281     command = cmdElems.front.children[0].text;
282   }
283   return new Task(name, workdir, host, envs, command, beginAt, endAt, repeat, stdout);
284 }
285 
286 string[string] getAttrs(T)(ref DOMEntity!T dom) {
287   string[string] a;
288   foreach (at; dom.attributes) {
289     a[at.name] = at.value;
290   }
291   return a;
292 }
293 
294 auto children(T)(ref DOMEntity!T dom, string path) {
295   return dom.children.filter!(c => c.name == path);
296 }
297 
298 unittest {
299   auto content = `<?xml version="1.0" encoding="UTF-8"?>
300 <profile>
301   <task name="openurp" workdir="~/tmp" host="localhost" begin_at="20221231T160000" stdout="${task_name}.out">
302     <env name="database" value="sues"/>
303     <env name="target_file" value="${task_name}_${begin_at}.dmp"/>
304     <commands k="v">
305       echo ${target_file} ${database}
306       #list dir
307       ls -al
308     </commands>
309   </task>
310 </profile>
311 `;
312   auto profile = Profile.parse(content);
313 
314   assert(profile.tasks.length == 1);
315   assert("openurp" in profile.tasks);
316   writeln(profile.tasks["openurp"].commands);
317   //config.tasks["openurp"].execute();
318 }
319 
320 /**
321   Server settings for startup a sas task server.
322  */
323 class ServerSetting{
324   string[] ips;
325   ushort port;
326   string path;
327   string secret;
328   string[string] agentSecrets;
329 
330   this(string[] ips, ushort port, string path, string secret, string[string] agentSecrets) {
331     this.ips = ips;
332     this.port = port;
333     this.path = path;
334     this.secret = secret;
335     this.agentSecrets = agentSecrets;
336   }
337 
338   public static ServerSetting parse(string content) {
339     import std.conv;
340 
341     auto dom = parseDOM!simpleXML(content).children[0];
342     auto attrs = getAttrs(dom);
343     string hosts;
344     if ("ips" in attrs) {
345       hosts = attrs.get("ips", "127.0.0.1");
346     } else {
347       hosts = attrs.get("hosts", "127.0.0.1");
348     }
349     ushort port = attrs.get("port", "8080").to!ushort;
350     auto path = attrs.get("path", "/sas");
351     auto passwd = attrs.get("secret", "changeit");
352 
353     import beangle.xml;
354     string[string] agentPasswds;
355     foreach (entry; children(dom, "agent")){
356       auto agentAttrs = getAttrs(entry);
357       agentPasswds[agentAttrs["name"]] = agentAttrs["secret"];
358     }
359     agentPasswds.rehash();
360     return new ServerSetting(split(hosts, ","), port, path, passwd, agentPasswds);
361   }
362 }
363 unittest {
364   auto content = `<?xml version="1.0" encoding="UTF-8"?>
365 <sas host="localhost" port="8080" path="/sastask" secret="changeit">
366 	<agent name="agent1" secret="agent_password1"/>
367 	<agent name="agent2" secret="agent_password2"/>
368 </sas>
369 `;
370   auto setting = ServerSetting.parse(content);
371   assert(setting.agentSecrets.length == 2);
372   assert("agent2" in setting.agentSecrets);
373   assert("/sastask" == setting.path);
374   assert("changeit" == setting.secret);
375 }
376