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 server.server;
20 
21 import core.sys.posix.sys.stat;
22 import std.algorithm;
23 import std.array;
24 import std.conv;
25 import std.datetime;
26 import std.exception : enforce;
27 import std.experimental.allocator;
28 import std.experimental.allocator.mallocator;
29 import std.experimental.logger;
30 import std.file;
31 import std.file;
32 import std.getopt;
33 import std.path;
34 import std.process;
35 import std.socket;
36 import std.stdio;
37 
38 import msgpack;
39 
40 import dsymbol.string_interning;
41 
42 import common.dcd_version;
43 import common.messages;
44 import common.socket;
45 import dsymbol.modulecache;
46 import dsymbol.symbol;
47 import server.autocomplete;
48 
49 /// Name of the server configuration file
50 enum CONFIG_FILE_NAME = "dcd.conf";
51 
52 version(linux) version = useXDG;
53 version(BSD) version = useXDG;
54 version(FreeBSD) version = useXDG;
55 version(OSX) version = useXDG;
56 
57 int main(string[] args)
58 {
59 	ushort port;
60 	bool help;
61 	bool printVersion;
62 	bool ignoreConfig;
63 	string[] importPaths;
64 	LogLevel level = globalLogLevel;
65 	version(Windows)
66 	{
67 		bool useTCP = true;
68 		string socketFile;
69 	}
70 	else
71 	{
72 		bool useTCP = false;
73 		string socketFile = generateSocketName();
74 	}
75 
76 	sharedLog.fatalHandler = () {};
77 
78 	try
79 	{
80 		getopt(args, "port|p", &port, "I", &importPaths, "help|h", &help,
81 			"version", &printVersion, "ignoreConfig", &ignoreConfig,
82 			"logLevel", &level, "tcp", &useTCP, "socketFile", &socketFile);
83 	}
84 	catch (ConvException e)
85 	{
86 		fatal(e.msg);
87 		printHelp(args[0]);
88 		return 1;
89 	}
90 
91 	globalLogLevel = level;
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 	if (help)
105 	{
106 		printHelp(args[0]);
107 		return 0;
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 	version (Windows) if (socketFile !is null)
122 	{
123 		fatal("UNIX domain sockets not supported on Windows");
124 		return 1;
125 	}
126 
127 	if (serverIsRunning(useTCP, socketFile,  port))
128 	{
129 		fatal("Another instance of DCD-server is already running");
130 		return 1;
131 	}
132 
133 	info("Starting up...");
134 	StopWatch sw = StopWatch(AutoStart.yes);
135 
136 	if (!ignoreConfig)
137 		importPaths ~= loadConfiguredImportDirs();
138 
139 	Socket socket;
140 	if (useTCP)
141 	{
142 		socket = new TcpSocket(AddressFamily.INET);
143 		socket.blocking = true;
144 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
145 		socket.bind(new InternetAddress("localhost", port));
146 		info("Listening on port ", port);
147 	}
148 	else
149 	{
150 		version(Windows)
151 		{
152 			fatal("UNIX domain sockets not supported on Windows");
153 			return 1;
154 		}
155 		else
156 		{
157 			socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
158 			if (exists(socketFile))
159 			{
160 				info("Cleaning up old socket file at ", socketFile);
161 				remove(socketFile);
162 			}
163 			socket.blocking = true;
164 			socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
165 			socket.bind(new UnixAddress(socketFile));
166 			setAttributes(socketFile, S_IRUSR | S_IWUSR);
167 			info("Listening at ", socketFile);
168 		}
169 	}
170 	socket.listen(0);
171 
172 	scope (exit)
173 	{
174 		info("Shutting down sockets...");
175 		socket.shutdown(SocketShutdown.BOTH);
176 		socket.close();
177 		if (!useTCP)
178 			remove(socketFile);
179 		info("Sockets shut down.");
180 	}
181 
182 	ModuleCache cache = ModuleCache(new ASTAllocator);
183 	cache.addImportPaths(importPaths);
184 	infof("Import directories:\n    %-(%s\n    %)", cache.getImportPaths());
185 
186 	ubyte[] buffer = cast(ubyte[]) Mallocator.instance.allocate(1024 * 1024 * 4); // 4 megabytes should be enough for anybody...
187 	scope(exit) Mallocator.instance.deallocate(buffer);
188 
189 	sw.stop();
190 	info(cache.symbolsAllocated, " symbols cached.");
191 	info("Startup completed in ", sw.peek().to!("msecs", float), " milliseconds.");
192 
193 	// No relative paths
194 	version (Posix) chdir("/");
195 
196 	version (LittleEndian)
197 		immutable expectedClient = IPv4Union([1, 0, 0, 127]);
198 	else
199 		immutable expectedClient = IPv4Union([127, 0, 0, 1]);
200 
201 	serverLoop: while (true)
202 	{
203 		auto s = socket.accept();
204 		s.blocking = true;
205 
206 		if (useTCP)
207 		{
208 			// Only accept connections from localhost
209 			IPv4Union actual;
210 			InternetAddress clientAddr = cast(InternetAddress) s.remoteAddress();
211 			actual.i = clientAddr.addr;
212 			// Shut down if somebody tries connecting from outside
213 			if (actual.i != expectedClient.i)
214 			{
215 				fatal("Connection attempted from ", clientAddr.toAddrString());
216 				return 1;
217 			}
218 		}
219 
220 		scope (exit)
221 		{
222 			s.shutdown(SocketShutdown.BOTH);
223 			s.close();
224 		}
225 		ptrdiff_t bytesReceived = s.receive(buffer);
226 
227 		auto requestWatch = StopWatch(AutoStart.yes);
228 
229 		size_t messageLength;
230 		// bit magic!
231 		(cast(ubyte*) &messageLength)[0..size_t.sizeof] = buffer[0..size_t.sizeof];
232 		while (bytesReceived < messageLength + size_t.sizeof)
233 		{
234 			immutable b = s.receive(buffer[bytesReceived .. $]);
235 			if (b == Socket.ERROR)
236 			{
237 				bytesReceived = Socket.ERROR;
238 				break;
239 			}
240 			bytesReceived += b;
241 		}
242 
243 		if (bytesReceived == Socket.ERROR)
244 		{
245 			warning("Socket recieve failed");
246 			break;
247 		}
248 
249 		AutocompleteRequest request;
250 		msgpack.unpack(buffer[size_t.sizeof .. bytesReceived], request);
251 		if (request.kind & RequestKind.clearCache)
252 		{
253 			info("Clearing cache.");
254 			cache.clear();
255 		}
256 		else if (request.kind & RequestKind.shutdown)
257 		{
258 			info("Shutting down.");
259 			break serverLoop;
260 		}
261 		else if (request.kind & RequestKind.query)
262 		{
263 			AutocompleteResponse response;
264 			response.completionType = "ack";
265 			ubyte[] responseBytes = msgpack.pack(response);
266 			s.send(responseBytes);
267 			continue;
268 		}
269 		if (request.kind & RequestKind.addImport)
270 		{
271 			cache.addImportPaths(request.importPaths);
272 		}
273 		if (request.kind & RequestKind.listImports)
274 		{
275 			AutocompleteResponse response;
276 			response.importPaths = cache.getImportPaths().array();
277 			ubyte[] responseBytes = msgpack.pack(response);
278 			info("Returning import path list");
279 			s.send(responseBytes);
280 		}
281 		else if (request.kind & RequestKind.autocomplete)
282 		{
283 			info("Getting completions");
284 			AutocompleteResponse response = complete(request, cache);
285 			ubyte[] responseBytes = msgpack.pack(response);
286 			s.send(responseBytes);
287 		}
288 		else if (request.kind & RequestKind.doc)
289 		{
290 			info("Getting doc comment");
291 			try
292 			{
293 				AutocompleteResponse response = getDoc(request, cache);
294 				ubyte[] responseBytes = msgpack.pack(response);
295 				s.send(responseBytes);
296 			}
297 			catch (Exception e)
298 			{
299 				warning("Could not get DDoc information", e.msg);
300 			}
301 		}
302 		else if (request.kind & RequestKind.symbolLocation)
303 		{
304 			try
305 			{
306 				AutocompleteResponse response = findDeclaration(request, cache);
307 				ubyte[] responseBytes = msgpack.pack(response);
308 				s.send(responseBytes);
309 			}
310 			catch (Exception e)
311 			{
312 				warning("Could not get symbol location", e.msg);
313 			}
314 		}
315 		else if (request.kind & RequestKind.search)
316 		{
317 			AutocompleteResponse response = symbolSearch(request, cache);
318 			ubyte[] responseBytes = msgpack.pack(response);
319 			s.send(responseBytes);
320 		}
321 		info("Request processed in ", requestWatch.peek().to!("msecs", float), " milliseconds");
322 	}
323 	return 0;
324 }
325 
326 /**
327  * Locates the configuration file
328  */
329 string getConfigurationLocation()
330 {
331 	version (useXDG)
332 	{
333 		string configDir = environment.get("XDG_CONFIG_HOME", null);
334 		if (configDir is null)
335 		{
336 			configDir = environment.get("HOME", null);
337 			if (configDir !is null)
338 				configDir = buildPath(configDir, ".config", "dcd", CONFIG_FILE_NAME);
339 			if (!exists(configDir))
340 				configDir = buildPath("/etc/", CONFIG_FILE_NAME);
341 		}
342 		else
343 		{
344 			configDir = buildPath(configDir, "dcd", CONFIG_FILE_NAME);
345 		}
346 		return configDir;
347 	}
348 	else version(Windows)
349 	{
350 		return CONFIG_FILE_NAME;
351 	}
352 }
353 
354 /// IP v4 address as bytes and a uint
355 union IPv4Union
356 {
357 	/// the bytes
358 	ubyte[4] b;
359 	/// the uint
360 	uint i;
361 }
362 
363 /**
364  * Prints a warning message to the user when an old config file is detected.
365  */
366 void warnAboutOldConfigLocation()
367 {
368 	version (linux) if ("~/.config/dcd".expandTilde().exists()
369 		&& "~/.config/dcd".expandTilde().isFile())
370 	{
371 		warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
372 		warning("!! Upgrade warning:");
373 		warning("!! '~/.config/dcd' should be moved to '$XDG_CONFIG_HOME/dcd/dcd.conf'");
374 		warning("!! or '$HOME/.config/dcd/dcd.conf'");
375 		warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
376 	}
377 }
378 
379 /**
380  * Loads import directories from the configuration file
381  */
382 string[] loadConfiguredImportDirs()
383 {
384 	warnAboutOldConfigLocation();
385 	immutable string configLocation = getConfigurationLocation();
386 	if (!configLocation.exists())
387 		return [];
388 	info("Loading configuration from ", configLocation);
389 	File f = File(configLocation, "rt");
390 	return f.byLine(KeepTerminator.no)
391 		.filter!(a => a.length > 0 && a[0] != '#' && existanceCheck(a))
392 		.map!(a => a.idup)
393 		.array();
394 }
395 
396 /**
397  * Implements the --help switch.
398  */
399 void printHelp(string programName)
400 {
401     writefln(
402 `
403     Usage: %s options
404 
405 options:
406     -I PATH
407         Includes PATH in the listing of paths that are searched for file
408         imports.
409 
410     --help | -h
411         Prints this help message.
412 
413     --version
414         Prints the version number and then exits.
415 
416     --port PORTNUMBER | -pPORTNUMBER
417         Listens on PORTNUMBER instead of the default port 9166 when TCP sockets
418         are used.
419 
420     --logLevel LEVEL
421         The logging level. Valid values are 'all', 'trace', 'info', 'warning',
422         'error', 'critical', 'fatal', and 'off'.
423 
424     --tcp
425         Listen on a TCP socket instead of a UNIX domain socket. This switch
426         has no effect on Windows.
427 
428     --socketFile FILENAME
429         Use the given FILENAME as the path to the UNIX domain socket. Using
430         this switch is an error on Windows.`, programName);
431 }