1 /** 2 Copyright: Copyright (c) 2015-2016 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module launcher; 8 9 import std.experimental.logger; 10 import std.process; 11 import std.string; 12 import std.algorithm; 13 import std.stdio; 14 import std.array; 15 import std.range; 16 public import std.typecons : Flag, Yes, No; 17 import std.file; 18 import std.path; 19 import std.conv : to; 20 21 import voxelman.utils.messagewindow; 22 import voxelman.utils.linebuffer; 23 import gui; 24 25 enum DEFAULT_PORT = 1234; 26 27 struct PluginInfo 28 { 29 string id; 30 string semver; 31 string downloadUrl; 32 bool isEnabled = true; 33 PluginInfo*[] dependencies; 34 PluginInfo*[] dependants; 35 } 36 37 struct PluginPack 38 { 39 string id; 40 string semver; 41 string filename; 42 PluginInfo*[] plugins; 43 } 44 45 enum AppType 46 { 47 client, 48 server 49 } 50 51 enum JobType : int 52 { 53 run, 54 compile, 55 compileAndRun, 56 test 57 } 58 59 enum JobState 60 { 61 build, 62 run 63 } 64 65 enum Compiler 66 { 67 dmd, 68 ldc, 69 gdc 70 } 71 72 string[] compilerExeNames = ["dmd", "ldc2", "gdc"]; 73 74 struct JobParams 75 { 76 string[string] runParameters; 77 AppType appType = AppType.client; 78 Flag!"start" start = Yes.start; 79 Flag!"build" build = Yes.build; 80 Flag!"arch64" arch64 = Yes.arch64; 81 Flag!"nodeps" nodeps = Yes.nodeps; 82 Flag!"force" force = No.force; 83 Flag!"release" release = No.release; 84 Compiler compiler; 85 JobType jobType; 86 } 87 88 struct Job 89 { 90 JobParams params; 91 string command; 92 MessageWindow messageWindow; 93 ProcessPipes pipes; 94 95 JobState jobState = JobState.build; 96 string title; 97 bool isRunning; 98 bool needsClose; 99 bool needsRestart; 100 int status; 101 } 102 103 string jobStateString(Job* job) 104 { 105 if (!job.isRunning) return "[STOPPED]"; 106 final switch(job.jobState) with(JobState) 107 { 108 case build: break; 109 case run: return "[RUNNING]"; 110 } 111 112 final switch(job.params.jobType) with(JobType) 113 { 114 case run: return "[INVALID]"; 115 case compile: return "[BUILDING]"; 116 case compileAndRun: return "[BUILDING]"; 117 case test: return "[TESTING]"; 118 } 119 assert(false); 120 } 121 122 struct ServerInfo 123 { 124 string name; 125 string ip; 126 ushort port; 127 } 128 129 immutable buildFolder = "builds/default"; 130 immutable configFolder = "config"; 131 immutable serversFname = "config/servers.txt"; 132 struct Launcher 133 { 134 string pluginFolderPath; 135 string pluginPackFolderPath; 136 PluginInfo*[] plugins; 137 PluginInfo*[string] pluginsById; 138 PluginPack*[] pluginPacks; 139 PluginPack*[string] pluginsPacksById; 140 ServerInfo*[] servers; 141 142 Job*[] jobs; 143 size_t numRunningJobs; 144 LineBuffer appLog; 145 146 void createJob(JobParams params = JobParams.init) 147 { 148 auto job = new Job(params); 149 job.messageWindow.init(); 150 job.messageWindow.messageHandler = (string com)=>sendCommand(job,com); 151 updateJobType(job); 152 restartJobState(job); 153 updateTitle(job); 154 jobs ~= job; 155 } 156 157 static void updateTitle(Job* job) 158 { 159 string title = job.params.appType == AppType.client ? `Client` : `Server`; 160 job.title = title; 161 } 162 163 static void updateJobType(Job* job) 164 { 165 final switch(job.params.jobType) with(JobType) { 166 case run: 167 job.params.build = No.build; 168 job.params.start = Yes.start; 169 break; 170 case compile: 171 job.params.build = Yes.build; 172 job.params.start = No.start; 173 break; 174 case compileAndRun: 175 job.params.build = Yes.build; 176 job.params.start = Yes.start; 177 break; 178 case test: 179 job.params.build = Yes.build; 180 job.params.start = No.start; 181 break; 182 } 183 } 184 185 static void restartJobState(Job* job) 186 { 187 final switch(job.params.jobType) with(JobType) { 188 case run: job.jobState = JobState.run; break; 189 case compile: job.jobState = JobState.build; break; 190 case compileAndRun: job.jobState = JobState.build; break; 191 case test: job.jobState = JobState.build; break; 192 } 193 } 194 195 void startJob(Job* job) 196 { 197 assert(!job.isRunning); 198 ++numRunningJobs; 199 200 updateJobType(job); 201 updateTitle(job); 202 203 string command; 204 string workDir; 205 206 if (job.jobState == JobState.build) 207 { 208 final switch(job.params.jobType) with(JobType) { 209 case run: return; 210 case compile: goto case; 211 case compileAndRun: 212 command = makeCompileCommand(job.params); 213 workDir = ""; 214 break; 215 case test: 216 command = makeTestCommand(job.params); 217 workDir = ""; 218 break; 219 } 220 } 221 else if (job.jobState == JobState.run) { 222 command = makeRunCommand(job.params); 223 workDir = buildFolder; 224 } 225 226 ProcessPipes pipes = pipeShell(command, Redirect.all, null, Config.none, workDir); 227 228 (*job) = Job(job.params, command, job.messageWindow, pipes, job.jobState, job.title); 229 job.isRunning = true; 230 } 231 232 size_t stopProcesses() 233 { 234 foreach(job; jobs) 235 job.pipes.pid.kill; 236 return jobs.length; 237 } 238 239 bool anyProcessesRunning() @property 240 { 241 return numRunningJobs > 0; 242 } 243 244 void update() 245 { 246 foreach(job; jobs) logPipes(job); 247 248 foreach(job; jobs) 249 { 250 if (job.isRunning) 251 { 252 auto res = job.pipes.pid.tryWait(); 253 if (res.terminated) 254 { 255 --numRunningJobs; 256 job.isRunning = false; 257 job.status = res.status; 258 259 bool success = job.status == 0; 260 bool doneBuild = job.jobState == JobState.build; 261 bool needsStart = job.params.start; 262 if (doneBuild) 263 { 264 onJobBuildCompletion(job, job.status == 0); 265 } 266 if (success && doneBuild && needsStart) 267 { 268 job.jobState = JobState.run; 269 startJob(job); 270 } 271 } 272 } 273 274 if (!job.isRunning && job.needsRestart) 275 { 276 job.messageWindow.lineBuffer.clear(); 277 restartJobState(job); 278 startJob(job); 279 } 280 281 job.needsRestart = false; 282 } 283 284 jobs = remove!(a => a.needsClose && !a.isRunning)(jobs); 285 jobs.each!(j => j.needsClose = false); 286 } 287 288 void setRootPath(string pluginFolder, string pluginPackFolder, string toolFolder) 289 { 290 pluginFolderPath = pluginFolder; 291 pluginPackFolderPath = pluginPackFolder; 292 } 293 294 void clear() 295 { 296 plugins = null; 297 pluginsById = null; 298 pluginPacks = null; 299 pluginsPacksById = null; 300 } 301 302 void readPlugins() 303 { 304 if (!exists(pluginFolderPath)) return; 305 foreach (entry; dirEntries(pluginFolderPath, SpanMode.depth)) 306 { 307 if (entry.isFile && baseName(entry.name) == "plugininfo.d") 308 { 309 string fileData = cast(string)read(entry.name); 310 auto p = readPluginInfo(fileData); 311 plugins ~= p; 312 pluginsById[p.id] = p; 313 } 314 } 315 } 316 317 void readPluginPacks() 318 { 319 foreach (entry; dirEntries(pluginPackFolderPath, SpanMode.depth)) 320 { 321 if (entry.isFile && entry.name.extension == ".txt") 322 { 323 string fileData = cast(string)read(entry.name); 324 auto pack = readPluginPack(fileData); 325 pack.filename = entry.name.absolutePath.buildNormalizedPath; 326 pluginPacks ~= pack; 327 pluginsPacksById[pack.id] = pack; 328 } 329 } 330 } 331 332 void readServers() 333 { 334 import std.regex : matchFirst, ctRegex; 335 if (!exists(serversFname)) return; 336 string serversData = cast(string)read(serversFname); 337 foreach(line; serversData.lineSplitter) 338 { 339 auto serverInfoStr = matchFirst(line, ctRegex!(`(?P<ip>[^:]*):(?P<port>\d{1,5})\s*(?P<name>.*)`, "s")); 340 auto sinfo = new ServerInfo; 341 sinfo.ip = serverInfoStr["ip"].toCString; 342 sinfo.port = to!ushort(serverInfoStr["port"]); 343 sinfo.name = serverInfoStr["name"].toCString; 344 infof("%s", *sinfo); 345 servers ~= sinfo; 346 } 347 } 348 349 void addServer(ServerInfo server) 350 { 351 auto info = new ServerInfo(); 352 *info = server; 353 servers ~= info; 354 saveServers(); 355 } 356 357 void removeServer(size_t serverIndex) 358 { 359 servers = remove(servers, serverIndex); 360 saveServers(); 361 } 362 363 void saveServers() 364 { 365 import std.exception; 366 try 367 { 368 auto f = File(serversFname, "w"); 369 foreach(server; servers) 370 { 371 f.writefln("%s:%s %s", server.ip, server.port, server.name); 372 } 373 } 374 catch(ErrnoException e) 375 { 376 error(e); 377 } 378 } 379 } 380 381 string makeCompileCommand(JobParams params) 382 { 383 immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`; 384 immutable deps = params.nodeps ? ` --nodeps` : ``; 385 immutable doForce = params.force ? ` --force` : ``; 386 immutable release = params.release ? `--build=release` : `--build=debug`; 387 immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]); 388 return format("dub build -q %s %s --config=exe%s%s %s\0", arch, compiler, deps, doForce, release)[0..$-1]; 389 } 390 391 string makeRunCommand(JobParams params) 392 { 393 string conf = params.appType == AppType.client ? `voxelman.exe --app=client` : `voxelman.exe --app=server`; 394 string command = conf; 395 396 foreach(paramName, paramValue; params.runParameters) 397 { 398 if (paramValue) 399 command ~= format(" --%s=%s", paramName, paramValue); 400 else 401 command ~= format(" --%s", paramName); 402 } 403 404 command ~= '\0'; 405 return command[0..$-1]; 406 } 407 408 string makeTestCommand(JobParams params) 409 { 410 immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`; 411 immutable deps = params.nodeps ? ` --nodeps` : ``; 412 immutable doForce = params.force ? ` --force` : ``; 413 immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]); 414 return format("dub test -q %s %s %s %s\0", arch, compiler, deps, doForce)[0..$-1]; 415 } 416 417 void onJobBuildCompletion(Job* job, bool success) 418 { 419 if (success) 420 { 421 if (job.params.jobType != JobType.test) 422 job.messageWindow.putln("Compilation successful"); 423 } 424 else 425 { 426 if (job.params.jobType != JobType.test) 427 job.messageWindow.putln("Compilation failed"); 428 } 429 } 430 431 void sendCommand(Job* job, string command) 432 { 433 if (!job.isRunning) return; 434 job.pipes.stdin.rawWrite(command); 435 job.pipes.stdin.rawWrite("\n"); 436 } 437 438 void logPipes(Job* job) 439 { 440 import std.exception : ErrnoException; 441 import std.utf : UTFException; 442 if (!job.isRunning) return; 443 444 try 445 { 446 foreach(pipe; only(job.pipes.stdout, job.pipes.stderr)) 447 { 448 auto size = pipe.size; 449 if (size > 0) 450 { 451 char[1024] buf; 452 size_t charsToRead = min(pipe.size, buf.length); 453 char[] data = pipe.rawRead(buf[0..charsToRead]); 454 job.messageWindow.lineBuffer.put(data); 455 } 456 } 457 } 458 catch(ErrnoException e) 459 { // Ignore e 460 // It happens only when both launcher and child process is 32bit 461 // and child crashes with access violation (in opengl call for example). 462 // exception std.exception.ErrnoException@std\stdio.d(920): 463 // Could not seek in file `HANDLE(32C)' (Invalid argument) 464 } 465 catch(UTFException e) 466 { // Ignore e 467 } 468 } 469 470 PluginInfo* readPluginInfo(string fileData) 471 { 472 import std.regex : matchFirst, ctRegex; 473 474 auto pinfo = new PluginInfo; 475 476 auto idCapture = matchFirst(fileData, ctRegex!(`id\s*=\s*"(?P<id>[^"]*)"`, "s")); 477 pinfo.id = idCapture["id"].toCString; 478 479 auto semverCapture = matchFirst(fileData, ctRegex!(`semver\s*=\s*"(?P<semver>[^"]*)"`, "s")); 480 pinfo.semver = semverCapture["semver"].toCString; 481 482 return pinfo; 483 } 484 485 PluginPack* readPluginPack(string fileData) 486 { 487 import std.array : empty; 488 import std.regex : matchFirst, ctRegex; 489 import std.string : lineSplitter; 490 491 auto pack = new PluginPack; 492 493 auto input = fileData.lineSplitter; 494 495 if (!input.empty) { 496 auto packInfo = matchFirst(input.front, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 497 pack.id = packInfo["id"].toCString; 498 pack.semver = packInfo["semver"].toCString; 499 input.popFront; 500 } 501 502 foreach(line; input) 503 { 504 if (line.empty) 505 continue; 506 507 auto pluginInfo = matchFirst(line, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 508 auto pinfo = new PluginInfo; 509 pinfo.id = pluginInfo["id"].toCString; 510 pinfo.semver = pluginInfo["semver"].toCString; 511 pack.plugins ~= pinfo; 512 } 513 514 return pack; 515 } 516 517 string toCString(in const(char)[] s) 518 { 519 import std.exception : assumeUnique; 520 auto copy = new char[s.length + 1]; 521 copy[0..s.length] = s[]; 522 copy[s.length] = '\0'; 523 return assumeUnique(copy[0..s.length]); 524 } 525 526 string fromCString(char[] str) 527 { 528 char[] chars = str.ptr.fromStringz(); 529 return chars.ptr[0..chars.length+1].idup[0..$-1]; 530 }