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