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