1 module workspaced.com.dcd; 2 3 import std.file : tempDir; 4 5 import core.thread; 6 import std.algorithm; 7 import std.ascii; 8 import std.conv; 9 import std.datetime; 10 import std.experimental.logger; 11 import std.experimental.logger : trace; 12 import std.json; 13 import std.path; 14 import std.process; 15 import std.random; 16 import std.stdio; 17 import std.string; 18 19 import painlessjson; 20 21 import workspaced.api; 22 import workspaced.helpers; 23 24 version (OSX) version = haveUnixSockets; 25 version (linux) version = haveUnixSockets; 26 version (BSD) version = haveUnixSockets; 27 version (FreeBSD) version = haveUnixSockets; 28 29 @component("dcd") 30 class DCDComponent : ComponentWrapper 31 { 32 mixin DefaultComponentWrapper; 33 34 enum latestKnownVersion = [0, 11, 1]; 35 void load() 36 { 37 installedVersion = workspaced.globalConfiguration.get("dcd", "_installedVersion", ""); 38 39 if (installedVersion.length && this.clientPath == workspaced.globalConfiguration.get("dcd", 40 "_clientPath", "") 41 && this.serverPath == workspaced.globalConfiguration.get("dcd", "_serverPath", "")) 42 { 43 version (haveUnixSockets) 44 hasUnixDomainSockets = config.get("dcd", "_hasUnixDomainSockets", false); 45 supportsFullOutput = config.get("dcd", "_supportsFullOutput", false); 46 trace("Reusing previously identified DCD ", installedVersion); 47 } 48 else 49 { 50 reloadBinaries(); 51 } 52 } 53 54 void reloadBinaries() 55 { 56 string clientPath = this.clientPath; 57 string serverPath = this.serverPath; 58 59 installedVersion = clientPath.getVersionAndFixPath; 60 string clientPathInfo = clientPath != "dcd-client" ? "(" ~ clientPath ~ ") " : ""; 61 trace("Detected dcd-client ", clientPathInfo, installedVersion); 62 63 string serverInstalledVersion = serverPath.getVersionAndFixPath; 64 string serverPathInfo = serverPath != "dcd-server" ? "(" ~ serverPath ~ ") " : ""; 65 trace("Detected dcd-server ", serverPathInfo, serverInstalledVersion); 66 67 if (serverInstalledVersion != installedVersion) 68 throw new Exception("client & server version mismatch"); 69 70 config.set("dcd", "clientPath", clientPath); 71 config.set("dcd", "serverPath", serverPath); 72 73 assert(this.clientPath == clientPath); 74 assert(this.serverPath == serverPath); 75 76 version (haveUnixSockets) 77 hasUnixDomainSockets = supportsUnixDomainSockets(installedVersion); 78 79 //dfmt off 80 if (isOutdated) 81 workspaced.broadcast(refInstance, JSONValue([ 82 "type": JSONValue("outdated"), 83 "component": JSONValue("dcd") 84 ])); 85 //dfmt on 86 supportsFullOutput = rawExec([clientPath, "--help"]).output.canFind("--extended"); 87 88 workspaced.globalConfiguration.set("dcd", "_clientPath", clientPath); 89 workspaced.globalConfiguration.set("dcd", "_serverPath", serverPath); 90 version (haveUnixSockets) 91 workspaced.globalConfiguration.set("dcd", "_hasUnixDomainSockets", hasUnixDomainSockets); 92 workspaced.globalConfiguration.set("dcd", "_supportsFullOutput", supportsFullOutput); 93 workspaced.globalConfiguration.set("dcd", "_installedVersion", installedVersion); 94 } 95 96 /// Returns: true if DCD version is less than latestKnownVersion or if server and client mismatch or if it doesn't exist. 97 bool isOutdated() 98 { 99 if (!installedVersion) 100 { 101 string clientPath = this.clientPath; 102 string serverPath = this.serverPath; 103 104 try 105 { 106 installedVersion = clientPath.getVersionAndFixPath; 107 if (serverPath.getVersionAndFixPath != installedVersion) 108 return true; 109 } 110 catch (ProcessException) 111 { 112 return true; 113 } 114 } 115 116 if (installedVersion == "vbin") // locally compiled 117 return false; 118 119 return !checkVersion(installedVersion, latestKnownVersion); 120 } 121 122 /// Returns: the current detected installed version of dcd-client. 123 string clientInstalledVersion() @property const 124 { 125 return installedVersion; 126 } 127 128 private auto serverThreads() 129 { 130 return threads(1, 2); 131 } 132 133 /// This stops the dcd-server instance safely and waits for it to exit 134 override void shutdown(bool dtor = false) 135 { 136 stopServerSync(); 137 if (!dtor && _threads) 138 serverThreads.finish(); 139 } 140 141 /// This will start the dcd-server and load import paths from the current provider 142 void setupServer(string[] additionalImports = [], bool quietServer = false) 143 { 144 startServer(importPaths ~ importFiles ~ additionalImports, quietServer); 145 } 146 147 /// This will start the dcd-server 148 void startServer(string[] additionalImports = [], bool quietServer = false) 149 { 150 if (isPortRunning(port)) 151 throw new Exception("Already running dcd on port " ~ port.to!string); 152 string[] imports; 153 foreach (i; additionalImports) 154 if (i.length) 155 imports ~= "-I" ~ i; 156 this.runningPort = port; 157 this.socketFile = buildPath(tempDir, 158 "workspace-d-sock" ~ thisProcessID.to!string ~ "-" ~ uniform!ulong.to!string(36)); 159 serverPipes = raw([serverPath] ~ clientArgs ~ imports, 160 Redirect.stdin | Redirect.stderr | Redirect.stdoutToStderr); 161 while (!serverPipes.stderr.eof) 162 { 163 string line = serverPipes.stderr.readln(); 164 if (!quietServer) 165 trace("Server: ", line); 166 if (line.canFind("Startup completed in ")) 167 break; 168 } 169 running = true; 170 serverThreads.create({ 171 mixin(traceTask); 172 if (quietServer) 173 foreach (block; serverPipes.stderr.byChunk(4096)) 174 { 175 } 176 else 177 while (serverPipes.stderr.isOpen && !serverPipes.stderr.eof) 178 { 179 auto line = serverPipes.stderr.readln(); 180 trace("Server: ", line); // evaluates lazily, so read before 181 } 182 auto code = serverPipes.pid.wait(); 183 info("DCD-Server stopped with code ", code); 184 if (code != 0) 185 { 186 info("Broadcasting dcd server crash."); 187 workspaced.broadcast(refInstance, JSONValue([ 188 "type": JSONValue("crash"), 189 "component": JSONValue("dcd") 190 ])); 191 running = false; 192 } 193 }); 194 } 195 196 void stopServerSync() 197 { 198 if (!running) 199 return; 200 int i = 0; 201 running = false; 202 doClient(["--shutdown"]).pid.wait; 203 while (serverPipes.pid && !serverPipes.pid.tryWait().terminated) 204 { 205 Thread.sleep(10.msecs); 206 if (++i > 200) // Kill after 2 seconds 207 { 208 killServer(); 209 return; 210 } 211 } 212 } 213 214 /// This stops the dcd-server asynchronously 215 /// Returns: null 216 Future!void stopServer() 217 { 218 auto ret = new Future!void(); 219 gthreads.create({ 220 mixin(traceTask); 221 try 222 { 223 stopServerSync(); 224 ret.finish(); 225 } 226 catch (Throwable t) 227 { 228 ret.error(t); 229 } 230 }); 231 return ret; 232 } 233 234 /// This will kill the process associated with the dcd-server instance 235 void killServer() 236 { 237 if (serverPipes.pid && !serverPipes.pid.tryWait().terminated) 238 serverPipes.pid.kill(); 239 } 240 241 /// This will stop the dcd-server safely and restart it again using setup-server asynchronously 242 /// Returns: null 243 Future!void restartServer(bool quiet = false) 244 { 245 auto ret = new Future!void; 246 gthreads.create({ 247 mixin(traceTask); 248 try 249 { 250 stopServerSync(); 251 setupServer([], quiet); 252 ret.finish(); 253 } 254 catch (Throwable t) 255 { 256 ret.error(t); 257 } 258 }); 259 return ret; 260 } 261 262 /// This will query the current dcd-server status 263 /// Returns: `{isRunning: bool}` If the dcd-server process is not running anymore it will return isRunning: false. Otherwise it will check for server status using `dcd-client --query` 264 auto serverStatus() @property 265 { 266 DCDServerStatus status; 267 if (serverPipes.pid && serverPipes.pid.tryWait().terminated) 268 status.isRunning = false; 269 else if (hasUnixDomainSockets) 270 status.isRunning = true; 271 else 272 status.isRunning = isPortRunning(runningPort); 273 return status; 274 } 275 276 /// Searches for a symbol across all files using `dcd-client --search` 277 Future!(DCDSearchResult[]) searchSymbol(string query) 278 { 279 auto ret = new Future!(DCDSearchResult[]); 280 gthreads.create({ 281 mixin(traceTask); 282 try 283 { 284 if (!running) 285 { 286 ret.finish(null); 287 return; 288 } 289 auto pipes = doClient(["--search", query]); 290 scope (exit) 291 { 292 pipes.pid.wait(); 293 pipes.destroy(); 294 } 295 pipes.stdin.close(); 296 DCDSearchResult[] results; 297 while (pipes.stdout.isOpen && !pipes.stdout.eof) 298 { 299 string line = pipes.stdout.readln(); 300 if (line.length == 0) 301 continue; 302 string[] splits = line.chomp.split('\t'); 303 if (splits.length >= 3) 304 results ~= DCDSearchResult(splits[0], splits[2].to!int, splits[1]); 305 } 306 ret.finish(results); 307 } 308 catch (Throwable t) 309 { 310 ret.error(t); 311 } 312 }); 313 return ret; 314 } 315 316 /// Reloads import paths from the current provider. Call reload there before calling it here. 317 void refreshImports() 318 { 319 addImports(importPaths ~ importFiles); 320 } 321 322 /// Manually adds import paths as string array 323 void addImports(string[] imports) 324 { 325 knownImports ~= imports; 326 updateImports(); 327 } 328 329 string clientPath() @property @ignoredFunc 330 { 331 return config.get("dcd", "clientPath", "dcd-client"); 332 } 333 334 string serverPath() @property @ignoredFunc 335 { 336 return config.get("dcd", "serverPath", "dcd-server"); 337 } 338 339 ushort port() @property @ignoredFunc 340 { 341 return cast(ushort) config.get!int("dcd", "port", 9166); 342 } 343 344 /// Searches for an open port to spawn dcd-server in asynchronously starting with `port`, always increasing by one. 345 /// Returns: 0 if not available, otherwise the port as number 346 Future!ushort findAndSelectPort(ushort port = 9166) 347 { 348 if (hasUnixDomainSockets) 349 { 350 return Future!ushort.fromResult(0); 351 } 352 auto ret = new Future!ushort; 353 gthreads.create({ 354 mixin(traceTask); 355 try 356 { 357 auto newPort = findOpen(port); 358 port = newPort; 359 ret.finish(port); 360 } 361 catch (Throwable t) 362 { 363 ret.error(t); 364 } 365 }); 366 return ret; 367 } 368 369 /// Finds the declaration of the symbol at position `pos` in the code 370 Future!DCDDeclaration findDeclaration(scope const(char)[] code, int pos) 371 { 372 auto ret = new Future!DCDDeclaration; 373 gthreads.create({ 374 mixin(traceTask); 375 try 376 { 377 if (!running || pos >= code.length) 378 { 379 ret.finish(DCDDeclaration.init); 380 return; 381 } 382 383 // We need to move by one character on identifier characters to ensure the start character fits. 384 if (!isIdentifierSeparatingChar(code[pos])) 385 pos++; 386 387 auto pipes = doClient(["-c", pos.to!string, "--symbolLocation"]); 388 scope (exit) 389 { 390 pipes.pid.wait(); 391 pipes.destroy(); 392 } 393 pipes.stdin.write(code); 394 pipes.stdin.close(); 395 string line = pipes.stdout.readln(); 396 if (line.length == 0) 397 { 398 ret.finish(DCDDeclaration.init); 399 return; 400 } 401 string[] splits = line.chomp.split('\t'); 402 if (splits.length != 2) 403 { 404 ret.finish(DCDDeclaration.init); 405 return; 406 } 407 ret.finish(DCDDeclaration(splits[0], splits[1].to!int)); 408 } 409 catch (Throwable t) 410 { 411 ret.error(t); 412 } 413 }); 414 return ret; 415 } 416 417 /// Finds the documentation of the symbol at position `pos` in the code 418 Future!string getDocumentation(scope const(char)[] code, int pos) 419 { 420 auto ret = new Future!string; 421 gthreads.create({ 422 mixin(traceTask); 423 try 424 { 425 if (!running) 426 { 427 ret.finish(""); 428 return; 429 } 430 auto pipes = doClient(["--doc", "-c", pos.to!string]); 431 scope (exit) 432 { 433 pipes.pid.wait(); 434 pipes.destroy(); 435 } 436 pipes.stdin.write(code); 437 pipes.stdin.close(); 438 string data; 439 while (pipes.stdout.isOpen && !pipes.stdout.eof) 440 { 441 string line = pipes.stdout.readln(); 442 if (line.length) 443 data ~= line.chomp; 444 } 445 ret.finish(data.unescapeTabs); 446 } 447 catch (Throwable t) 448 { 449 ret.error(t); 450 } 451 }); 452 return ret; 453 } 454 455 /// Returns the used socket file. Only available on OSX, linux and BSD with DCD >= 0.8.0 456 /// Throws an error if not available. 457 string getSocketFile() 458 { 459 if (!hasUnixDomainSockets) 460 throw new Exception("Unix domain sockets not supported"); 461 return socketFile; 462 } 463 464 /// Returns the used running port. Throws an error if using unix sockets instead 465 ushort getRunningPort() 466 { 467 if (hasUnixDomainSockets) 468 throw new Exception("Using unix domain sockets instead of a port"); 469 return runningPort; 470 } 471 472 /// Queries for code completion at position `pos` in code 473 /// Raw is anything else than identifiers and calltips which might not be implemented by this point. 474 /// calltips.symbols and identifiers.definition, identifiers.file, identifiers.location and identifiers.documentation are only available with dcd ~master as of now. 475 Future!DCDCompletions listCompletion(scope const(char)[] code, int pos) 476 { 477 auto ret = new Future!DCDCompletions; 478 gthreads.create({ 479 mixin(traceTask); 480 try 481 { 482 DCDCompletions completions; 483 if (!running) 484 { 485 info("DCD not running!"); 486 ret.finish(completions); 487 return; 488 } 489 auto pipes = doClient((supportsFullOutput ? ["--extended"] : []) ~ [ 490 "-c", pos.to!string 491 ]); 492 scope (exit) 493 { 494 pipes.pid.wait(); 495 pipes.destroy(); 496 } 497 pipes.stdin.write(code); 498 pipes.stdin.close(); 499 string[] data; 500 while (pipes.stdout.isOpen && !pipes.stdout.eof) 501 { 502 string line = pipes.stdout.readln(); 503 trace("DCD Client: ", line); 504 if (line.length == 0) 505 continue; 506 data ~= line.chomp; 507 } 508 completions.raw = data; 509 int[] emptyArr; 510 if (data.length == 0) 511 { 512 completions.type = DCDCompletions.Type.identifiers; 513 ret.finish(completions); 514 return; 515 } 516 if (data[0] == "calltips") 517 { 518 if (supportsFullOutput) 519 { 520 foreach (line; data[1 .. $]) 521 { 522 auto parts = line.split("\t"); 523 if (parts.length < 5) 524 continue; 525 completions._calltips ~= parts[2]; 526 string location = parts[3]; 527 string file; 528 int index; 529 if (location.length) 530 { 531 auto space = location.lastIndexOf(' '); 532 if (space != -1) 533 { 534 file = location[0 .. space]; 535 if (location[space + 1 .. $].all!isDigit) 536 index = location[space + 1 .. $].to!int; 537 } 538 else 539 file = location; 540 } 541 completions._symbols ~= DCDCompletions.Symbol(file, index, parts[4].unescapeTabs); 542 } 543 } 544 else 545 { 546 completions._calltips = data[1 .. $]; 547 completions._symbols.length = completions._calltips.length; 548 } 549 completions.type = DCDCompletions.Type.calltips; 550 ret.finish(completions); 551 return; 552 } 553 else if (data[0] == "identifiers") 554 { 555 DCDIdentifier[] identifiers; 556 foreach (line; data[1 .. $]) 557 { 558 string[] splits = line.split('\t'); 559 DCDIdentifier symbol; 560 if (supportsFullOutput) 561 { 562 if (splits.length < 5) 563 continue; 564 string location = splits[3]; 565 string file; 566 int index; 567 if (location.length) 568 { 569 auto space = location.lastIndexOf(' '); 570 if (space != -1) 571 { 572 file = location[0 .. space]; 573 if (location[space + 1 .. $].all!isDigit) 574 index = location[space + 1 .. $].to!int; 575 } 576 else 577 file = location; 578 } 579 symbol = DCDIdentifier(splits[0], splits[1], splits[2], file, 580 index, splits[4].unescapeTabs); 581 } 582 else 583 { 584 if (splits.length < 2) 585 continue; 586 symbol = DCDIdentifier(splits[0], splits[1]); 587 } 588 identifiers ~= symbol; 589 } 590 completions.type = DCDCompletions.Type.identifiers; 591 completions._identifiers = identifiers; 592 ret.finish(completions); 593 return; 594 } 595 else 596 { 597 completions.type = DCDCompletions.Type.raw; 598 ret.finish(completions); 599 return; 600 } 601 } 602 catch (Throwable e) 603 { 604 ret.error(e); 605 } 606 }); 607 return ret; 608 } 609 610 void updateImports() 611 { 612 if (!running) 613 return; 614 string[] args; 615 foreach (path; knownImports) 616 if (path.length) 617 args ~= "-I" ~ path; 618 execClient(args); 619 } 620 621 bool fromRunning(bool supportsFullOutput, string socketFile, ushort runningPort) 622 { 623 if (socketFile.length ? isSocketRunning(socketFile) : isPortRunning(runningPort)) 624 { 625 running = true; 626 this.supportsFullOutput = supportsFullOutput; 627 this.socketFile = socketFile; 628 this.runningPort = runningPort; 629 this.hasUnixDomainSockets = !!socketFile.length; 630 return true; 631 } 632 else 633 return false; 634 } 635 636 bool getSupportsFullOutput() @property 637 { 638 return supportsFullOutput; 639 } 640 641 bool isUsingUnixDomainSockets() @property 642 { 643 return hasUnixDomainSockets; 644 } 645 646 bool isActive() @property 647 { 648 return running; 649 } 650 651 private: 652 string installedVersion; 653 bool supportsFullOutput; 654 bool hasUnixDomainSockets = false; 655 bool running = false; 656 ProcessPipes serverPipes; 657 ushort runningPort; 658 string socketFile; 659 string[] knownImports; 660 661 string[] clientArgs() 662 { 663 if (hasUnixDomainSockets) 664 return ["--socketFile", socketFile]; 665 else 666 return ["--port", runningPort.to!string]; 667 } 668 669 auto doClient(string[] args) 670 { 671 return raw([clientPath] ~ clientArgs ~ args); 672 } 673 674 auto raw(string[] args, Redirect redirect = Redirect.all) 675 { 676 return pipeProcess(args, redirect, null, Config.none, refInstance ? instance.cwd : null); 677 } 678 679 auto execClient(string[] args) 680 { 681 return rawExec([clientPath] ~ clientArgs ~ args); 682 } 683 684 auto rawExec(string[] args) 685 { 686 return execute(args, null, Config.none, size_t.max, refInstance ? instance.cwd : null); 687 } 688 689 bool isSocketRunning(string socket) 690 { 691 if (!hasUnixDomainSockets) 692 return false; 693 auto ret = execute([clientPath, "-q", "--socketFile", socket]); 694 return ret.status == 0; 695 } 696 697 bool isPortRunning(ushort port) 698 { 699 if (hasUnixDomainSockets) 700 return false; 701 auto ret = execute([clientPath, "-q", "--port", port.to!string]); 702 return ret.status == 0; 703 } 704 705 ushort findOpen(ushort port) 706 { 707 --port; 708 bool isRunning; 709 do 710 { 711 isRunning = isPortRunning(++port); 712 } 713 while (isRunning); 714 return port; 715 } 716 } 717 718 bool supportsUnixDomainSockets(string ver) 719 { 720 return checkVersion(ver, [0, 8, 0]); 721 } 722 723 unittest 724 { 725 assert(supportsUnixDomainSockets("0.8.0-beta2+9ec55f40a26f6bb3ca95dc9232a239df6ed25c37")); 726 assert(!supportsUnixDomainSockets("0.7.9-beta3")); 727 assert(!supportsUnixDomainSockets("0.7.0")); 728 assert(supportsUnixDomainSockets("v0.9.8 c7ea7e081ed9ad2d85e9f981fd047d7fcdb2cf51")); 729 assert(supportsUnixDomainSockets("1.0.0")); 730 } 731 732 private string unescapeTabs(string val) 733 { 734 return val.replace("\\t", "\t").replace("\\n", "\n").replace("\\\\", "\\"); 735 } 736 737 /// Returned by findDeclaration 738 struct DCDDeclaration 739 { 740 string file; 741 int position; 742 } 743 744 /// Returned by listCompletion 745 /// When identifiers: `{type:"identifiers", identifiers:[{identifier:string, type:string, definition:string, file:string, location:number, documentation:string}]}` 746 /// When calltips: `{type:"calltips", calltips:[string], symbols:[{file:string, location:number, documentation:string}]}` 747 /// When raw: `{type:"raw", raw:[string]}` 748 struct DCDCompletions 749 { 750 /// Type of a completion 751 enum Type 752 { 753 /// Unknown/Unimplemented raw output 754 raw, 755 /// Completion after a dot or a variable name 756 identifiers, 757 /// Completion for arguments in a function call 758 calltips, 759 } 760 761 struct Symbol 762 { 763 string file; 764 int location; 765 string documentation; 766 } 767 768 /// Type of the completion (identifiers, calltips, raw) 769 Type type; 770 /// Contains the raw DCD output 771 string[] raw; 772 union 773 { 774 DCDIdentifier[] _identifiers; 775 struct 776 { 777 string[] _calltips; 778 Symbol[] _symbols; 779 } 780 } 781 782 enum DCDCompletions empty = DCDCompletions(Type.identifiers); 783 784 /// Only set with type==identifiers. 785 inout(DCDIdentifier[]) identifiers() inout @property 786 { 787 if (type != Type.identifiers) 788 throw new Exception("Type is not identifiers but attempted to access identifiers"); 789 return _identifiers; 790 } 791 792 /// Only set with type==calltips. 793 inout(string[]) calltips() inout @property 794 { 795 if (type != Type.calltips) 796 throw new Exception("Type is not calltips but attempted to access calltips"); 797 return _calltips; 798 } 799 800 /// Only set with type==calltips. 801 inout(Symbol[]) symbols() inout @property 802 { 803 if (type != Type.calltips) 804 throw new Exception("Type is not calltips but attempted to access symbols"); 805 return _symbols; 806 } 807 } 808 809 /// Returned by status 810 struct DCDServerStatus 811 { 812 /// 813 bool isRunning; 814 } 815 816 /// Type of the identifiers value in listCompletion 817 struct DCDIdentifier 818 { 819 /// 820 string identifier; 821 /// 822 string type; 823 /// 824 string definition; 825 /// 826 string file; 827 /// byte location 828 int location; 829 /// 830 string documentation; 831 } 832 833 /// Returned by search-symbol 834 struct DCDSearchResult 835 { 836 /// 837 string file; 838 /// 839 int position; 840 /// 841 string type; 842 }