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