1 module served.lsp.jsonrpc; 2 3 // version = TracePackets; 4 5 import core.exception; 6 import core.thread; 7 8 import std.container : DList; 9 import std.conv; 10 import std.experimental.logger; 11 import std.json; 12 import std.stdio; 13 import std.string; 14 15 import served.lsp.filereader; 16 import served.lsp.protocol; 17 18 /// Fiber which runs in the background, reading from a FileReader, and calling methods when requested over the RPC interface. 19 class RPCProcessor : Fiber 20 { 21 /// Constructs this RPC processor using a FileReader to read RPC commands from and a std.stdio.File to write RPC commands to. 22 /// Creates this fiber with a reasonable fiber size. 23 this(FileReader reader, File writer) 24 { 25 super(&run, 4096 * 32); 26 this.reader = reader; 27 this.writer = writer; 28 29 // LDC requires we don't use any TLS variables in the RPC processor on OSX 30 // this assumption holds true for most of serve-d, but should be verified 31 // every once in a while using `-vtls` to print all TLS variables. 32 static if (__traits(hasMember, Fiber, "allowMigration")) 33 allowMigration(); 34 } 35 36 /// Instructs the RPC processor to stop at the next IO read instruction. 37 void stop() 38 { 39 stopped = true; 40 } 41 42 /// Sends an RPC response or error. 43 /// If `result` or `error` is not given on the response message, they won't be sent. 44 /// Otherwise on success `result` must be set or on error `error` must be set. 45 /// This also logs the error to stderr if it is given. 46 /// Params: 47 /// res = the response message to send. 48 void send(ResponseMessage res) 49 { 50 const(char)[][7] buf; 51 int len; 52 buf[len++] = `{"jsonrpc":"2.0","id":`; 53 buf[len++] = res.id.serializeJson; 54 55 if (!res.result.isNone) 56 { 57 buf[len++] = `,"result":`; 58 buf[len++] = res.result.serializeJson; 59 } 60 61 if (!res.error.isNone) 62 { 63 buf[len++] = `,"error":`; 64 buf[len++] = res.error.serializeJson; 65 stderr.writeln(buf[len - 1]); 66 } 67 68 buf[len++] = `}`; 69 sendRawPacket(buf[0 .. len]); 70 } 71 /// ditto 72 void send(ResponseMessageRaw res) 73 { 74 const(char)[][7] buf; 75 int len; 76 buf[len++] = `{"jsonrpc":"2.0","id":`; 77 buf[len++] = res.id.serializeJson; 78 79 if (res.resultJson.length) 80 { 81 buf[len++] = `,"result":`; 82 buf[len++] = res.resultJson; 83 } 84 85 if (!res.error.isNone) 86 { 87 buf[len++] = `,"error":`; 88 buf[len++] = res.error.serializeJson; 89 stderr.writeln(buf[len - 1]); 90 } 91 92 buf[len++] = `}`; 93 sendRawPacket(buf[0 .. len]); 94 } 95 96 /// Sends an RPC request (method call) to the other side. Doesn't do any additional processing. 97 /// Params: 98 /// req = The request to send 99 void send(RequestMessage req) 100 { 101 sendRawPacket(req.serializeJson); 102 } 103 104 /// ditto 105 void send(RequestMessageRaw req) 106 { 107 int i; 108 const(char)[][7] buffer; 109 buffer[i++] = `{"jsonrpc":"2.0","method":`; 110 buffer[i++] = req.method.serializeJson; 111 if (!req.id.isNone) 112 { 113 buffer[i++] = `,"id":`; 114 buffer[i++] = req.id.serializeJson; 115 } 116 if (req.paramsJson.length) 117 { 118 buffer[i++] = `,"params":`; 119 buffer[i++] = req.paramsJson; 120 } 121 buffer[i++] = `}`; 122 sendRawPacket(buffer[0 .. i]); 123 } 124 125 /// Sends a raw JSON object to the other RPC side. 126 deprecated void send(JSONValue raw) 127 { 128 if (!("jsonrpc" in raw)) 129 { 130 error(raw); 131 throw new Exception("Sent objects must have a jsonrpc"); 132 } 133 const content = raw.toString(JSONOptions.doNotEscapeSlashes); 134 sendRawPacket(content); 135 } 136 137 /// ditto 138 void sendRawPacket(scope const(char)[] rawJson) 139 { 140 sendRawPacket((&rawJson)[0 .. 1]); 141 } 142 143 /// ditto 144 void sendRawPacket(scope const(char)[][] parts) 145 { 146 // Consider turning on logging on client side instead! 147 // (vscode setting: "serve-d.trace.server": "verbose") 148 version (TracePackets) 149 { 150 import std.algorithm; 151 152 trace(">> ", parts.join); 153 } 154 155 size_t len = 0; 156 foreach (part; parts) 157 len += part.length; 158 159 { 160 scope w = writer.lockingBinaryWriter; 161 w.put("Content-Length: "); 162 w.put(len.to!string); 163 w.put("\r\n\r\n"); 164 foreach (part; parts) 165 w.put(part); 166 } 167 writer.flush(); 168 } 169 170 /// Sends a notification with the given `method` name to the other RPC side without any parameters. 171 void notifyMethod(string method) 172 { 173 RequestMessageRaw req; 174 req.method = method; 175 send(req); 176 } 177 178 /// Sends a notification with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 179 void notifyMethod(T)(string method, T value) 180 { 181 notifyMethodRaw(method, value.serializeJson); 182 } 183 184 /// ditto 185 deprecated void notifyMethod(string method, JSONValue value) 186 { 187 notifyMethodRaw(method, value.toString); 188 } 189 190 /// ditto 191 void notifyMethod(string method, JsonValue value) 192 { 193 RequestMessage req; 194 req.method = method; 195 req.params = value; 196 send(req); 197 } 198 199 /// ditto 200 void notifyMethodRaw(string method, scope const(char)[] value) 201 { 202 const(char)[][5] parts = [ 203 `{"jsonrpc":"2.0","method":`, 204 method.serializeJson, 205 `,"params":`, 206 value, 207 `}` 208 ]; 209 sendRawPacket(parts[]); 210 } 211 212 void notifyProgressRaw(scope const(char)[] token, scope const(char)[] value) 213 { 214 const(char)[][5] parts = [ 215 `{"jsonrpc":"2.0","method":"$/progress","params":{"token":`, 216 token, 217 `,"value":`, 218 value, 219 `}}` 220 ]; 221 sendRawPacket(parts[]); 222 } 223 224 void registerCapability(T)(scope const(char)[] id, scope const(char)[] method, T options) 225 { 226 const(char)[][7] parts = [ 227 `{"jsonrpc":"2.0","method":"client/registerCapability","registrations":[{"id":"`, 228 id.escapeJsonStringContent, 229 `","method":"`, 230 method.escapeJsonStringContent, 231 `","registerOptions":`, 232 options.serializeJson, 233 `]}` 234 ]; 235 sendRawPacket(parts[]); 236 } 237 238 /// Sends a request with the given `method` name to the other RPC side without any parameters. 239 /// Doesn't handle the response by the other RPC side. 240 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 241 RequestToken sendMethod(string method) 242 { 243 auto id = RequestToken.next(); 244 const(char)[][5] parts = [ 245 `{"jsonrpc":"2.0","id":`, 246 id.serializeJson, 247 `,"method":`, 248 method.serializeJson, 249 `}` 250 ]; 251 sendRawPacket(parts[]); 252 return id; 253 } 254 255 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 256 /// Doesn't handle the response by the other RPC side. 257 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 258 RequestToken sendMethod(T)(string method, T value) 259 { 260 return sendMethodRaw(method, value.serializeJson); 261 } 262 263 /// ditto 264 deprecated RequestToken sendMethod(string method, JSONValue value) 265 { 266 return sendMethod(method, value.toJsonValue); 267 } 268 269 /// ditto 270 RequestToken sendMethod(string method, JsonValue value) 271 { 272 return sendMethodRaw(method, value.serializeJson); 273 } 274 275 /// ditto 276 RequestToken sendMethodRaw(string method, scope const(char)[] value) 277 { 278 auto id = RequestToken.next(); 279 const(char)[][7] parts = [ 280 `{"jsonrpc":"2.0","id":`, 281 id.serializeJson, 282 `,"method":`, 283 method.serializeJson, 284 `,"params":`, 285 value, 286 `}` 287 ]; 288 sendRawPacket(parts[]); 289 return id; 290 } 291 292 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 293 /// Awaits the response (using yield) and returns once it's there. 294 /// 295 /// This is a small wrapper to call `awaitResponse(sendMethod(method, value))` 296 /// 297 /// Returns: The response deserialized from the RPC. 298 ResponseMessageRaw sendRequest(T)(string method, T value, Duration timeout = Duration.max) 299 { 300 return awaitResponse(sendMethod(method, value), timeout); 301 } 302 303 /// ditto 304 deprecated ResponseMessageRaw sendRequest(string method, JSONValue value, Duration timeout = Duration.max) 305 { 306 return awaitResponse(sendMethod(method, value), timeout); 307 } 308 309 /// ditto 310 ResponseMessageRaw sendRequest(string method, JsonValue value, Duration timeout = Duration.max) 311 { 312 return awaitResponse(sendMethod(method, value), timeout); 313 } 314 315 /// Calls the `window/logMessage` method with all arguments concatenated together using text() 316 /// Params: 317 /// type = the $(REF MessageType, served,lsp,protocol) to use as $(REF LogMessageParams, served,lsp,protocol) type 318 /// args = the message parts to send 319 void log(MessageType type = MessageType.log, Args...)(Args args) 320 { 321 scope const(char)[][3] parts = [ 322 `{"jsonrpc":"2.0","method":"window/logMessage","params":`, 323 LogMessageParams(type, text(args)).serializeJson, 324 `}` 325 ]; 326 sendRawPacket(parts[]); 327 } 328 329 /// Returns: `true` if there has been any messages been sent to us from the other RPC side, otherwise `false`. 330 bool hasData() const @property 331 { 332 return !messageQueue.empty; 333 } 334 335 /// Returns: the first message from the message queue. Removes it from the message queue so it will no longer be processed. 336 /// Throws: Exception if $(LREF hasData) is false. 337 RequestMessageRaw poll() 338 { 339 if (!hasData) 340 throw new Exception("No Data"); 341 auto ret = messageQueue.front; 342 messageQueue.removeFront(); 343 return ret; 344 } 345 346 /// Convenience wrapper around $(LREF WindowFunctions) for `this`. 347 WindowFunctions window() 348 { 349 return WindowFunctions(this); 350 } 351 352 /// Registers a wait handler for the given request token. When the other RPC 353 /// side sends a response to this token, the value will be saved, before it 354 /// is being awaited. 355 private size_t prepareWait(RequestToken tok) 356 { 357 size_t i; 358 bool found = false; 359 foreach (n, t; responseTokens) 360 { 361 if (t.handled) 362 { 363 // replace handled responses (overwrite reusable memory) 364 i = n; 365 found = true; 366 break; 367 } 368 } 369 if (!found) 370 i = responseTokens.length++; 371 responseTokens[i] = RequestWait(tok); 372 return i; 373 } 374 375 /// Waits until the given responseToken wait handler is resolved, then 376 /// return its result and makes the memory reusable. 377 private ResponseMessageRaw resolveWait(size_t i, Duration timeout = Duration.max) 378 { 379 import std.datetime.stopwatch; 380 381 StopWatch sw; 382 sw.start(); 383 while (!responseTokens[i].got) 384 { 385 if (timeout != Duration.max 386 && sw.peek > timeout) 387 throw new Exception("RPC response wait timed out"); 388 yield(); // yield until main loop placed a response 389 } 390 auto res = responseTokens[i].ret; 391 responseTokens[i].handled = true; // make memory reusable 392 return res; 393 } 394 395 private bool hasResponse(size_t handle) 396 { 397 return responseTokens[handle].got; 398 } 399 400 /** 401 Waits for a response message to a request from the other RPC side. 402 403 If this is called after the response has already been sent and processed 404 by yielding after sending the request, this will yield forever and use 405 up memory. 406 407 So it is important, if you are going to await a response, to do it 408 immediately when sending any request. 409 */ 410 ResponseMessageRaw awaitResponse(RequestToken tok, Duration timeout = Duration.max) 411 { 412 auto i = prepareWait(tok); 413 return resolveWait(i, timeout); 414 } 415 416 private: 417 void onData(RequestMessageRaw req) 418 { 419 version (TracePackets) 420 { 421 import std.algorithm; 422 423 trace("<< ", req.id, ": ", req.method, ": ", req.paramsJson); 424 } 425 426 messageQueue.insertBack(req); 427 } 428 429 FileReader reader; 430 File writer; 431 bool stopped; 432 DList!RequestMessageRaw messageQueue; 433 434 struct RequestWait 435 { 436 RequestToken token; 437 bool got = false; 438 bool handled = false; 439 ResponseMessageRaw ret; 440 } 441 442 RequestWait[] responseTokens; 443 444 void run() 445 { 446 assert(reader.isReading, "must start jsonrpc after file reader!"); 447 while (!stopped && reader.isReading) 448 { 449 bool gotAnyHeader; 450 bool inHeader = true; 451 size_t contentLength = 0; 452 do // dmd -O has an issue on mscoff where it forgets to emit a cmp here so this would break with while (inHeader) 453 { 454 string line = reader.yieldLine(&stopped, false); 455 if (!reader.isReading) 456 stop(); // abort in header 457 458 if (line.length) 459 gotAnyHeader = true; 460 461 if (!line.length && gotAnyHeader) 462 inHeader = false; 463 else if (line.startsWith("Content-Length:")) 464 contentLength = line["Content-Length:".length .. $].strip.to!size_t; 465 } 466 while (inHeader && !stopped); 467 468 if (stopped) 469 break; 470 471 if (contentLength <= 0) 472 { 473 send(ResponseMessage(RequestToken.init, ResponseError(ErrorCode.invalidRequest, "Invalid/no content length specified"))); 474 continue; 475 } 476 477 auto content = cast(const(char)[]) reader.yieldData(contentLength, &stopped, false); 478 if (stopped || content is null) 479 break; 480 assert(content.length == contentLength); 481 RequestMessageRaw request; 482 RequestMessageRaw[] extraRequests; 483 try 484 { 485 if (content.length && content[0] == '[') 486 { 487 int count; 488 content.visitJsonArray!((item) { 489 count++; 490 491 auto res = handleRequestImpl(item); 492 if (request == RequestMessageRaw.init) 493 request = res; 494 else if (res != RequestMessageRaw.init) 495 extraRequests ~= res; 496 }); 497 if (count == 0) 498 send(ResponseMessage(null, ResponseError(ErrorCode.invalidRequest, "Empty batch request"))); 499 } 500 else if (content.length && content[0] == '{') 501 { 502 request = handleRequestImpl(content); 503 } 504 else 505 { 506 send(ResponseMessage(null, ResponseError(ErrorCode.invalidRequest, "Invalid request type (must be object or array)"))); 507 } 508 } 509 catch (Exception e) 510 { 511 try 512 { 513 trace(e); 514 trace(content); 515 auto idx = content.indexOf("\"id\":"); 516 auto endIdx = content.indexOf(",", idx); 517 RequestToken fallback; 518 if (!content.startsWith("[") && idx != -1 && endIdx != -1) 519 fallback = deserializeJson!RequestToken(content[idx .. endIdx].strip); 520 send(ResponseMessage(fallback, ResponseError(ErrorCode.parseError, 521 "Parse error: " ~ e.msg))); 522 } 523 catch (Exception e) 524 { 525 errorf("Got invalid request '%s'!", content); 526 trace(e); 527 } 528 } 529 530 if (request != RequestMessageRaw.init) 531 { 532 onData(request); 533 Fiber.yield(); 534 } 535 536 foreach (req; extraRequests) 537 { 538 onData(request); 539 Fiber.yield(); 540 } 541 } 542 } 543 544 RequestMessageRaw handleRequestImpl(scope const(char)[] json) 545 { 546 if (!json.looksLikeJsonObject) 547 throw new Exception("malformed request JSON, must be object"); 548 auto slices = json.parseKeySlices!("id", "result", "error", "method", "params"); 549 550 auto id = slices.id; 551 if (slices.result.length && slices.method.length 552 || !slices.result.length && !slices.method.length && !slices.error.length) 553 { 554 ResponseMessage res; 555 if (id.length) 556 res.id = id.deserializeJson!RequestToken; 557 res.error = ResponseError(ErrorCode.invalidRequest, "missing required members or has ambiguous members"); 558 send(res); 559 return RequestMessageRaw.init; 560 } 561 562 bool isResponse = false; 563 if (id.length) 564 { 565 auto tok = id.deserializeJson!RequestToken; 566 foreach (ref waiting; responseTokens) 567 { 568 if (!waiting.got && waiting.token == tok) 569 { 570 waiting.got = true; 571 waiting.ret.id = tok; 572 auto res = slices.result; 573 auto err = slices.error; 574 if (res.length) 575 waiting.ret.resultJson = res.idup; 576 if (err.length) 577 waiting.ret.error = err.deserializeJson!ResponseError; 578 isResponse = true; 579 break; 580 } 581 } 582 583 if (!isResponse && slices.result.length) 584 { 585 send(ResponseMessage(tok, 586 ResponseError(ErrorCode.invalidRequest, "unknown request response ID"))); 587 return RequestMessageRaw.init; 588 } 589 } 590 if (!isResponse) 591 { 592 RequestMessageRaw request; 593 if (slices.id.length) 594 request.id = slices.id.deserializeJson!RequestToken; 595 if (slices.method.length) 596 request.method = slices.method.deserializeJson!string; 597 if (slices.params.length) 598 request.paramsJson = slices.params.idup; 599 600 if (request.paramsJson.length 601 && request.paramsJson.ptr[0] != '[' 602 && request.paramsJson.ptr[0] != '{') 603 { 604 auto err = ResponseError(ErrorCode.invalidParams, 605 "`params` MUST be an object (named arguments) or array " 606 ~ "(positional arguments), other types are not allowed by spec" 607 ); 608 if (request.id.isNone) 609 send(ResponseMessage(null, err)); 610 else 611 send(ResponseMessage(RequestToken(request.id.deref), err)); 612 } 613 else 614 { 615 return request; 616 } 617 } 618 return RequestMessageRaw.init; 619 } 620 } 621 622 /// Helper struct to simulate RPC connections with an RPCProcessor. 623 /// Intended for use with tests, not for real-world use. 624 struct MockRPC 625 { 626 import std.process; 627 628 enum shortDelay = 10.msecs; 629 630 Pipe rpcPipe; 631 Pipe resultPipe; 632 633 void writePacket(string s, string epilogue = "", string prologue = "") 634 { 635 with (rpcPipe.writeEnd.lockingBinaryWriter) 636 { 637 put(prologue); 638 put("Content-Length: "); 639 put(s.length.to!string); 640 put("\r\n\r\n"); 641 put(s); 642 put(epilogue); 643 } 644 rpcPipe.writeEnd.flush(); 645 } 646 647 string readPacket() 648 { 649 auto lenStr = resultPipe.readEnd.readln(); 650 assert(lenStr.startsWith("Content-Length: ")); 651 auto len = lenStr["Content-Length: ".length .. $].strip.to!int; 652 resultPipe.readEnd.readln(); 653 ubyte[] buf = new ubyte[len]; 654 size_t i; 655 while (!resultPipe.readEnd.eof && i < buf.length) 656 i += resultPipe.readEnd.rawRead(buf[i .. $]).length; 657 assert(i == buf.length); 658 return cast(string)buf; 659 } 660 661 void expectPacket(string s) 662 { 663 auto res = readPacket(); 664 assert(res == s, res); 665 } 666 667 void testRPC(void delegate(RPCProcessor rpc) cb) 668 { 669 rpcPipe = pipe(); 670 resultPipe = pipe(); 671 672 auto rpcInput = newFileReader(rpcPipe.readEnd); 673 rpcInput.start(); 674 scope (exit) 675 { 676 rpcInput.stop(); 677 Thread.sleep(shortDelay); 678 } 679 680 auto rpc = new RPCProcessor(rpcInput, resultPipe.writeEnd); 681 rpc.call(); 682 683 cb(rpc); 684 685 rpc.stop(); 686 if (rpc.state != Fiber.State.TERM) 687 rpc.call(); 688 assert(rpc.state == Fiber.State.TERM); 689 } 690 } 691 692 unittest 693 { 694 import served.lsp.protocol; 695 696 MockRPC mockRPC; 697 698 void writePacket(string s, string epilogue = "", string prologue = "") 699 { 700 mockRPC.writePacket(s, epilogue, prologue); 701 } 702 703 string readPacket() 704 { 705 return mockRPC.readPacket(); 706 } 707 708 void expectPacket(string s) 709 { 710 mockRPC.expectPacket(s); 711 } 712 713 void testRPC(void delegate(RPCProcessor rpc) cb) 714 { 715 mockRPC.testRPC(cb); 716 } 717 718 testRPC((rpc) { /* test immediate close */ }); 719 720 foreach (i; 0 .. 2) 721 testRPC((rpc) { 722 InitializeParams initializeParams = { 723 processId: 1234, 724 rootUri: "file:///root/uri", 725 capabilities: ClientCapabilities.init 726 }; 727 auto tok = rpc.sendMethod("initialize", initializeParams); 728 auto waitHandle = rpc.prepareWait(tok); 729 expectPacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"method":"initialize","params":{"processId":1234,"rootUri":"file:///root/uri","capabilities":{}}}`); 730 writePacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"result":{"capabilities":{}}}`); 731 rpc.call(); 732 Thread.sleep(MockRPC.shortDelay); 733 rpc.call(); 734 assert(rpc.hasResponse(waitHandle)); 735 auto res = rpc.resolveWait(waitHandle); 736 assert(res.resultJson == `{"capabilities":{}}`); 737 }); 738 739 // test close after unfinished message (e.g. client crashed) 740 testRPC((rpc) { 741 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put("Content-Length: 5\r\n\r\n"); 742 mockRPC.rpcPipe.writeEnd.flush(); 743 rpc.call(); 744 Thread.sleep(MockRPC.shortDelay); 745 rpc.call(); 746 }); 747 testRPC((rpc) { 748 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put("Content-Length: 5\r\n\r\ntrue"); 749 mockRPC.rpcPipe.writeEnd.flush(); 750 rpc.call(); 751 Thread.sleep(MockRPC.shortDelay); 752 rpc.call(); 753 }); 754 755 // test unlucky buffering 756 testRPC((rpc) { 757 void slowIO() 758 { 759 rpc.call(); 760 Thread.sleep(MockRPC.shortDelay); 761 rpc.call(); 762 } 763 764 InitializeParams initializeParams = { 765 processId: 1234, 766 rootUri: "file:///root/uri", 767 capabilities: ClientCapabilities.init 768 }; 769 auto tok = rpc.sendMethod("initialize", initializeParams); 770 auto waitHandle = rpc.prepareWait(tok); 771 expectPacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"method":"initialize","params":{"processId":1234,"rootUri":"file:///root/uri","capabilities":{}}}`); 772 auto s = `{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"result":{"capabilities":{}}}`; 773 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put("Content-Length: "); 774 mockRPC.rpcPipe.writeEnd.flush(); 775 slowIO(); 776 assert(!rpc.hasResponse(waitHandle)); 777 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put(s.length.to!string); 778 mockRPC.rpcPipe.writeEnd.flush(); 779 slowIO(); 780 assert(!rpc.hasResponse(waitHandle)); 781 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put("\r\n\r\n"); 782 mockRPC.rpcPipe.writeEnd.flush(); 783 slowIO(); 784 assert(!rpc.hasResponse(waitHandle)); 785 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put(s[0 .. $ / 2]); 786 mockRPC.rpcPipe.writeEnd.flush(); 787 slowIO(); 788 assert(!rpc.hasResponse(waitHandle)); 789 mockRPC.rpcPipe.writeEnd.lockingBinaryWriter.put(s[$ / 2 .. $]); 790 mockRPC.rpcPipe.writeEnd.flush(); 791 slowIO(); 792 assert(rpc.hasResponse(waitHandle)); 793 auto res = rpc.resolveWait(waitHandle); 794 assert(res.resultJson == `{"capabilities":{}}`); 795 }); 796 797 // test spec-resiliance with whitespace before/after message 798 foreach (pair; [["", "\r\n"], ["\r\n", ""], ["\r\n\r\n", "\r\n\r\n"]]) 799 testRPC((rpc) { 800 InitializeParams initializeParams = { 801 processId: 1234, 802 rootUri: "file:///root/uri", 803 capabilities: ClientCapabilities.init 804 }; 805 auto tok = rpc.sendMethod("initialize", initializeParams); 806 auto waitHandle = rpc.prepareWait(tok); 807 expectPacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"method":"initialize","params":{"processId":1234,"rootUri":"file:///root/uri","capabilities":{}}}`); 808 writePacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"result":{"capabilities":{}}}`, pair[1], pair[0]); 809 rpc.call(); 810 Thread.sleep(MockRPC.shortDelay); 811 rpc.call(); 812 assert(rpc.hasResponse(waitHandle)); 813 auto res = rpc.resolveWait(waitHandle); 814 assert(res.resultJson == `{"capabilities":{}}`); 815 816 writePacket(`{"jsonrpc":"2.0","id":"sendtest","method":"test","params":{"x":{"a":4}}}`, pair[1], pair[0]); 817 rpc.call(); 818 Thread.sleep(MockRPC.shortDelay); 819 rpc.call(); 820 assert(rpc.hasData); 821 auto msg = rpc.poll; 822 assert(!msg.id.isNone); 823 assert(msg.id.deref == "sendtest"); 824 assert(msg.method == "test"); 825 assert(msg.paramsJson == `{"x":{"a":4}}`); 826 }); 827 828 // test error handling 829 testRPC((rpc) { 830 InitializeParams initializeParams = { 831 processId: 1234, 832 rootUri: "file:///root/uri", 833 capabilities: ClientCapabilities.init 834 }; 835 auto tok = rpc.sendMethod("initialize", initializeParams); 836 auto waitHandle = rpc.prepareWait(tok); 837 expectPacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"method":"initialize","params":{"processId":1234,"rootUri":"file:///root/uri","capabilities":{}}}`); 838 writePacket(`{"jsonrpc":"2.0","id":` ~ tok.value.toString ~ `,"result":{"capabilities":{}}}`); 839 rpc.call(); 840 Thread.sleep(MockRPC.shortDelay); 841 rpc.call(); 842 assert(rpc.hasResponse(waitHandle)); 843 auto res = rpc.resolveWait(waitHandle); 844 assert(res.resultJson == `{"capabilities":{}}`); 845 846 void errorTest(string send, string recv) 847 { 848 writePacket(send); 849 rpc.call(); 850 Thread.sleep(MockRPC.shortDelay); 851 rpc.call(); 852 expectPacket(recv); 853 } 854 855 errorTest(`{"jsonrpc":"2.0","id":"invalid-token","result":{"capabilities":{}}}`, 856 `{"jsonrpc":"2.0","id":"invalid-token","error":{"code":-32600,"message":"unknown request response ID"}}`); 857 858 // twice to see that the same error gets handled 859 errorTest(`{"jsonrpc":"2.0","id":"invalid-token","result":{"capabilities":{}}}`, 860 `{"jsonrpc":"2.0","id":"invalid-token","error":{"code":-32600,"message":"unknown request response ID"}}`); 861 862 errorTest(`{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]`, 863 `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error: malformed request JSON, must be object"}}`); 864 865 errorTest(`[]`, 866 `{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Empty batch request"}}`); 867 868 errorTest(`[{}]`, 869 `{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"missing required members or has ambiguous members"}}`); 870 871 writePacket(`[{},{},{}]`); 872 rpc.call(); 873 Thread.sleep(MockRPC.shortDelay); 874 rpc.call(); 875 expectPacket(`{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"missing required members or has ambiguous members"}}`); 876 expectPacket(`{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"missing required members or has ambiguous members"}}`); 877 expectPacket(`{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"missing required members or has ambiguous members"}}`); 878 }); 879 } 880 881 /// Utility functions for common LSP methods performing UI things. 882 struct WindowFunctions 883 { 884 /// The RPC processor to use for sending/receiving 885 RPCProcessor rpc; 886 private bool safeShowMessage; 887 888 /// Runs window/showMessage which typically shows a notification box, without any buttons or feedback. 889 /// Logs the message to stderr too. 890 void showMessage(MessageType type, string message) 891 { 892 if (!safeShowMessage) 893 warningf("%s message: %s", type, message); 894 rpc.notifyMethod("window/showMessage", ShowMessageParams(type, message)); 895 safeShowMessage = false; 896 } 897 898 /// Runs window/showMessageRequest which typically shows a message box with possible action buttons to click. Returns the action which got clicked or one with null title if it has been dismissed. 899 MessageActionItem requestMessage(MessageType type, string message, MessageActionItem[] actions) 900 { 901 auto res = rpc.sendRequest("window/showMessageRequest", 902 ShowMessageRequestParams(type, message, actions.opt)); 903 if (!res.resultJson.length || res.resultJson == `null`) 904 return MessageActionItem(null); 905 return res.resultJson.deserializeJson!MessageActionItem; 906 } 907 908 /// ditto 909 string requestMessage(MessageType type, string message, string[] actions) 910 { 911 MessageActionItem[] a = new MessageActionItem[actions.length]; 912 foreach (i, action; actions) 913 a[i] = MessageActionItem(action); 914 return requestMessage(type, message, a).title; 915 } 916 917 /// Runs a function and shows a UI message on failure and logs the error. 918 /// Returns: true if fn was successfully run or false if an exception occured. 919 bool runOrMessage(lazy void fn, MessageType type, string message, 920 string file = __FILE__, size_t line = __LINE__) 921 { 922 try 923 { 924 fn(); 925 return true; 926 } 927 catch (Exception e) 928 { 929 errorf("Error running in %s(%s): %s", file, line, e); 930 showMessage(type, message); 931 return false; 932 } 933 } 934 935 /// Calls $(LREF showMessage) with MessageType.error 936 /// Also logs the message to stderr in a more readable way. 937 void showErrorMessage(string message) 938 { 939 error("Error message: ", message); 940 safeShowMessage = true; 941 showMessage(MessageType.error, message); 942 } 943 944 /// Calls $(LREF showMessage) with MessageType.warning 945 /// Also logs the message to stderr in a more readable way. 946 void showWarningMessage(string message) 947 { 948 warning("Warning message: ", message); 949 safeShowMessage = true; 950 showMessage(MessageType.warning, message); 951 } 952 953 /// Calls $(LREF showMessage) with MessageType.info 954 /// Also logs the message to stderr in a more readable way. 955 void showInformationMessage(string message) 956 { 957 info("Info message: ", message); 958 safeShowMessage = true; 959 showMessage(MessageType.info, message); 960 } 961 962 /// Calls $(LREF showMessage) with MessageType.log 963 /// Also logs the message to stderr in a more readable way. 964 void showLogMessage(string message) 965 { 966 trace("Log message: ", message); 967 safeShowMessage = true; 968 showMessage(MessageType.log, message); 969 } 970 }