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 }