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.world.worlddb; 22 import voxelman.utils.messagewindow; 23 import voxelman.utils.linebuffer; 24 import gui; 25 26 enum DEFAULT_PORT = 1234; 27 28 struct PluginInfo 29 { 30 string id; 31 string semver; 32 string downloadUrl; 33 bool isEnabled = true; 34 PluginInfo*[] dependencies; 35 PluginInfo*[] dependants; 36 } 37 38 struct PluginPack 39 { 40 string id; 41 string semver; 42 string filename; 43 PluginInfo*[] plugins; 44 } 45 46 enum AppType 47 { 48 client, 49 server, 50 combined 51 } 52 53 string[] appTypeString = ["client", "server", "combined"]; 54 string[] appTypeTitle = ["Client", "Server", "Combined"]; 55 56 enum JobType : int 57 { 58 run, 59 compile, 60 compileAndRun, 61 test 62 } 63 64 enum JobState 65 { 66 build, 67 run 68 } 69 70 enum Compiler 71 { 72 dmd, 73 ldc, 74 gdc 75 } 76 77 string[] compilerExeNames = ["dmd", "ldc2", "gdc"]; 78 79 struct JobParams 80 { 81 string[string] runParameters; 82 AppType appType = AppType.client; 83 Flag!"start" start = Yes.start; 84 Flag!"build" build = Yes.build; 85 Flag!"arch64" arch64 = Yes.arch64; 86 Flag!"nodeps" nodeps = Yes.nodeps; 87 Flag!"force" force = No.force; 88 Flag!"release" release = No.release; 89 Compiler compiler; 90 JobType jobType; 91 } 92 93 struct Job 94 { 95 JobParams params; 96 string command; 97 MessageWindow messageWindow; 98 ProcessPipes pipes; 99 100 JobState jobState = JobState.build; 101 string title; 102 bool autoClose; 103 void delegate() onClose; 104 105 bool isRunning; 106 bool needsClose; 107 bool needsRestart; 108 int status; 109 } 110 111 string jobStateString(Job* job) 112 { 113 if (!job.isRunning) return "[STOPPED]"; 114 final switch(job.jobState) with(JobState) 115 { 116 case build: break; 117 case run: return "[RUNNING]"; 118 } 119 120 final switch(job.params.jobType) with(JobType) 121 { 122 case run: return "[INVALID]"; 123 case compile: return "[BUILDING]"; 124 case compileAndRun: return "[BUILDING]"; 125 case test: return "[TESTING]"; 126 } 127 assert(false); 128 } 129 130 struct ServerInfo 131 { 132 string name; 133 string ip; 134 ushort port; 135 } 136 137 struct SaveInfo 138 { 139 string name; 140 string displaySize; 141 string path; 142 ulong size; 143 } 144 145 immutable buildFolder = "builds/default"; 146 immutable configFolder = "config"; 147 immutable serversFname = "config/servers.txt"; 148 immutable saveFolder = "saves"; 149 immutable saveExtention = ".db"; 150 151 struct Launcher 152 { 153 string pluginFolderPath; 154 string pluginPackFolderPath; 155 PluginInfo*[] plugins; 156 PluginInfo*[string] pluginsById; 157 PluginPack*[] pluginPacks; 158 PluginPack*[string] pluginsPacksById; 159 160 ServerInfo*[] servers; 161 SaveInfo*[] saves; 162 WorldDb worldDb; 163 164 Job* clientProcess; 165 Job* serverProcess; 166 167 Job*[] jobs; 168 size_t numRunningJobs; 169 LineBuffer appLog; 170 171 void init() 172 { 173 worldDb = new WorldDb; 174 } 175 176 Job* createJob(JobParams params = JobParams.init) 177 { 178 auto job = new Job(params); 179 job.messageWindow.init(); 180 job.messageWindow.messageHandler = (string com)=>sendCommand(job,com); 181 updateJobType(job); 182 restartJobState(job); 183 updateTitle(job); 184 jobs ~= job; 185 return job; 186 } 187 188 static void updateTitle(Job* job) 189 { 190 job.title = appTypeTitle[job.params.appType]; 191 } 192 193 static void updateJobType(Job* job) 194 { 195 final switch(job.params.jobType) with(JobType) { 196 case run: 197 job.params.build = No.build; 198 job.params.start = Yes.start; 199 break; 200 case compile: 201 job.params.build = Yes.build; 202 job.params.start = No.start; 203 break; 204 case compileAndRun: 205 job.params.build = Yes.build; 206 job.params.start = Yes.start; 207 break; 208 case test: 209 job.params.build = Yes.build; 210 job.params.start = No.start; 211 break; 212 } 213 } 214 215 static void restartJobState(Job* job) 216 { 217 final switch(job.params.jobType) with(JobType) { 218 case run: job.jobState = JobState.run; break; 219 case compile: job.jobState = JobState.build; break; 220 case compileAndRun: job.jobState = JobState.build; break; 221 case test: job.jobState = JobState.build; break; 222 } 223 } 224 225 void startJob(Job* job) 226 { 227 assert(!job.isRunning); 228 ++numRunningJobs; 229 230 updateJobType(job); 231 updateTitle(job); 232 233 string command; 234 string workDir; 235 236 if (job.jobState == JobState.build) 237 { 238 final switch(job.params.jobType) with(JobType) { 239 case run: return; 240 case compile: goto case; 241 case compileAndRun: 242 command = makeCompileCommand(job.params); 243 workDir = ""; 244 break; 245 case test: 246 command = makeTestCommand(job.params); 247 workDir = ""; 248 break; 249 } 250 } 251 else if (job.jobState == JobState.run) { 252 command = makeRunCommand(job.params); 253 workDir = buildFolder; 254 } 255 256 ProcessPipes pipes = pipeShell(command, Redirect.all, null, Config.none, workDir); 257 258 job.command = command; 259 job.pipes = pipes; 260 261 job.isRunning = true; 262 job.needsClose = false; 263 job.needsRestart = false; 264 job.status = 0; 265 } 266 267 size_t stopProcesses() 268 { 269 foreach(job; jobs) 270 job.pipes.pid.kill; 271 return jobs.length; 272 } 273 274 bool anyProcessesRunning() @property 275 { 276 return numRunningJobs > 0; 277 } 278 279 void update() 280 { 281 foreach(job; jobs) logPipes(job); 282 283 foreach(job; jobs) 284 { 285 if (job.isRunning) 286 { 287 auto res = job.pipes.pid.tryWait(); 288 if (res.terminated) 289 { 290 --numRunningJobs; 291 job.isRunning = false; 292 job.status = res.status; 293 294 bool success = job.status == 0; 295 bool doneBuild = job.jobState == JobState.build; 296 bool needsStart = job.params.start; 297 if (doneBuild) 298 { 299 onJobBuildCompletion(job, job.status == 0); 300 } 301 if (success && doneBuild && needsStart) 302 { 303 job.jobState = JobState.run; 304 startJob(job); 305 } 306 } 307 } 308 309 if (!job.isRunning && job.needsRestart) 310 { 311 job.messageWindow.lineBuffer.clear(); 312 restartJobState(job); 313 startJob(job); 314 } 315 316 job.needsRestart = false; 317 318 if (!job.isRunning && job.autoClose) 319 { 320 job.needsClose = true; 321 } 322 } 323 324 Job*[] newJobs; 325 foreach(job; jobs) 326 { 327 if (job.needsClose && !job.isRunning) 328 { 329 if (job.onClose) job.onClose(); 330 } 331 else 332 { 333 job.needsClose = false; 334 newJobs ~= job; 335 } 336 } 337 jobs = newJobs; 338 } 339 340 void setRootPath(string pluginFolder, string pluginPackFolder, string toolFolder) 341 { 342 pluginFolderPath = pluginFolder; 343 pluginPackFolderPath = pluginPackFolder; 344 } 345 346 void refresh() 347 { 348 clear(); 349 readPlugins(); 350 readPluginPacks(); 351 readServers(); 352 readSaves(); 353 } 354 355 void clear() 356 { 357 plugins = null; 358 pluginsById = null; 359 pluginPacks = null; 360 pluginsPacksById = null; 361 servers = null; 362 saves = null; 363 } 364 365 void readPlugins() 366 { 367 if (!exists(pluginFolderPath)) return; 368 foreach (entry; dirEntries(pluginFolderPath, SpanMode.depth)) 369 { 370 if (entry.isFile && baseName(entry.name) == "plugininfo.d") 371 { 372 string fileData = cast(string)read(entry.name); 373 auto p = readPluginInfo(fileData); 374 plugins ~= p; 375 pluginsById[p.id] = p; 376 } 377 } 378 } 379 380 void readPluginPacks() 381 { 382 foreach (entry; dirEntries(pluginPackFolderPath, SpanMode.depth)) 383 { 384 if (entry.isFile && entry.name.extension == ".txt") 385 { 386 string fileData = cast(string)read(entry.name); 387 auto pack = readPluginPack(fileData); 388 pack.filename = entry.name.absolutePath.buildNormalizedPath; 389 pluginPacks ~= pack; 390 pluginsPacksById[pack.id] = pack; 391 } 392 } 393 } 394 395 void readServers() 396 { 397 import std.regex : matchFirst, ctRegex; 398 if (!exists(serversFname)) return; 399 string serversData = cast(string)read(serversFname); 400 foreach(line; serversData.lineSplitter) 401 { 402 auto serverInfoStr = matchFirst(line, ctRegex!(`(?P<ip>[^:]*):(?P<port>\d{1,5})\s*(?P<name>.*)`, "s")); 403 auto sinfo = new ServerInfo; 404 sinfo.ip = serverInfoStr["ip"].toCString; 405 sinfo.port = to!ushort(serverInfoStr["port"]); 406 sinfo.name = serverInfoStr["name"].toCString; 407 infof("%s", *sinfo); 408 servers ~= sinfo; 409 } 410 } 411 412 void addServer(ServerInfo server) 413 { 414 auto info = new ServerInfo(); 415 *info = server; 416 servers ~= info; 417 saveServers(); 418 } 419 420 void removeServer(size_t serverIndex) 421 { 422 servers = remove(servers, serverIndex); 423 saveServers(); 424 } 425 426 void saveServers() 427 { 428 import std.exception; 429 try 430 { 431 auto f = File(serversFname, "w"); 432 foreach(server; servers) 433 { 434 f.writefln("%s:%s %s", server.ip, server.port, server.name); 435 } 436 } 437 catch(ErrnoException e) 438 { 439 error(e); 440 } 441 } 442 443 void createSave(string name) 444 { 445 if (!exists(saveFolder)) 446 { 447 mkdirRecurse(saveFolder); 448 } 449 auto saveFilename = buildPath(saveFolder, name~saveExtention).absolutePath; 450 worldDb.open(saveFilename); 451 worldDb.close(); 452 ulong fileSize = std.file.getSize(saveFilename); 453 string displaySize = formatFileSize(fileSize); 454 saves ~= new SaveInfo(name, displaySize, saveFilename, fileSize); 455 } 456 457 void readSaves() 458 { 459 if (!exists(saveFolder)) return; 460 foreach (entry; dirEntries(saveFolder, SpanMode.shallow)) 461 { 462 if (entry.isFile && extension(entry.name) == saveExtention) 463 { 464 string name = entry.name.baseName.stripExtension.toCString; 465 ulong fileSize = entry.size; 466 string displaySize = formatFileSize(fileSize); 467 saves ~= new SaveInfo(name, displaySize, entry.name.absolutePath, fileSize); 468 } 469 } 470 } 471 472 void deleteSave(size_t saveIndex) 473 { 474 auto save = saves[saveIndex]; 475 infof("delete %s", *save); 476 try std.file.remove(save.path); 477 catch (FileException e) warningf("error deleting save '%s': %s", save.path, e.msg); 478 saves = remove(saves, saveIndex); 479 } 480 481 void connect(ServerInfo* server, PluginPack* pack) 482 { 483 if (!clientProcess) 484 { 485 JobParams params; 486 params.runParameters["pack"] = pack.id; 487 params.appType = AppType.client; 488 params.jobType = JobType.run; 489 clientProcess = createJob(params); 490 clientProcess.autoClose = true; 491 clientProcess.onClose = &onClientClose; 492 startJob(clientProcess); 493 } 494 sendCommand(clientProcess, format("connect --ip=%s --port=%s", server.ip, server.port)); 495 } 496 497 void startCombined(PluginPack* pack, SaveInfo* save) 498 { 499 if (!clientProcess) 500 { 501 JobParams params; 502 params.runParameters["pack"] = pack.id; 503 params.runParameters["world_name"] = save.name; 504 params.appType = AppType.combined; 505 params.jobType = JobType.run; 506 clientProcess = createJob(params); 507 clientProcess.autoClose = true; 508 clientProcess.onClose = &onClientClose; 509 startJob(clientProcess); 510 infof("%s", clientProcess.command); 511 } 512 } 513 514 void startServer(PluginPack* pack, SaveInfo* save) 515 { 516 if (!serverProcess) 517 { 518 JobParams params; 519 params.runParameters["pack"] = pack.id; 520 params.runParameters["world_name"] = save.name; 521 params.appType = AppType.server; 522 params.jobType = JobType.run; 523 serverProcess = createJob(params); 524 serverProcess.autoClose = true; 525 serverProcess.onClose = &onServerClose; 526 startJob(serverProcess); 527 infof("%s", serverProcess.command); 528 } 529 } 530 531 void onClientClose() 532 { 533 clientProcess = null; 534 refresh(); 535 } 536 537 void onServerClose() 538 { 539 serverProcess = null; 540 } 541 } 542 543 string makeCompileCommand(JobParams params) 544 { 545 immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`; 546 immutable deps = params.nodeps ? ` --nodeps` : ``; 547 immutable doForce = params.force ? ` --force` : ``; 548 immutable release = params.release ? `--build=release` : `--build=debug`; 549 immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]); 550 return format("dub build -q %s %s --config=exe%s%s %s\0", arch, compiler, deps, doForce, release)[0..$-1]; 551 } 552 553 string makeRunCommand(JobParams params) 554 { 555 string command = format("voxelman.exe --app=%s", appTypeString[params.appType]); 556 557 foreach(paramName, paramValue; params.runParameters) 558 { 559 if (paramValue) 560 command ~= format(` --%s="%s"`, paramName, paramValue); 561 else 562 command ~= format(" --%s", paramName); 563 } 564 565 command ~= '\0'; 566 return command[0..$-1]; 567 } 568 569 string makeTestCommand(JobParams params) 570 { 571 immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`; 572 immutable deps = params.nodeps ? ` --nodeps` : ``; 573 immutable doForce = params.force ? ` --force` : ``; 574 immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]); 575 return format("dub test -q %s %s %s %s\0", arch, compiler, deps, doForce)[0..$-1]; 576 } 577 578 void onJobBuildCompletion(Job* job, bool success) 579 { 580 if (success) 581 { 582 if (job.params.jobType != JobType.test) 583 job.messageWindow.putln("Compilation successful"); 584 } 585 else 586 { 587 if (job.params.jobType != JobType.test) 588 job.messageWindow.putln("Compilation failed"); 589 } 590 } 591 592 void sendCommand(Job* job, string command) 593 { 594 if (!job.isRunning) return; 595 job.pipes.stdin.rawWrite(command); 596 job.pipes.stdin.rawWrite("\n"); 597 } 598 599 void logPipes(Job* job) 600 { 601 import std.exception : ErrnoException; 602 import std.utf : UTFException; 603 if (!job.isRunning) return; 604 605 try 606 { 607 foreach(pipe; only(job.pipes.stdout, job.pipes.stderr)) 608 { 609 auto size = pipe.size; 610 if (size > 0) 611 { 612 char[1024] buf; 613 size_t charsToRead = min(pipe.size, buf.length); 614 char[] data = pipe.rawRead(buf[0..charsToRead]); 615 job.messageWindow.lineBuffer.put(data); 616 } 617 } 618 } 619 catch(ErrnoException e) 620 { // Ignore e 621 // It happens only when both launcher and child process is 32bit 622 // and child crashes with access violation (in opengl call for example). 623 // exception std.exception.ErrnoException@std\stdio.d(920): 624 // Could not seek in file `HANDLE(32C)' (Invalid argument) 625 } 626 catch(UTFException e) 627 { // Ignore e 628 } 629 } 630 631 PluginInfo* readPluginInfo(string fileData) 632 { 633 import std.regex : matchFirst, ctRegex; 634 635 auto pinfo = new PluginInfo; 636 637 auto idCapture = matchFirst(fileData, ctRegex!(`id\s*=\s*"(?P<id>[^"]*)"`, "s")); 638 pinfo.id = idCapture["id"].toCString; 639 640 auto semverCapture = matchFirst(fileData, ctRegex!(`semver\s*=\s*"(?P<semver>[^"]*)"`, "s")); 641 pinfo.semver = semverCapture["semver"].toCString; 642 643 return pinfo; 644 } 645 646 PluginPack* readPluginPack(string fileData) 647 { 648 import std.array : empty; 649 import std.regex : matchFirst, ctRegex; 650 import std.string : lineSplitter; 651 652 auto pack = new PluginPack; 653 654 auto input = fileData.lineSplitter; 655 656 if (!input.empty) { 657 auto packInfo = matchFirst(input.front, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 658 pack.id = packInfo["id"].toCString; 659 pack.semver = packInfo["semver"].toCString; 660 input.popFront; 661 } 662 663 foreach(line; input) 664 { 665 if (line.empty) 666 continue; 667 668 auto pluginInfo = matchFirst(line, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m")); 669 auto pinfo = new PluginInfo; 670 pinfo.id = pluginInfo["id"].toCString; 671 pinfo.semver = pluginInfo["semver"].toCString; 672 pack.plugins ~= pinfo; 673 } 674 675 return pack; 676 } 677 678 string formatFileSize(ulong fileSize) 679 { 680 import voxelman.utils.scale; 681 int scale = calcScale(fileSize); 682 double scaledSize = scaled(fileSize, scale); 683 auto prec = stepPrecision(scaledSize); 684 string unitPrefix = scales[scale]; 685 return format("%.*f %sB", prec, scaledSize, unitPrefix); 686 } 687 688 string toCString(in const(char)[] s) 689 { 690 import std.exception : assumeUnique; 691 auto copy = new char[s.length + 1]; 692 copy[0..s.length] = s[]; 693 copy[s.length] = '\0'; 694 return assumeUnique(copy[0..s.length]); 695 } 696 697 string fromCString(char[] str) 698 { 699 char[] chars = str.ptr.fromStringz(); 700 return chars.ptr[0..chars.length+1].idup[0..$-1]; 701 }