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