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