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