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 RequestMessage req; 103 req.method = method; 104 req.params = value.toJSON; 105 send(req); 106 } 107 108 /// Sends a request with the given `method` name to the other RPC side without any parameters. 109 /// Doesn't handle the response by the other RPC side. 110 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 111 RequestToken sendMethod(string method) 112 { 113 RequestMessage req; 114 req.id = RequestToken.random; 115 req.method = method; 116 send(req); 117 return req.id; 118 } 119 120 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 121 /// Doesn't handle the response by the other RPC side. 122 /// Returns: a RequestToken to use with $(LREF awaitResponse) to get the response. Can be ignored if the response isn't important. 123 RequestToken sendMethod(T)(string method, T value) 124 { 125 RequestMessage req; 126 req.id = RequestToken.random; 127 req.method = method; 128 req.params = value.toJSON; 129 send(req); 130 return req.id; 131 } 132 133 /// Sends a request with the given `method` name to the other RPC side with the given `value` parameter serialized to JSON. 134 /// Awaits the response (using yield) and returns once it's there. 135 /// 136 /// This is a small wrapper to call `awaitResponse(sendMethod(method, value))` 137 /// 138 /// Returns: The response deserialized from the RPC. 139 ResponseMessage sendRequest(T)(string method, T value) 140 { 141 return awaitResponse(sendMethod(method, value)); 142 } 143 144 /// Calls the `window/logMessage` method with all arguments concatenated together using text() 145 /// Params: 146 /// type = the $(REF MessageType, served,lsp,protocol) to use as $(REF LogMessageParams, served,lsp,protocol) type 147 /// args = the message parts to send 148 void log(MessageType type = MessageType.log, Args...)(Args args) 149 { 150 send(JSONValue([ 151 "jsonrpc": JSONValue("2.0"), 152 "method": JSONValue("window/logMessage"), 153 "params": LogMessageParams(type, text(args)).toJSON 154 ])); 155 } 156 157 /// Returns: `true` if there has been any messages been sent to us from the other RPC side, otherwise `false`. 158 bool hasData() const @property 159 { 160 return !messageQueue.empty; 161 } 162 163 /// Returns: the first message from the message queue. Removes it from the message queue so it will no longer be processed. 164 /// Throws: Exception if $(LREF hasData) is false. 165 RequestMessage poll() 166 { 167 if (!hasData) 168 throw new Exception("No Data"); 169 auto ret = messageQueue.front; 170 messageQueue.removeFront(); 171 return ret; 172 } 173 174 /// Convenience wrapper around $(LREF WindowFunctions) for `this`. 175 WindowFunctions window() 176 { 177 return WindowFunctions(this); 178 } 179 180 /// Waits for a response message to a request from the other RPC side. 181 /// 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. 182 /// So it is important, if you are going to await a response, to do it immediately when sending any request. 183 ResponseMessage awaitResponse(RequestToken tok) 184 { 185 size_t i; 186 bool found = false; 187 foreach (n, t; responseTokens) 188 { 189 if (t.handled) 190 { 191 // replace handled responses (overwrite reusable memory) 192 i = n; 193 found = true; 194 break; 195 } 196 } 197 if (!found) 198 i = responseTokens.length++; 199 responseTokens[i] = RequestWait(tok); 200 while (!responseTokens[i].got) 201 yield(); // yield until main loop placed a response 202 auto res = responseTokens[i].ret; 203 responseTokens[i].handled = true; // make memory reusable 204 return res; 205 } 206 207 private: 208 void onData(RequestMessage req) 209 { 210 messageQueue.insertBack(req); 211 } 212 213 FileReader reader; 214 File writer; 215 bool stopped; 216 DList!RequestMessage messageQueue; 217 218 struct RequestWait 219 { 220 RequestToken token; 221 bool got = false; 222 bool handled = false; 223 ResponseMessage ret; 224 } 225 226 RequestWait[] responseTokens; 227 228 void run() 229 { 230 while (!stopped) 231 { 232 bool inHeader = true; 233 size_t contentLength = 0; 234 do // dmd -O has an issue on mscoff where it forgets to emit a cmp here so this would break with while (inHeader) 235 { 236 string line = reader.yieldLine; 237 if (!line.length && contentLength > 0) 238 inHeader = false; 239 else if (line.startsWith("Content-Length:")) 240 contentLength = line["Content-Length:".length .. $].strip.to!size_t; 241 } 242 while (inHeader); 243 assert(contentLength > 0); 244 auto content = cast(string) reader.yieldData(contentLength); 245 assert(content.length == contentLength); 246 RequestMessage request; 247 bool validRequest = false; 248 try 249 { 250 auto json = parseJSON(content); 251 auto id = "id" in json; 252 bool isResponse = false; 253 if (id) 254 { 255 auto tok = RequestToken(id); 256 foreach (ref waiting; responseTokens) 257 { 258 if (!waiting.got && waiting.token == tok) 259 { 260 waiting.got = true; 261 waiting.ret.id = tok; 262 auto res = "result" in json; 263 auto err = "error" in json; 264 if (res) 265 waiting.ret.result = *res; 266 if (err) 267 waiting.ret.error = (*err).fromJSON!ResponseError; 268 isResponse = true; 269 break; 270 } 271 } 272 } 273 if (!isResponse) 274 { 275 request = RequestMessage(json); 276 validRequest = true; 277 } 278 } 279 catch (Exception e) 280 { 281 try 282 { 283 trace(e); 284 trace(content); 285 auto idx = content.indexOf("\"id\":"); 286 auto endIdx = content.indexOf(",", idx); 287 JSONValue fallback; 288 if (idx != -1 && endIdx != -1) 289 fallback = parseJSON(content[idx .. endIdx].strip); 290 else 291 fallback = JSONValue(0); 292 send(ResponseMessage(RequestToken(&fallback), ResponseError(ErrorCode.parseError))); 293 } 294 catch (Exception e) 295 { 296 errorf("Got invalid request '%s'!", content); 297 trace(e); 298 } 299 } 300 if (validRequest) 301 { 302 onData(request); 303 Fiber.yield(); 304 } 305 } 306 } 307 } 308 309 /// Utility functions for common LSP methods performing UI things. 310 struct WindowFunctions 311 { 312 /// The RPC processor to use for sending/receiving 313 RPCProcessor rpc; 314 private bool safeShowMessage; 315 316 /// Runs window/showMessage which typically shows a notification box, without any buttons or feedback. 317 /// Logs the message to stderr too. 318 void showMessage(MessageType type, string message) 319 { 320 if (!safeShowMessage) 321 warningf("%s message: %s", type, message); 322 rpc.notifyMethod("window/showMessage", ShowMessageParams(type, message)); 323 safeShowMessage = false; 324 } 325 326 /// 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. 327 MessageActionItem requestMessage(MessageType type, string message, MessageActionItem[] actions) 328 { 329 auto res = rpc.sendRequest("window/showMessageRequest", 330 ShowMessageRequestParams(type, message, actions.opt)); 331 if (res.result == JSONValue.init) 332 return MessageActionItem(null); 333 return res.result.fromJSON!MessageActionItem; 334 } 335 336 /// ditto 337 string requestMessage(MessageType type, string message, string[] actions) 338 { 339 MessageActionItem[] a = new MessageActionItem[actions.length]; 340 foreach (i, action; actions) 341 a[i] = MessageActionItem(action); 342 return requestMessage(type, message, a).title; 343 } 344 345 /// Runs a function and shows a UI message on failure and logs the error. 346 /// Returns: true if fn was successfully run or false if an exception occured. 347 bool runOrMessage(lazy void fn, MessageType type, string message, 348 string file = __FILE__, size_t line = __LINE__) 349 { 350 try 351 { 352 fn(); 353 return true; 354 } 355 catch (Exception e) 356 { 357 errorf("Error running in %s(%s): %s", file, line, e); 358 showMessage(type, message); 359 return false; 360 } 361 } 362 363 /// Calls $(LREF showMessage) with MessageType.error 364 /// Also logs the message to stderr in a more readable way. 365 void showErrorMessage(string message) 366 { 367 error("Error message: ", message); 368 safeShowMessage = true; 369 showMessage(MessageType.error, message); 370 } 371 372 /// Calls $(LREF showMessage) with MessageType.warning 373 /// Also logs the message to stderr in a more readable way. 374 void showWarningMessage(string message) 375 { 376 warning("Warning message: ", message); 377 safeShowMessage = true; 378 showMessage(MessageType.warning, message); 379 } 380 381 /// Calls $(LREF showMessage) with MessageType.info 382 /// Also logs the message to stderr in a more readable way. 383 void showInformationMessage(string message) 384 { 385 info("Info message: ", message); 386 safeShowMessage = true; 387 showMessage(MessageType.info, message); 388 } 389 390 /// Calls $(LREF showMessage) with MessageType.log 391 /// Also logs the message to stderr in a more readable way. 392 void showLogMessage(string message) 393 { 394 trace("Log message: ", message); 395 safeShowMessage = true; 396 showMessage(MessageType.log, message); 397 } 398 }