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 }