1 module served.lsp.jsonrpc; 2 3 import core.exception; 4 import core.thread; 5 6 import painlessjson; 7 8 import std.container : DList, SList; 9 import std.conv; 10 import std.experimental.logger; 11 import std.json; 12 import std.stdio; 13 import std.string; 14 import std.typecons; 15 16 import served.lsp.filereader; 17 import served.lsp.protocol; 18 19 import tinyevent; 20 21 alias RequestHandler = ResponseMessage delegate(RequestMessage); 22 alias EventRequestHandler = void delegate(RequestMessage); 23 24 /// Fiber which runs in the background, reading from a FileReader, and calling methods when requested over the RPC interface. 25 class RPCProcessor : Fiber 26 { 27 /// Constructs this RPC processor using a FileReader to read RPC commands from and a std.stdio.File to write RPC commands to. 28 /// Creates this fiber with a reasonable fiber size. 29 this(FileReader reader, File writer) 30 { 31 super(&run, 4096 * 8); 32 this.reader = reader; 33 this.writer = writer; 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 `id`, `result` or `error` is not given on the response message, they won't be sent. 44 /// However according to the RPC specification, `id` must be set in order for this to be a response object. 45 /// Otherwise on success `result` must be set or on error `error` must be set. 46 /// This also logs the error to stderr if it is given. 47 /// Params: 48 /// res = the response message to send. 49 void send(ResponseMessage res) 50 { 51 auto msg = JSONValue(["jsonrpc": JSONValue("2.0")]); 52 if (res.id.hasData) 53 msg["id"] = res.id.toJSON; 54 55 if (!res.result.isNull) 56 msg["result"] = res.result.get; 57 58 if (!res.error.isNull) 59 { 60 msg["error"] = res.error.toJSON; 61 stderr.writeln(msg["error"]); 62 } 63 64 send(msg); 65 } 66 67 /// Sends an RPC request (method call) to the other side. Doesn't do any additional processing. 68 /// Params: 69 /// req = The request to send 70 void send(RequestMessage req) 71 { 72 send(req.toJSON); 73 } 74 75 /// Sends a raw JSON object to the other RPC side. 76 void send(JSONValue raw) 77 { 78 if (!("jsonrpc" in raw)) 79 { 80 error(raw); 81 throw new Exception("Sent objects must have a jsonrpc"); 82 } 83 const content = raw.toString(JSONOptions.doNotEscapeSlashes); 84 // Log on client side instead! (vscode setting: "serve-d.trace.server": "verbose") 85 //trace(content); 86 string data = "Content-Length: " ~ content.length.to!string ~ "\r\n\r\n" ~ content; 87 writer.rawWrite(data); 88 writer.flush(); 89 } 90 91 /// Sends a notification with the given `method` name to the other RPC side without any parameters. 92 void notifyMethod(string method) 93 { 94 RequestMessage req; 95 req.method = method; 96 send(req); 97 } 98 99 /// Sends a notification with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 100 void notifyMethod(T)(string method, T value) 101 { 102 notifyMethod(method, value.toJSON); 103 } 104 105 /// ditto 106 void notifyMethod(string method, JSONValue value) 107 { 108 RequestMessage req; 109 req.method = method; 110 req.params = value; 111 send(req); 112 } 113 114 /// Sends a request with the given `method` name to the other RPC side without any parameters. 115 /// Doesn't handle the response by the other RPC side. 116 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 117 RequestToken sendMethod(string method) 118 { 119 RequestMessage req; 120 req.id = RequestToken.random; 121 req.method = method; 122 send(req); 123 return req.id; 124 } 125 126 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 127 /// Doesn't handle the response by the other RPC side. 128 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 129 RequestToken sendMethod(T)(string method, T value) 130 { 131 return sendMethod(method, value.toJSON); 132 } 133 134 /// ditto 135 RequestToken sendMethod(string method, JSONValue value) 136 { 137 RequestMessage req; 138 req.id = RequestToken.random; 139 req.method = method; 140 req.params = value; 141 send(req); 142 return req.id; 143 } 144 145 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 146 /// Awaits the response (using yield) and returns once it's there. 147 /// 148 /// This is a small wrapper to call `awaitResponse(sendMethod(method, value))` 149 /// 150 /// Returns: The response deserialized from the RPC. 151 ResponseMessage sendRequest(T)(string method, T value) 152 { 153 return awaitResponse(sendMethod(method, value)); 154 } 155 156 /// ditto 157 ResponseMessage sendRequest(string method, JSONValue value) 158 { 159 return awaitResponse(sendMethod(method, value)); 160 } 161 162 /// Calls the `window/logMessage` method with all arguments concatenated together using text() 163 /// Params: 164 /// type = the $(REF MessageType, served,lsp,protocol) to use as $(REF LogMessageParams, served,lsp,protocol) type 165 /// args = the message parts to send 166 void log(MessageType type = MessageType.log, Args...)(Args args) 167 { 168 send(JSONValue([ 169 "jsonrpc": JSONValue("2.0"), 170 "method": JSONValue("window/logMessage"), 171 "params": LogMessageParams(type, text(args)).toJSON 172 ])); 173 } 174 175 /// Returns: `true` if there has been any messages been sent to us from the other RPC side, otherwise `false`. 176 bool hasData() const @property 177 { 178 return !messageQueue.empty; 179 } 180 181 /// Returns: the first message from the message queue. Removes it from the message queue so it will no longer be processed. 182 /// Throws: Exception if $(LREF hasData) is false. 183 RequestMessage poll() 184 { 185 if (!hasData) 186 throw new Exception("No Data"); 187 auto ret = messageQueue.front; 188 messageQueue.removeFront(); 189 return ret; 190 } 191 192 /// Convenience wrapper around $(LREF WindowFunctions) for `this`. 193 WindowFunctions window() 194 { 195 return WindowFunctions(this); 196 } 197 198 /// Waits for a response message to a request from the other RPC side. 199 /// If this is called after the response has already been sent and processed by yielding after sending the request, this will yield forever and use up memory. 200 /// So it is important, if you are going to await a response, to do it immediately when sending any request. 201 ResponseMessage awaitResponse(RequestToken tok) 202 { 203 size_t i; 204 bool found = false; 205 foreach (n, t; responseTokens) 206 { 207 if (t.handled) 208 { 209 // replace handled responses (overwrite reusable memory) 210 i = n; 211 found = true; 212 break; 213 } 214 } 215 if (!found) 216 i = responseTokens.length++; 217 responseTokens[i] = RequestWait(tok); 218 while (!responseTokens[i].got) 219 yield(); // yield until main loop placed a response 220 auto res = responseTokens[i].ret; 221 responseTokens[i].handled = true; // make memory reusable 222 return res; 223 } 224 225 private: 226 void onData(RequestMessage req) 227 { 228 messageQueue.insertBack(req); 229 } 230 231 FileReader reader; 232 File writer; 233 bool stopped; 234 DList!RequestMessage messageQueue; 235 236 struct RequestWait 237 { 238 RequestToken token; 239 bool got = false; 240 bool handled = false; 241 ResponseMessage ret; 242 } 243 244 RequestWait[] responseTokens; 245 246 void run() 247 { 248 assert(reader.isReading, "must start jsonrpc after file reader!"); 249 while (!stopped && reader.isReading) 250 { 251 bool inHeader = true; 252 size_t contentLength = 0; 253 do // dmd -O has an issue on mscoff where it forgets to emit a cmp here so this would break with while (inHeader) 254 { 255 string line = reader.yieldLine; 256 if (!reader.isReading) 257 stop(); // abort in header 258 259 if (!line.length && contentLength > 0) 260 inHeader = false; 261 else if (line.startsWith("Content-Length:")) 262 contentLength = line["Content-Length:".length .. $].strip.to!size_t; 263 } 264 while (inHeader && !stopped); 265 266 if (stopped) 267 break; 268 269 if (contentLength <= 0) 270 { 271 send(ResponseMessage(RequestToken.init, ResponseError(ErrorCode.invalidRequest, "Invalid/no content length specified"))); 272 continue; 273 } 274 275 auto content = cast(string) reader.yieldData(contentLength); 276 if (stopped || content is null) 277 break; 278 assert(content.length == contentLength); 279 RequestMessage request; 280 RequestMessage[] extraRequests; 281 try 282 { 283 auto json = parseJSON(content); 284 if (json.type == JSONType.array) 285 { 286 auto arr = json.array; 287 if (arr.length == 0) 288 send(ResponseMessage(RequestToken.init, ResponseError(ErrorCode.invalidRequest, "Empty batch request"))); 289 290 foreach (subRequest; arr) 291 { 292 auto res = handleRequestImpl(subRequest); 293 if (request == RequestMessage.init) 294 request = res; 295 else if (res != RequestMessage.init) 296 extraRequests ~= res; 297 } 298 } 299 else if (json.type == JSONType.object) 300 { 301 request = handleRequestImpl(json); 302 } 303 else 304 { 305 send(ResponseMessage(RequestToken.init, ResponseError(ErrorCode.invalidRequest, "Invalid request type (must be object or array)"))); 306 } 307 } 308 catch (Exception e) 309 { 310 try 311 { 312 trace(e); 313 trace(content); 314 auto idx = content.indexOf("\"id\":"); 315 auto endIdx = content.indexOf(",", idx); 316 RequestToken fallback; 317 if (!content.startsWith("[") && idx != -1 && endIdx != -1) 318 fallback = RequestToken(parseJSON(content[idx .. endIdx].strip)); 319 send(ResponseMessage(fallback, ResponseError(ErrorCode.parseError))); 320 } 321 catch (Exception e) 322 { 323 errorf("Got invalid request '%s'!", content); 324 trace(e); 325 } 326 } 327 328 if (request != RequestMessage.init) 329 { 330 onData(request); 331 Fiber.yield(); 332 } 333 334 foreach (req; extraRequests) 335 { 336 onData(request); 337 Fiber.yield(); 338 } 339 } 340 } 341 342 RequestMessage handleRequestImpl(JSONValue json) 343 { 344 auto id = "id" in json; 345 bool isResponse = false; 346 if (id) 347 { 348 auto tok = RequestToken(id); 349 foreach (ref waiting; responseTokens) 350 { 351 if (!waiting.got && waiting.token == tok) 352 { 353 waiting.got = true; 354 waiting.ret.id = tok; 355 auto res = "result" in json; 356 auto err = "error" in json; 357 if (res) 358 waiting.ret.result = *res; 359 if (err) 360 waiting.ret.error = (*err).fromJSON!ResponseError; 361 isResponse = true; 362 break; 363 } 364 } 365 } 366 if (!isResponse) 367 { 368 RequestMessage request = RequestMessage(json); 369 if (request.params != JSONValue.init 370 && request.params.type != JSONType.object 371 && request.params.type != JSONType.array) 372 { 373 send(ResponseMessage(request.id, 374 ResponseError(ErrorCode.invalidParams, 375 "`params` MUST be an object (named arguments) or array " 376 ~ "(positional arguments), other types are not allowed by spec" 377 ))); 378 } 379 else 380 { 381 return request; 382 } 383 } 384 return RequestMessage.init; 385 } 386 } 387 388 /// Utility functions for common LSP methods performing UI things. 389 struct WindowFunctions 390 { 391 /// The RPC processor to use for sending/receiving 392 RPCProcessor rpc; 393 private bool safeShowMessage; 394 395 /// Runs window/showMessage which typically shows a notification box, without any buttons or feedback. 396 /// Logs the message to stderr too. 397 void showMessage(MessageType type, string message) 398 { 399 if (!safeShowMessage) 400 warningf("%s message: %s", type, message); 401 rpc.notifyMethod("window/showMessage", ShowMessageParams(type, message)); 402 safeShowMessage = false; 403 } 404 405 /// 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. 406 MessageActionItem requestMessage(MessageType type, string message, MessageActionItem[] actions) 407 { 408 auto res = rpc.sendRequest("window/showMessageRequest", 409 ShowMessageRequestParams(type, message, actions.opt)); 410 if (res.result == JSONValue.init) 411 return MessageActionItem(null); 412 return res.result.fromJSON!MessageActionItem; 413 } 414 415 /// ditto 416 string requestMessage(MessageType type, string message, string[] actions) 417 { 418 MessageActionItem[] a = new MessageActionItem[actions.length]; 419 foreach (i, action; actions) 420 a[i] = MessageActionItem(action); 421 return requestMessage(type, message, a).title; 422 } 423 424 /// Runs a function and shows a UI message on failure and logs the error. 425 /// Returns: true if fn was successfully run or false if an exception occured. 426 bool runOrMessage(lazy void fn, MessageType type, string message, 427 string file = __FILE__, size_t line = __LINE__) 428 { 429 try 430 { 431 fn(); 432 return true; 433 } 434 catch (Exception e) 435 { 436 errorf("Error running in %s(%s): %s", file, line, e); 437 showMessage(type, message); 438 return false; 439 } 440 } 441 442 /// Calls $(LREF showMessage) with MessageType.error 443 /// Also logs the message to stderr in a more readable way. 444 void showErrorMessage(string message) 445 { 446 error("Error message: ", message); 447 safeShowMessage = true; 448 showMessage(MessageType.error, message); 449 } 450 451 /// Calls $(LREF showMessage) with MessageType.warning 452 /// Also logs the message to stderr in a more readable way. 453 void showWarningMessage(string message) 454 { 455 warning("Warning message: ", message); 456 safeShowMessage = true; 457 showMessage(MessageType.warning, message); 458 } 459 460 /// Calls $(LREF showMessage) with MessageType.info 461 /// Also logs the message to stderr in a more readable way. 462 void showInformationMessage(string message) 463 { 464 info("Info message: ", message); 465 safeShowMessage = true; 466 showMessage(MessageType.info, message); 467 } 468 469 /// Calls $(LREF showMessage) with MessageType.log 470 /// Also logs the message to stderr in a more readable way. 471 void showLogMessage(string message) 472 { 473 trace("Log message: ", message); 474 safeShowMessage = true; 475 showMessage(MessageType.log, message); 476 } 477 }