1 module served.dcd.client; 2 3 @safe: 4 5 import core.time; 6 import core.sync.mutex; 7 8 import std.algorithm; 9 import std.array; 10 import std.ascii; 11 import std.conv; 12 import std.process; 13 import std.socket; 14 import std.stdio; 15 import std.string; 16 17 import dcd.common.messages; 18 import dcd.common.dcd_version; 19 import dcd.common.socket; 20 21 version (OSX) version = haveUnixSockets; 22 version (linux) version = haveUnixSockets; 23 version (BSD) version = haveUnixSockets; 24 version (FreeBSD) version = haveUnixSockets; 25 26 public import dcd.common.messages : 27 DCDResponse = AutocompleteResponse, 28 DCDCompletionType = CompletionType, 29 isDCDServerRunning = serverIsRunning; 30 31 version (haveUnixSockets) 32 enum platformSupportsDCDUnixSockets = true; 33 else 34 enum platformSupportsDCDUnixSockets = false; 35 36 interface IDCDClient 37 { 38 string socketFile() const @property; 39 void socketFile(string) @property; 40 ushort runningPort() const @property; 41 void runningPort(ushort) @property; 42 bool usingUnixDomainSockets() const @property; 43 44 bool queryRunning(); 45 bool shutdown(); 46 bool clearCache(); 47 bool addImportPaths(string[] importPaths); 48 bool removeImportPaths(string[] importPaths); 49 string[] listImportPaths(); 50 SymbolInformation requestSymbolInfo(CodeRequest loc); 51 string[] requestDocumentation(CodeRequest loc); 52 DCDResponse.Completion[] requestSymbolSearch(string query); 53 LocalUse requestLocalUse(CodeRequest loc); 54 Completion requestAutocomplete(CodeRequest loc); 55 } 56 57 class ExternalDCDClient : IDCDClient 58 { 59 string clientPath; 60 ushort _runningPort; 61 string _socketFile; 62 63 this(string clientPath) 64 { 65 this.clientPath = clientPath; 66 } 67 68 string socketFile() const @property 69 { 70 return _socketFile; 71 } 72 73 void socketFile(string value) @property 74 { 75 _socketFile = value; 76 } 77 78 ushort runningPort() const @property 79 { 80 return _runningPort; 81 } 82 83 void runningPort(ushort value) @property 84 { 85 _runningPort = value; 86 } 87 88 bool usingUnixDomainSockets() const @property 89 { 90 version (haveUnixSockets) 91 return true; 92 else 93 return false; 94 } 95 96 bool queryRunning() 97 { 98 return doClient(["--query"]).pid.wait == 0; 99 } 100 101 bool shutdown() 102 { 103 return doClient(["--shutdown"]).pid.wait == 0; 104 } 105 106 bool clearCache() 107 { 108 return doClient(["--clearCache"]).pid.wait == 0; 109 } 110 111 bool addImportPaths(string[] importPaths) 112 { 113 string[] args; 114 foreach (path; importPaths) 115 if (path.length) 116 args ~= "-I" ~ path; 117 return execClient(args).status == 0; 118 } 119 120 bool removeImportPaths(string[] importPaths) 121 { 122 string[] args; 123 foreach (path; importPaths) 124 if (path.length) 125 args ~= "-R" ~ path; 126 return execClient(args).status == 0; 127 } 128 129 string[] listImportPaths() 130 { 131 auto pipes = doClient(["--listImports"]); 132 scope (exit) 133 { 134 pipes.pid.wait(); 135 pipes.destroy(); 136 } 137 pipes.stdin.close(); 138 auto results = appender!(string[]); 139 while (pipes.stdout.isOpen && !pipes.stdout.eof) 140 { 141 results.put((() @trusted => pipes.stdout.readln())()); 142 } 143 return results.data; 144 } 145 146 SymbolInformation requestSymbolInfo(CodeRequest loc) 147 { 148 auto pipes = doClient([ 149 "-c", loc.cursorPosition.to!string, "--symbolLocation" 150 ]); 151 scope (exit) 152 { 153 pipes.pid.wait(); 154 pipes.destroy(); 155 } 156 pipes.stdin.write(loc.sourceCode); 157 pipes.stdin.close(); 158 string line = (() @trusted => pipes.stdout.readln())(); 159 if (line.length == 0) 160 return SymbolInformation.init; 161 string[] splits = line.chomp.split('\t'); 162 if (splits.length != 2) 163 return SymbolInformation.init; 164 SymbolInformation ret; 165 ret.declarationFilePath = splits[0]; 166 if (ret.declarationFilePath == "stdin") 167 ret.declarationFilePath = loc.fileName; 168 ret.declarationLocation = splits[1].to!size_t; 169 return ret; 170 } 171 172 string[] requestDocumentation(CodeRequest loc) 173 { 174 auto pipes = doClient(["--doc", "-c", loc.cursorPosition.to!string]); 175 scope (exit) 176 { 177 pipes.pid.wait(); 178 pipes.destroy(); 179 } 180 pipes.stdin.write(loc.sourceCode); 181 pipes.stdin.close(); 182 string[] data; 183 while (pipes.stdout.isOpen && !pipes.stdout.eof) 184 { 185 string line = (() @trusted => pipes.stdout.readln())(); 186 if (line.length) 187 data ~= line.chomp.unescapeTabs; 188 } 189 return data; 190 } 191 192 DCDResponse.Completion[] requestSymbolSearch(string query) 193 { 194 auto pipes = doClient(["--search", query]); 195 scope (exit) 196 { 197 pipes.pid.wait(); 198 pipes.destroy(); 199 } 200 pipes.stdin.close(); 201 auto results = appender!(DCDResponse.Completion[]); 202 while (pipes.stdout.isOpen && !pipes.stdout.eof) 203 { 204 string line = (() @trusted => pipes.stdout.readln())(); 205 if (line.length == 0) 206 continue; 207 string[] splits = line.chomp.split('\t'); 208 if (splits.length >= 3) 209 { 210 DCDResponse.Completion item; 211 item.identifier = query; // hack 212 item.kind = splits[1] == "" ? char.init : splits[1][0]; 213 item.symbolFilePath = splits[0]; 214 item.symbolLocation = splits[2].to!size_t; 215 results ~= item; 216 } 217 } 218 return results.data; 219 } 220 221 LocalUse requestLocalUse(CodeRequest loc) 222 { 223 auto pipes = doClient([ 224 "--localUse", 225 "-c", loc.cursorPosition.to!string 226 ]); 227 scope (exit) 228 { 229 pipes.pid.wait(); 230 pipes.destroy(); 231 } 232 pipes.stdin.write(loc.sourceCode); 233 pipes.stdin.close(); 234 235 string header = (() @trusted => pipes.stdout.readln())().chomp; 236 if (header == "00000" || !header.length) 237 return LocalUse.init; 238 239 LocalUse ret; 240 auto headerParts = header.split('\t'); 241 if (headerParts.length < 2) 242 return LocalUse.init; 243 244 ret.declarationFilePath = headerParts[0]; 245 ret.declarationLocation = headerParts[1].length ? headerParts[1].to!size_t : 0; 246 while (pipes.stdout.isOpen && !pipes.stdout.eof) 247 { 248 string line = (() @trusted => pipes.stdout.readln())().chomp; 249 if (line.length == 0) 250 continue; 251 ret.uses ~= line.to!size_t; 252 } 253 return ret; 254 } 255 256 Completion requestAutocomplete(CodeRequest loc) 257 { 258 auto pipes = doClient([ 259 "--extended", 260 "-c", loc.cursorPosition.to!string 261 ]); 262 scope (exit) 263 { 264 pipes.pid.wait(); 265 pipes.destroy(); 266 } 267 pipes.stdin.write(loc.sourceCode); 268 pipes.stdin.close(); 269 auto dataApp = appender!(string[]); 270 while (pipes.stdout.isOpen && !pipes.stdout.eof) 271 { 272 string line = (() @trusted => pipes.stdout.readln())(); 273 if (line.length == 0) 274 continue; 275 dataApp ~= line.chomp; 276 } 277 278 string[] data = dataApp.data; 279 auto symbols = appender!(DCDResponse.Completion[]); 280 Completion c; 281 if (data.length == 0) 282 { 283 c.type = CompletionType.identifiers; 284 return c; 285 } 286 287 c.type = cast(CompletionType)data[0]; 288 if (c.type == CompletionType.identifiers 289 || c.type == CompletionType.calltips) 290 { 291 foreach (line; data[1 .. $]) 292 { 293 string[] splits = line.split('\t'); 294 DCDResponse.Completion symbol; 295 if (splits.length < 5) 296 continue; 297 string location = splits[3]; 298 string file; 299 int index; 300 if (location.length) 301 { 302 auto space = location.lastIndexOf(' '); 303 if (space != -1) 304 { 305 file = location[0 .. space]; 306 if (location[space + 1 .. $].all!isDigit) 307 index = location[space + 1 .. $].to!int; 308 } 309 else 310 file = location; 311 } 312 symbol.identifier = splits[0]; 313 symbol.kind = splits[1] == "" ? char.init : splits[1][0]; 314 symbol.definition = splits[2]; 315 symbol.symbolFilePath = file; 316 symbol.symbolLocation = index; 317 symbol.documentation = splits[4].unescapeTabs; 318 symbols ~= symbol; 319 } 320 } 321 322 c.completions = symbols.data; 323 return c; 324 } 325 326 private: 327 string[] clientArgs() 328 { 329 if (usingUnixDomainSockets) 330 return ["--socketFile", socketFile]; 331 else 332 return ["--port", runningPort.to!string]; 333 } 334 335 auto doClient(string[] args) 336 { 337 return raw([clientPath] ~ clientArgs ~ args); 338 } 339 340 auto raw(string[] args, Redirect redirect = Redirect.all) 341 { 342 return pipeProcess(args, redirect, null, Config.none, null); 343 } 344 345 auto execClient(string[] args) 346 { 347 return rawExec([clientPath] ~ clientArgs ~ args); 348 } 349 350 auto rawExec(string[] args) 351 { 352 return execute(args, null, Config.none, size_t.max, null); 353 } 354 } 355 356 class BuiltinDCDClient : IDCDClient 357 { 358 public static enum minSupportedServerInclusive = [0, 8, 0]; 359 public static enum maxSupportedServerExclusive = [0, 14, 0]; 360 361 public static immutable clientVersion = DCD_VERSION; 362 363 bool useTCP; 364 string _socketFile; 365 ushort port = DEFAULT_PORT_NUMBER; 366 367 private Mutex socketMutex; 368 private Socket socket = null; 369 370 this() 371 { 372 version (haveUnixSockets) 373 { 374 this((() @trusted => generateSocketName())()); 375 } 376 else 377 { 378 this(DEFAULT_PORT_NUMBER); 379 } 380 } 381 382 this(string socketFile) 383 { 384 socketMutex = new Mutex(); 385 useTCP = false; 386 this._socketFile = _socketFile; 387 } 388 389 this(ushort port) 390 { 391 socketMutex = new Mutex(); 392 useTCP = true; 393 this.port = port; 394 } 395 396 string socketFile() const @property 397 { 398 return _socketFile; 399 } 400 401 void socketFile(string value) @property 402 { 403 version (haveUnixSockets) 404 { 405 if (value.length > 0) 406 useTCP = false; 407 } 408 _socketFile = value; 409 } 410 411 ushort runningPort() const @property 412 { 413 return port; 414 } 415 416 void runningPort(ushort value) @property 417 { 418 if (value != 0) 419 useTCP = true; 420 port = value; 421 } 422 423 bool usingUnixDomainSockets() const @property 424 { 425 version (haveUnixSockets) 426 return true; 427 else 428 return false; 429 } 430 431 bool queryRunning() @trusted 432 { 433 return serverIsRunning(useTCP, socketFile, port); 434 } 435 436 Socket connectForRequest() 437 { 438 socketMutex.lock(); 439 scope (failure) 440 { 441 socket = null; 442 socketMutex.unlock(); 443 } 444 445 assert(socket is null, "Didn't call closeRequestConnection but attempted to connect again"); 446 447 if (useTCP) 448 { 449 socket = new TcpSocket(AddressFamily.INET); 450 socket.connect(new InternetAddress("127.0.0.1", port)); 451 } 452 else 453 { 454 version (haveUnixSockets) 455 { 456 socket = new Socket(AddressFamily.UNIX, SocketType.STREAM); 457 socket.connect(new UnixAddress(socketFile)); 458 } 459 else 460 { 461 // should never be called with non-null socketFile on Windows 462 assert(false); 463 } 464 } 465 466 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"( 467 5)); 468 socket.blocking = true; 469 return socket; 470 } 471 472 void closeRequestConnection() 473 { 474 scope (exit) 475 { 476 socket = null; 477 socketMutex.unlock(); 478 } 479 480 socket.shutdown(SocketShutdown.BOTH); 481 socket.close(); 482 } 483 484 bool performNotification(AutocompleteRequest request) @trusted 485 { 486 auto sock = connectForRequest(); 487 scope (exit) 488 closeRequestConnection(); 489 490 return sendRequest(sock, request); 491 } 492 493 DCDResponse performRequest(AutocompleteRequest request) @trusted 494 { 495 auto sock = connectForRequest(); 496 scope (exit) 497 closeRequestConnection(); 498 499 if (!sendRequest(sock, request)) 500 throw new Exception("Failed to send request"); 501 502 try 503 { 504 return getResponse(sock); 505 } 506 catch (Exception e) 507 { 508 return DCDResponse.init; 509 } 510 } 511 512 bool shutdown() 513 { 514 AutocompleteRequest request; 515 request.kind = RequestKind.shutdown; 516 return performNotification(request); 517 } 518 519 bool clearCache() 520 { 521 AutocompleteRequest request; 522 request.kind = RequestKind.clearCache; 523 return performNotification(request); 524 } 525 526 bool addImportPaths(string[] importPaths) 527 { 528 AutocompleteRequest request; 529 request.kind = RequestKind.addImport; 530 request.importPaths = importPaths; 531 return performNotification(request); 532 } 533 534 bool removeImportPaths(string[] importPaths) 535 { 536 AutocompleteRequest request; 537 request.kind = RequestKind.removeImport; 538 request.importPaths = importPaths; 539 return performNotification(request); 540 } 541 542 string[] listImportPaths() 543 { 544 AutocompleteRequest request; 545 request.kind = RequestKind.listImports; 546 return performRequest(request).importPaths; 547 } 548 549 SymbolInformation requestSymbolInfo(CodeRequest loc) 550 { 551 AutocompleteRequest request; 552 request.kind = RequestKind.symbolLocation; 553 loc.apply(request); 554 return SymbolInformation(performRequest(request)); 555 } 556 557 string[] requestDocumentation(CodeRequest loc) 558 { 559 AutocompleteRequest request; 560 request.kind = RequestKind.doc; 561 loc.apply(request); 562 return performRequest(request).completions.map!"a.documentation".array; 563 } 564 565 DCDResponse.Completion[] requestSymbolSearch(string query) 566 { 567 AutocompleteRequest request; 568 request.kind = RequestKind.search; 569 request.searchName = query; 570 return performRequest(request).completions; 571 } 572 573 LocalUse requestLocalUse(CodeRequest loc) 574 { 575 AutocompleteRequest request; 576 request.kind = RequestKind.localUse; 577 loc.apply(request); 578 return LocalUse(performRequest(request)); 579 } 580 581 Completion requestAutocomplete(CodeRequest loc) 582 { 583 AutocompleteRequest request; 584 request.kind = RequestKind.autocomplete; 585 loc.apply(request); 586 return Completion(performRequest(request)); 587 } 588 } 589 590 struct CodeRequest 591 { 592 string fileName; 593 const(char)[] sourceCode; 594 size_t cursorPosition = size_t.max; 595 596 // private because sourceCode is const but in AutocompleteRequest it's not 597 private void apply(ref AutocompleteRequest request) 598 { 599 request.fileName = fileName; 600 // @trusted because the apply function is only used in places where we 601 // know that the request is not used outside the CodeRequest scope. 602 request.sourceCode = (() @trusted => cast(ubyte[]) sourceCode)(); 603 request.cursorPosition = cursorPosition; 604 } 605 } 606 607 struct SymbolInformation 608 { 609 string declarationFilePath; 610 size_t declarationLocation; 611 612 this(DCDResponse res) 613 { 614 declarationFilePath = res.symbolFilePath; 615 declarationLocation = res.symbolLocation; 616 } 617 } 618 619 struct Completion 620 { 621 CompletionType type; 622 DCDResponse.Completion[] completions; 623 624 this(DCDResponse res) 625 { 626 type = cast(CompletionType) res.completionType; 627 completions = res.completions; 628 } 629 } 630 631 struct LocalUse 632 { 633 string declarationFilePath; 634 size_t declarationLocation; 635 size_t[] uses; 636 637 this(DCDResponse res) 638 { 639 declarationFilePath = res.symbolFilePath; 640 declarationLocation = res.symbolLocation; 641 uses = res.completions.map!"a.symbolLocation".array; 642 } 643 } 644 645 private string unescapeTabs(string val) 646 { 647 if (!val.length) 648 return val; 649 650 auto ret = appender!string; 651 size_t i = 0; 652 while (i < val.length) 653 { 654 size_t index = val.indexOf('\\', i); 655 if (index == -1 || cast(int) index == cast(int) val.length - 1) 656 { 657 if (!ret.data.length) 658 { 659 return val; 660 } 661 else 662 { 663 ret.put(val[i .. $]); 664 break; 665 } 666 } 667 else 668 { 669 char c = val[index + 1]; 670 switch (c) 671 { 672 case 'n': 673 c = '\n'; 674 break; 675 case 't': 676 c = '\t'; 677 break; 678 default: 679 break; 680 } 681 ret.put(val[i .. index]); 682 ret.put(c); 683 i = index + 2; 684 } 685 } 686 return ret.data; 687 } 688 689 unittest 690 { 691 shouldEqual("hello world", "hello world".unescapeTabs); 692 shouldEqual("hello\nworld", "hello\\nworld".unescapeTabs); 693 shouldEqual("hello\\nworld", "hello\\\\nworld".unescapeTabs); 694 shouldEqual("hello\\\nworld", "hello\\\\\\nworld".unescapeTabs); 695 }