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