1 /**
2  * This file is part of DCD, a development tool for the D programming language.
3  * Copyright (C) 2014 Brian Schott
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 module dcd.common.messages;
20 
21 import std.socket;
22 import msgpack;
23 import core.time : dur;
24 
25 /**
26  * The type of completion list being returned
27  */
28 enum CompletionType : string
29 {
30 	/**
31 	 * The completion list contains a listing of identifier/kind pairs.
32 	 */
33 	identifiers = "identifiers",
34 
35 	/**
36 	 * The auto-completion list consists of a listing of functions and their
37 	 * parameters.
38 	 */
39 	calltips = "calltips",
40 
41 	/**
42 	 * The response contains the location of a symbol declaration.
43 	 */
44 	location = "location",
45 
46 	/**
47 	 * The response contains documentation comments for the symbol.
48 	 */
49 	ddoc = "ddoc",
50 }
51 
52 /**
53  * Request kind
54  */
55 enum RequestKind : ushort
56 {
57 	// dfmt off
58 	uninitialized =  0b00000000_00000000,
59 	/// Autocompletion
60 	autocomplete =   0b00000000_00000001,
61 	/// Clear the completion cache
62 	clearCache =     0b00000000_00000010,
63 	/// Add import directory to server
64 	addImport =      0b00000000_00000100,
65 	/// Shut down the server
66 	shutdown =       0b00000000_00001000,
67 	/// Get declaration location of given symbol
68 	symbolLocation = 0b00000000_00010000,
69 	/// Get the doc comments for the symbol
70 	doc =            0b00000000_00100000,
71 	/// Query server status
72 	query =	         0b00000000_01000000,
73 	/// Search for symbol
74 	search =         0b00000000_10000000,
75 	/// List import directories
76 	listImports =    0b00000001_00000000,
77 	/// local symbol usage
78 	localUse =     	 0b00000010_00000000,
79 	// dfmt on
80 }
81 
82 /**
83  * Autocompletion request message
84  */
85 struct AutocompleteRequest
86 {
87 	/**
88 	 * File name used for error reporting
89 	 */
90 	string fileName;
91 
92 	/**
93 	 * Command coming from the client
94 	 */
95 	RequestKind kind;
96 
97 	/**
98 	 * Paths to be searched for import files
99 	 */
100 	string[] importPaths;
101 
102 	/**
103 	 * The source code to auto complete
104 	 */
105 	ubyte[] sourceCode;
106 
107 	/**
108 	 * The cursor position
109 	 */
110 	size_t cursorPosition;
111 
112 	/**
113 	 * Name of symbol searched for
114 	 */
115 	string searchName;
116 }
117 
118 /**
119  * Autocompletion response message
120  */
121 struct AutocompleteResponse
122 {
123 	static struct Completion
124 	{
125 		/**
126 		 * The name of the symbol for a completion, for calltips just the function name.
127 		 */
128 		string identifier;
129 		/**
130 		 * The kind of the item. Will be char.init for calltips.
131 		 */
132 		ubyte kind;
133 		/**
134 		 * Definition for a symbol for a completion including attributes or the arguments for calltips.
135 		 */
136 		string definition;
137 		/**
138 		 * The path to the file that contains the symbol.
139 		 */
140 		string symbolFilePath;
141 		/**
142 		 * The byte offset at which the symbol is located or symbol location for symbol searches.
143 		 */
144 		size_t symbolLocation;
145 		/**
146 		 * Documentation associated with this symbol.
147 		 */
148 		string documentation;
149 
150 		deprecated("Use identifier (or definition for calltips) instead") string compatibilityContent() const
151 		{
152 			if (kind == ubyte.init)
153 				return definition;
154 			else
155 				return identifier;
156 		}
157 
158 		alias compatibilityContent this;
159 	}
160 
161 	/**
162 	 * The autocompletion type. (Parameters or identifier)
163 	 */
164 	string completionType;
165 
166 	/**
167 	 * The path to the file that contains the symbol.
168 	 */
169 	string symbolFilePath;
170 
171 	/**
172 	 * The byte offset at which the symbol is located.
173 	 */
174 	size_t symbolLocation;
175 
176 	/**
177 	 * The completions
178 	 */
179 	Completion[] completions;
180 
181 	/**
182 	 * Import paths that are registered by the server.
183 	 */
184 	string[] importPaths;
185 
186 	/**
187 	 * Symbol identifier
188 	 */
189 	ulong symbolIdentifier;
190 
191 	deprecated("use completions[].documentation + escapeConsoleOutputString instead") string[] docComments() @property
192 	{
193 		string[] ret;
194 		foreach (ref completion; completions)
195 			ret ~= completion.documentation.escapeConsoleOutputString(true);
196 		return ret;
197 	}
198 
199 	deprecated("use completions[].kind instead") char[] completionKinds() @property
200 	{
201 		char[] ret;
202 		foreach (ref completion; completions)
203 			ret ~= completion.kind;
204 		return ret;
205 	}
206 
207 	deprecated("use completions[].symbolLocation instead") size_t[] locations() @property
208 	{
209 		size_t[] ret;
210 		foreach (ref completion; completions)
211 			ret ~= completion.symbolLocation;
212 		return ret;
213 	}
214 
215 	/**
216 	 * Creates an empty acknowledgement response
217 	 */
218 	static AutocompleteResponse ack()
219 	{
220 		AutocompleteResponse response;
221 		response.completionType = "ack";
222 		return response;
223 	}
224 }
225 
226 /**
227  * Returns: true on success
228  */
229 bool sendRequest(Socket socket, AutocompleteRequest request)
230 {
231 	ubyte[] message = msgpack.pack(request);
232 	ubyte[] messageBuffer = new ubyte[message.length + message.length.sizeof];
233 	auto messageLength = message.length;
234 	messageBuffer[0 .. size_t.sizeof] = (cast(ubyte*) &messageLength)[0 .. size_t.sizeof];
235 	messageBuffer[size_t.sizeof .. $] = message[];
236 	return socket.send(messageBuffer) == messageBuffer.length;
237 }
238 
239 /**
240  * Gets the response from the server
241  */
242 AutocompleteResponse getResponse(Socket socket)
243 {
244 	ubyte[1024 * 16] buffer;
245 	ptrdiff_t bytesReceived = 0;
246 	auto unpacker = StreamingUnpacker([]);
247 
248 	do {
249 		bytesReceived = socket.receive(buffer);
250 		if (bytesReceived == Socket.ERROR)
251 			throw new Exception(lastSocketError);
252 		if (bytesReceived == 0)
253 			break;
254 		unpacker.feed(buffer[0..bytesReceived]);
255 	} while (bytesReceived == buffer.length);
256 
257 	if (unpacker.size == 0)
258 		throw new Exception("Server closed the connection, 0 bytes received");
259 
260 	if (!unpacker.execute())
261 		throw new Exception("Could not unpack the response");
262 
263 	return unpacker.purge().value.as!AutocompleteResponse;
264 }
265 
266 /**
267  * Returns: true if a server instance is running
268  * Params:
269  *     useTCP = `true` to check a TCP port, `false` for UNIX domain socket
270  *     socketFile = the file name for the UNIX domain socket
271  *     port = the TCP port
272  */
273 bool serverIsRunning(bool useTCP, string socketFile, ushort port)
274 {
275 	scope (failure)
276 		return false;
277 	AutocompleteRequest request;
278 	request.kind = RequestKind.query;
279 	Socket socket;
280 	scope (exit)
281 	{
282 		socket.shutdown(SocketShutdown.BOTH);
283 		socket.close();
284 	}
285 	version(Windows) useTCP = true;
286 	if (useTCP)
287 	{
288 		socket = new TcpSocket(AddressFamily.INET);
289 		socket.connect(new InternetAddress("localhost", port));
290 	}
291 	else
292 	{
293 		version(Windows) {} else
294 		{
295 			socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
296 			socket.connect(new UnixAddress(socketFile));
297 		}
298 	}
299 	socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5));
300 	socket.blocking = true;
301 	if (sendRequest(socket, request))
302 		return getResponse(socket).completionType == "ack";
303 	else
304 		return false;
305 }
306 
307 /// Escapes \n, \t and \ in the string. If `single` is true \t won't be escaped.
308 /// Params:
309 ///   s = the string to escape
310 ///   single = if true, `s` is already tab separated and only needs to be escape new lines.
311 ///            Otherwise `s` is treated as one value that is meant to be printed tab separated with other values.
312 /// Returns: An escaped string that is safe to print to the console so it can be read line by line
313 string escapeConsoleOutputString(string s, bool single = false)
314 {
315 	import std.array : Appender;
316 
317 	Appender!(char[]) app;
318 	void putChar(char c)
319 	{
320 		switch (c)
321 		{
322 		case '\\':
323 			app.put('\\');
324 			app.put('\\');
325 			break;
326 		case '\n':
327 			app.put('\\');
328 			app.put('n');
329 			break;
330 		case '\t':
331 			if (single)
332 				goto default;
333 			else
334 			{
335 				app.put('\\');
336 				app.put('t');
337 				break;
338 			}
339 		default:
340 			app.put(c);
341 			break;
342 		}
343 	}
344 
345 	foreach (char c; s)
346 		putChar(c);
347 
348 	return app.data.idup;
349 }
350 
351 /// Joins string arguments with tabs and escapes them
352 string makeTabSeparated(string[] args...)
353 {
354 	import std.algorithm : map;
355 	import std.array : join;
356 
357 	return args.map!(a => a.escapeConsoleOutputString).join("\t");
358 }