1 module workspaced.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 return LocalUse.init; // TODO: implement 224 } 225 226 Completion requestAutocomplete(CodeRequest loc) 227 { 228 auto pipes = doClient([ 229 "--extended", 230 "-c", loc.cursorPosition.to!string 231 ]); 232 scope (exit) 233 { 234 pipes.pid.wait(); 235 pipes.destroy(); 236 } 237 pipes.stdin.write(loc.sourceCode); 238 pipes.stdin.close(); 239 auto dataApp = appender!(string[]); 240 while (pipes.stdout.isOpen && !pipes.stdout.eof) 241 { 242 string line = (() @trusted => pipes.stdout.readln())(); 243 if (line.length == 0) 244 continue; 245 dataApp ~= line.chomp; 246 } 247 248 string[] data = dataApp.data; 249 auto symbols = appender!(DCDResponse.Completion[]); 250 Completion c; 251 if (data.length == 0) 252 { 253 c.type = CompletionType.identifiers; 254 return c; 255 } 256 257 c.type = cast(CompletionType)data[0]; 258 if (c.type == CompletionType.identifiers 259 || c.type == CompletionType.calltips) 260 { 261 foreach (line; data[1 .. $]) 262 { 263 string[] splits = line.split('\t'); 264 DCDResponse.Completion symbol; 265 if (splits.length < 5) 266 continue; 267 string location = splits[3]; 268 string file; 269 int index; 270 if (location.length) 271 { 272 auto space = location.lastIndexOf(' '); 273 if (space != -1) 274 { 275 file = location[0 .. space]; 276 if (location[space + 1 .. $].all!isDigit) 277 index = location[space + 1 .. $].to!int; 278 } 279 else 280 file = location; 281 } 282 symbol.identifier = splits[0]; 283 symbol.kind = splits[1] == "" ? char.init : splits[1][0]; 284 symbol.definition = splits[2]; 285 symbol.symbolFilePath = file; 286 symbol.symbolLocation = index; 287 symbol.documentation = splits[4].unescapeTabs; 288 symbols ~= symbol; 289 } 290 } 291 292 c.completions = symbols.data; 293 return c; 294 } 295 296 private: 297 string[] clientArgs() 298 { 299 if (usingUnixDomainSockets) 300 return ["--socketFile", socketFile]; 301 else 302 return ["--port", runningPort.to!string]; 303 } 304 305 auto doClient(string[] args) 306 { 307 return raw([clientPath] ~ clientArgs ~ args); 308 } 309 310 auto raw(string[] args, Redirect redirect = Redirect.all) 311 { 312 return pipeProcess(args, redirect, null, Config.none, null); 313 } 314 315 auto execClient(string[] args) 316 { 317 return rawExec([clientPath] ~ clientArgs ~ args); 318 } 319 320 auto rawExec(string[] args) 321 { 322 return execute(args, null, Config.none, size_t.max, null); 323 } 324 } 325 326 class BuiltinDCDClient : IDCDClient 327 { 328 public static enum minSupportedServerInclusive = [0, 8, 0]; 329 public static enum maxSupportedServerExclusive = [0, 14, 0]; 330 331 public static immutable clientVersion = DCD_VERSION; 332 333 bool useTCP; 334 string _socketFile; 335 ushort port = DEFAULT_PORT_NUMBER; 336 337 private Mutex socketMutex; 338 private Socket socket = null; 339 340 this() 341 { 342 version (haveUnixSockets) 343 { 344 this((() @trusted => generateSocketName())()); 345 } 346 else 347 { 348 this(DEFAULT_PORT_NUMBER); 349 } 350 } 351 352 this(string socketFile) 353 { 354 socketMutex = new Mutex(); 355 useTCP = false; 356 this._socketFile = _socketFile; 357 } 358 359 this(ushort port) 360 { 361 socketMutex = new Mutex(); 362 useTCP = true; 363 this.port = port; 364 } 365 366 string socketFile() const @property 367 { 368 return _socketFile; 369 } 370 371 void socketFile(string value) @property 372 { 373 version (haveUnixSockets) 374 { 375 if (value.length > 0) 376 useTCP = false; 377 } 378 _socketFile = value; 379 } 380 381 ushort runningPort() const @property 382 { 383 return port; 384 } 385 386 void runningPort(ushort value) @property 387 { 388 if (value != 0) 389 useTCP = true; 390 port = value; 391 } 392 393 bool usingUnixDomainSockets() const @property 394 { 395 version (haveUnixSockets) 396 return true; 397 else 398 return false; 399 } 400 401 bool queryRunning() @trusted 402 { 403 return serverIsRunning(useTCP, socketFile, port); 404 } 405 406 Socket connectForRequest() 407 { 408 socketMutex.lock(); 409 scope (failure) 410 { 411 socket = null; 412 socketMutex.unlock(); 413 } 414 415 assert(socket is null, "Didn't call closeRequestConnection but attempted to connect again"); 416 417 if (useTCP) 418 { 419 socket = new TcpSocket(AddressFamily.INET); 420 socket.connect(new InternetAddress("127.0.0.1", port)); 421 } 422 else 423 { 424 version (haveUnixSockets) 425 { 426 socket = new Socket(AddressFamily.UNIX, SocketType.STREAM); 427 socket.connect(new UnixAddress(socketFile)); 428 } 429 else 430 { 431 // should never be called with non-null socketFile on Windows 432 assert(false); 433 } 434 } 435 436 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"( 437 5)); 438 socket.blocking = true; 439 return socket; 440 } 441 442 void closeRequestConnection() 443 { 444 scope (exit) 445 { 446 socket = null; 447 socketMutex.unlock(); 448 } 449 450 socket.shutdown(SocketShutdown.BOTH); 451 socket.close(); 452 } 453 454 bool performNotification(AutocompleteRequest request) @trusted 455 { 456 auto sock = connectForRequest(); 457 scope (exit) 458 closeRequestConnection(); 459 460 return sendRequest(sock, request); 461 } 462 463 DCDResponse performRequest(AutocompleteRequest request) @trusted 464 { 465 auto sock = connectForRequest(); 466 scope (exit) 467 closeRequestConnection(); 468 469 if (!sendRequest(sock, request)) 470 throw new Exception("Failed to send request"); 471 472 try 473 { 474 return getResponse(sock); 475 } 476 catch (Exception e) 477 { 478 return DCDResponse.init; 479 } 480 } 481 482 bool shutdown() 483 { 484 AutocompleteRequest request; 485 request.kind = RequestKind.shutdown; 486 return performNotification(request); 487 } 488 489 bool clearCache() 490 { 491 AutocompleteRequest request; 492 request.kind = RequestKind.clearCache; 493 return performNotification(request); 494 } 495 496 bool addImportPaths(string[] importPaths) 497 { 498 AutocompleteRequest request; 499 request.kind = RequestKind.addImport; 500 request.importPaths = importPaths; 501 return performNotification(request); 502 } 503 504 bool removeImportPaths(string[] importPaths) 505 { 506 AutocompleteRequest request; 507 request.kind = RequestKind.removeImport; 508 request.importPaths = importPaths; 509 return performNotification(request); 510 } 511 512 string[] listImportPaths() 513 { 514 AutocompleteRequest request; 515 request.kind = RequestKind.listImports; 516 return performRequest(request).importPaths; 517 } 518 519 SymbolInformation requestSymbolInfo(CodeRequest loc) 520 { 521 AutocompleteRequest request; 522 request.kind = RequestKind.symbolLocation; 523 loc.apply(request); 524 return SymbolInformation(performRequest(request)); 525 } 526 527 string[] requestDocumentation(CodeRequest loc) 528 { 529 AutocompleteRequest request; 530 request.kind = RequestKind.doc; 531 loc.apply(request); 532 return performRequest(request).completions.map!"a.documentation".array; 533 } 534 535 DCDResponse.Completion[] requestSymbolSearch(string query) 536 { 537 AutocompleteRequest request; 538 request.kind = RequestKind.search; 539 request.searchName = query; 540 return performRequest(request).completions; 541 } 542 543 LocalUse requestLocalUse(CodeRequest loc) 544 { 545 AutocompleteRequest request; 546 request.kind = RequestKind.localUse; 547 loc.apply(request); 548 return LocalUse(performRequest(request)); 549 } 550 551 Completion requestAutocomplete(CodeRequest loc) 552 { 553 AutocompleteRequest request; 554 request.kind = RequestKind.autocomplete; 555 loc.apply(request); 556 return Completion(performRequest(request)); 557 } 558 } 559 560 struct CodeRequest 561 { 562 string fileName; 563 const(char)[] sourceCode; 564 size_t cursorPosition = size_t.max; 565 566 // private because sourceCode is const but in AutocompleteRequest it's not 567 private void apply(ref AutocompleteRequest request) 568 { 569 request.fileName = fileName; 570 // @trusted because the apply function is only used in places where we 571 // know that the request is not used outside the CodeRequest scope. 572 request.sourceCode = (() @trusted => cast(ubyte[]) sourceCode)(); 573 request.cursorPosition = cursorPosition; 574 } 575 } 576 577 struct SymbolInformation 578 { 579 string declarationFilePath; 580 size_t declarationLocation; 581 582 this(DCDResponse res) 583 { 584 declarationFilePath = res.symbolFilePath; 585 declarationLocation = res.symbolLocation; 586 } 587 } 588 589 struct Completion 590 { 591 CompletionType type; 592 DCDResponse.Completion[] completions; 593 594 this(DCDResponse res) 595 { 596 type = cast(CompletionType) res.completionType; 597 completions = res.completions; 598 } 599 } 600 601 struct LocalUse 602 { 603 string declarationFilePath; 604 size_t declarationLocation; 605 size_t[] uses; 606 607 this(DCDResponse res) 608 { 609 declarationFilePath = res.symbolFilePath; 610 declarationLocation = res.symbolLocation; 611 uses = res.completions.map!"a.symbolLocation".array; 612 } 613 } 614 615 private string unescapeTabs(string val) 616 { 617 if (!val.length) 618 return val; 619 620 auto ret = appender!string; 621 size_t i = 0; 622 while (i < val.length) 623 { 624 size_t index = val.indexOf('\\', i); 625 if (index == -1 || cast(int) index == cast(int) val.length - 1) 626 { 627 if (!ret.data.length) 628 { 629 return val; 630 } 631 else 632 { 633 ret.put(val[i .. $]); 634 break; 635 } 636 } 637 else 638 { 639 char c = val[index + 1]; 640 switch (c) 641 { 642 case 'n': 643 c = '\n'; 644 break; 645 case 't': 646 c = '\t'; 647 break; 648 default: 649 break; 650 } 651 ret.put(val[i .. index]); 652 ret.put(c); 653 i = index + 2; 654 } 655 } 656 return ret.data; 657 } 658 659 unittest 660 { 661 shouldEqual("hello world", "hello world".unescapeTabs); 662 shouldEqual("hello\nworld", "hello\\nworld".unescapeTabs); 663 shouldEqual("hello\\nworld", "hello\\\\nworld".unescapeTabs); 664 shouldEqual("hello\\\nworld", "hello\\\\\\nworld".unescapeTabs); 665 }