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.server.main;
20 
21 import core.sys.posix.sys.stat;
22 import std.algorithm;
23 import std.array;
24 import std.conv;
25 import std.datetime.stopwatch : AutoStart, StopWatch;
26 import std.exception : enforce;
27 import stdx.allocator;
28 import stdx.allocator.mallocator;
29 import std.experimental.logger;
30 import std.file;
31 import std.getopt;
32 import std.path: buildPath;
33 import std.process;
34 import std.socket;
35 import std.stdio;
36 
37 import msgpack;
38 
39 import dcd.common.dcd_version;
40 import dcd.common.messages;
41 import dcd.common.socket;
42 import dsymbol.modulecache;
43 import dcd.server.autocomplete;
44 import dcd.server.server;
45 
46 int main(string[] args)
47 {
48 	ushort port;
49 	bool help;
50 	bool printVersion;
51 	bool ignoreConfig;
52 	string[] importPaths;
53 	LogLevel level = globalLogLevel;
54 	version(Windows)
55 	{
56 		bool useTCP = true;
57 		string socketFile;
58 	}
59 	else
60 	{
61 		bool useTCP = false;
62 		string socketFile = generateSocketName();
63 	}
64 
65 	sharedLog.fatalHandler = () {};
66 
67 	try
68 	{
69 		getopt(args, "port|p", &port, "I", &importPaths, "help|h", &help,
70 			"version", &printVersion, "ignoreConfig", &ignoreConfig,
71 			"logLevel", &level, "tcp", &useTCP, "socketFile", &socketFile);
72 	}
73 	catch (ConvException e)
74 	{
75 		fatal(e.msg);
76 		printHelp(args[0]);
77 		return 1;
78 	}
79 
80 	globalLogLevel = level;
81 
82 	if (printVersion)
83 	{
84 		version (Windows)
85 			writeln(DCD_VERSION);
86 		else version (built_with_dub)
87 			writeln(DCD_VERSION);
88 		else
89 			write(DCD_VERSION, " ", GIT_HASH);
90 		return 0;
91 	}
92 
93 	if (help)
94 	{
95 		printHelp(args[0]);
96 		return 0;
97 	}
98 
99 	// If the user specified a port number, assume that they wanted a TCP
100 	// connection. Otherwise set the port number to the default and let the
101 	// useTCP flag deterimen what to do later.
102 	if (port != 0)
103 		useTCP = true;
104 	else
105 		port = DEFAULT_PORT_NUMBER;
106 
107 	if (useTCP)
108 		socketFile = null;
109 
110 	version (Windows) if (socketFile !is null)
111 	{
112 		fatal("UNIX domain sockets not supported on Windows");
113 		return 1;
114 	}
115 
116 	if (serverIsRunning(useTCP, socketFile, port))
117 	{
118 		fatal("Another instance of DCD-server is already running");
119 		return 1;
120 	}
121 
122 	info("Starting up...");
123 	StopWatch sw = StopWatch(AutoStart.yes);
124 
125 	if (!ignoreConfig)
126 		importPaths ~= loadConfiguredImportDirs();
127 
128 	Socket socket;
129 	if (useTCP)
130 	{
131 		socket = new TcpSocket(AddressFamily.INET);
132 		socket.blocking = true;
133 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
134 		socket.bind(new InternetAddress("localhost", port));
135 		info("Listening on port ", port);
136 	}
137 	else
138 	{
139 		version(Windows)
140 		{
141 			fatal("UNIX domain sockets not supported on Windows");
142 			return 1;
143 		}
144 		else
145 		{
146 			socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
147 			if (exists(socketFile))
148 			{
149 				info("Cleaning up old socket file at ", socketFile);
150 				remove(socketFile);
151 			}
152 			socket.blocking = true;
153 			socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
154 			socket.bind(new UnixAddress(socketFile));
155 			setAttributes(socketFile, S_IRUSR | S_IWUSR);
156 			info("Listening at ", socketFile);
157 		}
158 	}
159 	socket.listen(32);
160 
161 	scope (exit)
162 	{
163 		info("Shutting down sockets...");
164 		socket.shutdown(SocketShutdown.BOTH);
165 		socket.close();
166 		if (!useTCP)
167 			remove(socketFile);
168 		info("Sockets shut down.");
169 	}
170 
171 	ModuleCache cache = ModuleCache(new ASTAllocator);
172 	cache.addImportPaths(importPaths);
173 	infof("Import directories:\n    %-(%s\n    %)", cache.getImportPaths());
174 
175 	ubyte[] buffer = cast(ubyte[]) Mallocator.instance.allocate(1024 * 1024 * 4); // 4 megabytes should be enough for anybody...
176 	scope(exit) Mallocator.instance.deallocate(buffer);
177 
178 	sw.stop();
179 	info(cache.symbolsAllocated, " symbols cached.");
180 	info("Startup completed in ", sw.peek().total!"msecs"(), " milliseconds.");
181 
182 	// No relative paths
183 	version (Posix) chdir("/");
184 
185 	version (LittleEndian)
186 		immutable expectedClient = IPv4Union([1, 0, 0, 127]);
187 	else
188 		immutable expectedClient = IPv4Union([127, 0, 0, 1]);
189 
190 	serverLoop: while (true)
191 	{
192 		auto s = socket.accept();
193 		s.blocking = true;
194 
195 		if (useTCP)
196 		{
197 			// Only accept connections from localhost
198 			IPv4Union actual;
199 			InternetAddress clientAddr = cast(InternetAddress) s.remoteAddress();
200 			actual.i = clientAddr.addr;
201 			// Shut down if somebody tries connecting from outside
202 			if (actual.i != expectedClient.i)
203 			{
204 				fatal("Connection attempted from ", clientAddr.toAddrString());
205 				return 1;
206 			}
207 		}
208 
209 		scope (exit)
210 		{
211 			s.shutdown(SocketShutdown.BOTH);
212 			s.close();
213 		}
214 
215 		ptrdiff_t bytesReceived = s.receive(buffer);
216 
217 		sw.reset();
218 		sw.start();
219 
220 		size_t messageLength;
221 		// bit magic!
222 		(cast(ubyte*) &messageLength)[0..size_t.sizeof] = buffer[0..size_t.sizeof];
223 		while (bytesReceived < messageLength + size_t.sizeof)
224 		{
225 			immutable b = s.receive(buffer[bytesReceived .. $]);
226 			if (b == Socket.ERROR)
227 			{
228 				bytesReceived = Socket.ERROR;
229 				break;
230 			}
231 			bytesReceived += b;
232 		}
233 
234 		if (bytesReceived == Socket.ERROR)
235 		{
236 			warning("Socket recieve failed");
237 			break;
238 		}
239 
240 		AutocompleteRequest request;
241 		msgpack.unpack(buffer[size_t.sizeof .. bytesReceived], request);
242 
243 		if (request.kind & RequestKind.clearCache)
244 		{
245 			info("Clearing cache.");
246 			cache.clear();
247 		}
248 		else if (request.kind & RequestKind.shutdown)
249 		{
250 			info("Shutting down.");
251 			break serverLoop;
252 		}
253 		else if (request.kind & RequestKind.query)
254 		{
255 			s.sendResponse(AutocompleteResponse.ack);
256 			continue;
257 		}
258 
259 		if (request.kind & RequestKind.addImport)
260 		{
261 			cache.addImportPaths(request.importPaths);
262 		}
263 
264 		if (request.kind & RequestKind.listImports)
265 		{
266 			AutocompleteResponse response;
267 			response.importPaths = cache.getImportPaths().map!(a => cast() a).array();
268 			info("Returning import path list");
269 			s.sendResponse(response);
270 		}
271 		else if (request.kind & RequestKind.autocomplete)
272 		{
273 			info("Getting completions");
274 			s.sendResponse(complete(request, cache));
275 		}
276 		else if (request.kind & RequestKind.doc)
277 		{
278 			info("Getting doc comment");
279 			s.trySendResponse(getDoc(request, cache), "Could not get DDoc information");
280 		}
281 		else if (request.kind & RequestKind.symbolLocation)
282 			s.trySendResponse(findDeclaration(request, cache), "Could not get symbol location");
283 		else if (request.kind & RequestKind.search)
284 			s.sendResponse(symbolSearch(request, cache));
285 		else if (request.kind & RequestKind.localUse)
286 			s.trySendResponse(findLocalUse(request, cache), "Couldnot find local usage");
287 
288 		sw.stop();
289 		info("Request processed in ", sw.peek().total!"msecs"(), " milliseconds");
290 	}
291 	return 0;
292 }
293 
294 /// Lazily evaluates a response with an exception handler and sends it to a socket or logs msg if evaluating response fails.
295 void trySendResponse(Socket socket, lazy AutocompleteResponse response, lazy string msg)
296 {
297 	try
298 	{
299 		sendResponse(socket, response);
300 	}
301 	catch (Exception e)
302 	{
303 		warningf("%s: %s", msg, e.msg);
304 	}
305 }
306 
307 /// Packs an AutocompleteResponse and sends it to a socket.
308 void sendResponse(Socket socket, AutocompleteResponse response)
309 {
310 	ubyte[] responseBytes = msgpack.pack(response);
311 	socket.send(responseBytes);
312 }
313 
314 /// IP v4 address as bytes and a uint
315 union IPv4Union
316 {
317 	/// the bytes
318 	ubyte[4] b;
319 	/// the uint
320 	uint i;
321 }
322 
323 import std.regex : ctRegex;
324 alias envVarRegex = ctRegex!(`\$\{([_a-zA-Z][_a-zA-Z 0-9]*)\}`);
325 
326 private unittest
327 {
328 	import std.regex : replaceAll;
329 
330 	enum input = `${HOME}/aaa/${_bb_b}/ccc`;
331 
332 	assert(replaceAll!(m => m[1])(input, envVarRegex) == `HOME/aaa/_bb_b/ccc`);
333 }
334 
335 /**
336  * Implements the --help switch.
337  */
338 void printHelp(string programName)
339 {
340     writefln(
341 `
342     Usage: %s options
343 
344 options:
345     -I PATH
346         Includes PATH in the listing of paths that are searched for file
347         imports.
348 
349     --help | -h
350         Prints this help message.
351 
352     --version
353         Prints the version number and then exits.
354 
355     --port PORTNUMBER | -pPORTNUMBER
356         Listens on PORTNUMBER instead of the default port 9166 when TCP sockets
357         are used.
358 
359     --logLevel LEVEL
360         The logging level. Valid values are 'all', 'trace', 'info', 'warning',
361         'error', 'critical', 'fatal', and 'off'.
362 
363     --tcp
364         Listen on a TCP socket instead of a UNIX domain socket. This switch
365         has no effect on Windows.
366 
367     --socketFile FILENAME
368         Use the given FILENAME as the path to the UNIX domain socket. Using
369         this switch is an error on Windows.`, programName);
370 }