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