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 client.client;
20 
21 import std.socket;
22 import std.stdio;
23 import std.getopt;
24 import std.array;
25 import std.process;
26 import std.algorithm;
27 import std.path;
28 import std.file;
29 import std.conv;
30 import std.string;
31 import std.experimental.logger;
32 
33 import common.messages;
34 import common.dcd_version;
35 import common.socket;
36 
37 int main(string[] args)
38 {
39 	sharedLog.fatalHandler = () {};
40 
41 	size_t cursorPos = size_t.max;
42 	string[] importPaths;
43 	ushort port;
44 	bool help;
45 	bool shutdown;
46 	bool clearCache;
47 	bool symbolLocation;
48 	bool doc;
49 	bool query;
50 	bool printVersion;
51 	bool listImports;
52 	bool getIdentifier;
53 	bool localUse;
54 	string search;
55 	version(Windows)
56 	{
57 		bool useTCP = true;
58 		string socketFile;
59 	}
60 	else
61 	{
62 		bool useTCP = false;
63 		string socketFile = generateSocketName();
64 	}
65 
66 	try
67 	{
68 		getopt(args, "cursorPos|c", &cursorPos, "I", &importPaths,
69 			"port|p", &port, "help|h", &help, "shutdown", &shutdown,
70 			"clearCache", &clearCache, "symbolLocation|l", &symbolLocation,
71 			"doc|d", &doc, "query|status|q", &query, "search|s", &search,
72 			"version", &printVersion, "listImports", &listImports,
73 			"tcp", &useTCP, "socketFile", &socketFile,
74 			"getIdentifier", &getIdentifier,
75 			"localUsage", &localUse, // TODO:remove this line in Nov. 2017
76 			"localUse|u", &localUse);
77 	}
78 	catch (ConvException e)
79 	{
80 		fatal(e.msg);
81 		printHelp(args[0]);
82 		return 1;
83 	}
84 
85 	AutocompleteRequest request;
86 
87 	if (help)
88 	{
89 		printHelp(args[0]);
90 		return 0;
91 	}
92 
93 	if (printVersion)
94 	{
95 		version (Windows)
96 			writeln(DCD_VERSION);
97 		else version(built_with_dub)
98 			writeln(DCD_VERSION);
99 		else
100 			write(DCD_VERSION, " ", GIT_HASH);
101 		return 0;
102 	}
103 
104 	version (Windows) if (socketFile !is null)
105 	{
106 		fatal("UNIX domain sockets not supported on Windows");
107 		return 1;
108 	}
109 
110 	// If the user specified a port number, assume that they wanted a TCP
111 	// connection. Otherwise set the port number to the default and let the
112 	// useTCP flag deterimen what to do later.
113 	if (port != 0)
114 		useTCP = true;
115 	else
116 		port = DEFAULT_PORT_NUMBER;
117 
118 	if (useTCP)
119 		socketFile = null;
120 
121 	if (query)
122 	{
123 		if (serverIsRunning(useTCP, socketFile, port))
124 		{
125 			writeln("Server is running");
126 			return 0;
127 		}
128 		else
129 		{
130 			writeln("Server is not running");
131 			return 1;
132 		}
133 	}
134 	else if (shutdown || clearCache)
135 	{
136 		if (shutdown)
137 		request.kind = RequestKind.shutdown;
138 		else if (clearCache)
139 			request.kind = RequestKind.clearCache;
140 		Socket socket = createSocket(socketFile, port);
141 		scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); }
142 		return sendRequest(socket, request) ? 0 : 1;
143 	}
144 	else if (importPaths.length > 0)
145 	{
146 		request.kind |= RequestKind.addImport;
147 		request.importPaths = importPaths.map!(a => absolutePath(a)).array;
148 		if (cursorPos == size_t.max)
149 		{
150 			Socket socket = createSocket(socketFile, port);
151 			scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); }
152 			if (!sendRequest(socket, request))
153 				return 1;
154 			return 0;
155 		}
156 	}
157 	else if (listImports)
158 	{
159 		request.kind |= RequestKind.listImports;
160 		Socket socket = createSocket(socketFile, port);
161 		scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); }
162 		sendRequest(socket, request);
163 		AutocompleteResponse response = getResponse(socket);
164 		printImportList(response);
165 		return 0;
166 	}
167 	else if (search == null && cursorPos == size_t.max)
168 	{
169 		// cursor position is a required argument
170 		printHelp(args[0]);
171 		return 1;
172 	}
173 
174 	// Read in the source
175 	immutable bool usingStdin = args.length <= 1;
176 	string fileName = usingStdin ? "stdin" : args[1];
177 	if (!usingStdin && !exists(args[1]))
178 	{
179 		stderr.writefln("%s does not exist", args[1]);
180 		return 1;
181 	}
182 	ubyte[] sourceCode;
183 	if (usingStdin)
184 	{
185 		ubyte[4096] buf;
186 		while (true)
187 		{
188 			auto b = stdin.rawRead(buf);
189 			if (b.length == 0)
190 				break;
191 			sourceCode ~= b;
192 		}
193 	}
194 	else
195 	{
196 		if (!exists(args[1]))
197 		{
198 			stderr.writeln("Could not find ", args[1]);
199 			return 1;
200 		}
201 		File f = File(args[1]);
202 		sourceCode = uninitializedArray!(ubyte[])(to!size_t(f.size));
203 		f.rawRead(sourceCode);
204 	}
205 
206 	request.fileName = fileName;
207 	request.importPaths = importPaths;
208 	request.sourceCode = sourceCode;
209 	request.cursorPosition = cursorPos;
210 	request.searchName = search;
211 
212 	if (symbolLocation | getIdentifier)
213 		request.kind |= RequestKind.symbolLocation;
214 	else if (doc)
215 		request.kind |= RequestKind.doc;
216 	else if (search)
217 		request.kind |= RequestKind.search;
218 	else if(localUse)
219 		request.kind |= RequestKind.localUse;
220 	else
221 		request.kind |= RequestKind.autocomplete;
222 
223 	// Send message to server
224 	Socket socket = createSocket(socketFile, port);
225 	scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); }
226 	if (!sendRequest(socket, request))
227 		return 1;
228 
229 	AutocompleteResponse response = getResponse(socket);
230 
231 	if (symbolLocation)
232 		printLocationResponse(response);
233 	else if (getIdentifier)
234 		printIdentifierResponse(response);
235 	else if (doc)
236 		printDocResponse(response);
237 	else if (search !is null)
238 		printSearchResponse(response);
239 	else if (localUse)
240 		printLocalUse(response);
241 	else
242 		printCompletionResponse(response);
243 
244 	return 0;
245 }
246 
247 private:
248 
249 void printHelp(string programName)
250 {
251 	writefln(
252 `
253     Usage: %1$s [Options] [FILENAME]
254 
255     A file name is optional. If it is given, autocomplete information will be
256     given for the file specified. If it is missing, input will be read from
257     stdin instead.
258 
259     Source code is assumed to be UTF-8 encoded and must not exceed 4 megabytes.
260 
261 Options:
262     --help | -h
263         Displays this help message
264 
265     --cursorPos | -c position
266         Provides auto-completion at the given cursor position. The cursor
267         position is measured in bytes from the beginning of the source code.
268 
269     --clearCache
270         Instructs the server to clear out its autocompletion cache.
271 
272     --shutdown
273         Instructs the server to shut down.
274 
275     --symbolLocation | -l
276         Get the file name and position that the symbol at the cursor location
277         was defined.
278 
279     --doc | -d
280         Gets documentation comments associated with the symbol at the cursor
281         location.
282 
283     --search | -s symbolName
284         Searches for symbolName in both stdin / the given file name as well as
285         others files cached by the server.
286 
287     --localUse | -u
288         Searches for all the uses of the symbol at the cursor location
289         in the given filename (or stdin).
290 
291     --query | -q | --status
292         Query the server statis. Returns 0 if the server is running. Returns
293         1 if the server could not be contacted.
294 
295     -I PATH
296         Instructs the server to add PATH to its list of paths searched for
297         imported modules.
298 
299     --version
300         Prints the version number and then exits.
301 
302     --port PORTNUMBER | -p PORTNUMBER
303         Uses PORTNUMBER to communicate with the server instead of the default
304         port 9166. Only used on Windows or when the --tcp option is set.
305 
306     --tcp
307         Send requests on a TCP socket instead of a UNIX domain socket. This
308         switch has no effect on Windows.
309 
310     --socketFile FILENAME
311         Use the given FILENAME as the path to the UNIX domain socket. Using
312         this switch is an error on Windows.`, programName);
313 }
314 
315 Socket createSocket(string socketFile, ushort port)
316 {
317 	import core.time : dur;
318 
319 	Socket socket;
320 	if (socketFile is null)
321 	{
322 		socket = new TcpSocket(AddressFamily.INET);
323 		socket.connect(new InternetAddress("localhost", port));
324 	}
325 	else
326 	{
327 		version(Windows)
328 		{
329 			// should never be called with non-null socketFile on Windows
330 			assert(false);
331 		}
332 		else
333 		{
334 			socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
335 			socket.connect(new UnixAddress(socketFile));
336 		}
337 	}
338 	socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5));
339 	socket.blocking = true;
340 	return socket;
341 }
342 
343 void printDocResponse(ref const AutocompleteResponse response)
344 {
345 	import std.algorithm : each;
346 	response.docComments.each!(writeln);
347 }
348 
349 void printIdentifierResponse(ref const AutocompleteResponse response)
350 {
351 	if (response.completions.length == 0)
352 		return;
353 	write(response.completions[0]);
354 	write("\t");
355 	writeln(response.symbolIdentifier);
356 }
357 
358 void printLocationResponse(ref const AutocompleteResponse response)
359 {
360 	if (response.symbolFilePath is null)
361 		writeln("Not found");
362 	else
363 		writefln("%s\t%d", response.symbolFilePath, response.symbolLocation);
364 }
365 
366 void printCompletionResponse(ref const AutocompleteResponse response)
367 {
368 	if (response.completions.length > 0)
369 	{
370 		writeln(response.completionType);
371 		auto app = appender!(string[])();
372 		if (response.completionType == CompletionType.identifiers)
373 		{
374 			for (size_t i = 0; i < response.completions.length; i++)
375 				app.put(format("%s\t%s", response.completions[i], response.completionKinds[i]));
376 		}
377 		else
378 		{
379 			foreach (completion; response.completions)
380 			{
381 				app.put(completion);
382 			}
383 		}
384 		// Deduplicate overloaded methods
385 		foreach (line; app.data.sort().uniq)
386 			writeln(line);
387 	}
388 }
389 
390 void printSearchResponse(const AutocompleteResponse response)
391 {
392 	foreach(i; 0 .. response.completions.length)
393 	{
394 		writefln("%s\t%s\t%s", response.completions[i], response.completionKinds[i],
395 			response.locations[i]);
396 	}
397 }
398 
399 void printLocalUse(const AutocompleteResponse response)
400 {
401 	if (response.symbolFilePath.length)
402 	{
403 		writeln(response.symbolFilePath, '\t', response.symbolLocation);
404 		foreach(loc; response.locations)
405 			writeln(loc);
406 	}
407 	else write("00000");
408 }
409 
410 void printImportList(const AutocompleteResponse response)
411 {
412 	import std.algorithm.iteration : each;
413 
414 	response.importPaths.each!(a => writeln(a));
415 }