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