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