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 = 9166;
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 	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(0);
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().to!("msecs", float), " 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 		ptrdiff_t bytesReceived = s.receive(buffer);
215 
216 		auto requestWatch = StopWatch(AutoStart.yes);
217 
218 		size_t messageLength;
219 		// bit magic!
220 		(cast(ubyte*) &messageLength)[0..size_t.sizeof] = buffer[0..size_t.sizeof];
221 		while (bytesReceived < messageLength + size_t.sizeof)
222 		{
223 			immutable b = s.receive(buffer[bytesReceived .. $]);
224 			if (b == Socket.ERROR)
225 			{
226 				bytesReceived = Socket.ERROR;
227 				break;
228 			}
229 			bytesReceived += b;
230 		}
231 
232 		if (bytesReceived == Socket.ERROR)
233 		{
234 			warning("Socket recieve failed");
235 			break;
236 		}
237 
238 		AutocompleteRequest request;
239 		msgpack.unpack(buffer[size_t.sizeof .. bytesReceived], request);
240 		if (request.kind & RequestKind.clearCache)
241 		{
242 			info("Clearing cache.");
243 			cache.clear();
244 		}
245 		else if (request.kind & RequestKind.shutdown)
246 		{
247 			info("Shutting down.");
248 			break serverLoop;
249 		}
250 		else if (request.kind & RequestKind.query)
251 		{
252 			AutocompleteResponse response;
253 			response.completionType = "ack";
254 			ubyte[] responseBytes = msgpack.pack(response);
255 			s.send(responseBytes);
256 			continue;
257 		}
258 		if (request.kind & RequestKind.addImport)
259 		{
260 			cache.addImportPaths(request.importPaths);
261 		}
262 		if (request.kind & RequestKind.listImports)
263 		{
264 			AutocompleteResponse response;
265 			response.importPaths = cache.getImportPaths().array();
266 			ubyte[] responseBytes = msgpack.pack(response);
267 			info("Returning import path list");
268 			s.send(responseBytes);
269 		}
270 		else if (request.kind & RequestKind.autocomplete)
271 		{
272 			info("Getting completions");
273 			AutocompleteResponse response = complete(request, cache);
274 			ubyte[] responseBytes = msgpack.pack(response);
275 			s.send(responseBytes);
276 		}
277 		else if (request.kind & RequestKind.doc)
278 		{
279 			info("Getting doc comment");
280 			try
281 			{
282 				AutocompleteResponse response = getDoc(request, cache);
283 				ubyte[] responseBytes = msgpack.pack(response);
284 				s.send(responseBytes);
285 			}
286 			catch (Exception e)
287 			{
288 				warning("Could not get DDoc information", e.msg);
289 			}
290 		}
291 		else if (request.kind & RequestKind.symbolLocation)
292 		{
293 			try
294 			{
295 				AutocompleteResponse response = findDeclaration(request, cache);
296 				ubyte[] responseBytes = msgpack.pack(response);
297 				s.send(responseBytes);
298 			}
299 			catch (Exception e)
300 			{
301 				warning("Could not get symbol location", e.msg);
302 			}
303 		}
304 		else if (request.kind & RequestKind.search)
305 		{
306 			AutocompleteResponse response = symbolSearch(request, cache);
307 			ubyte[] responseBytes = msgpack.pack(response);
308 			s.send(responseBytes);
309 		}
310 		info("Request processed in ", requestWatch.peek().to!("msecs", float), " milliseconds");
311 	}
312 	return 0;
313 }
314 
315 /**
316  * Locates the configuration file
317  */
318 string getConfigurationLocation()
319 {
320 	version (useXDG)
321 	{
322 		string configDir = environment.get("XDG_CONFIG_HOME", null);
323 		if (configDir is null)
324 		{
325 			configDir = environment.get("HOME", null);
326 			if (configDir !is null)
327 				configDir = buildPath(configDir, ".config", "dcd", CONFIG_FILE_NAME);
328 			if (!exists(configDir))
329 				configDir = buildPath("/etc/", CONFIG_FILE_NAME);
330 		}
331 		else
332 		{
333 			configDir = buildPath(configDir, "dcd", CONFIG_FILE_NAME);
334 		}
335 		return configDir;
336 	}
337 	else version(Windows)
338 	{
339 		return CONFIG_FILE_NAME;
340 	}
341 }
342 
343 /// IP v4 address as bytes and a uint
344 union IPv4Union
345 {
346 	/// the bytes
347 	ubyte[4] b;
348 	/// the uint
349 	uint i;
350 }
351 
352 /**
353  * Prints a warning message to the user when an old config file is detected.
354  */
355 void warnAboutOldConfigLocation()
356 {
357 	version (linux) if ("~/.config/dcd".expandTilde().exists()
358 		&& "~/.config/dcd".expandTilde().isFile())
359 	{
360 		warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
361 		warning("!! Upgrade warning:");
362 		warning("!! '~/.config/dcd' should be moved to '$XDG_CONFIG_HOME/dcd/dcd.conf'");
363 		warning("!! or '$HOME/.config/dcd/dcd.conf'");
364 		warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
365 	}
366 }
367 
368 /**
369  * Loads import directories from the configuration file
370  */
371 string[] loadConfiguredImportDirs()
372 {
373 	warnAboutOldConfigLocation();
374 	immutable string configLocation = getConfigurationLocation();
375 	if (!configLocation.exists())
376 		return [];
377 	info("Loading configuration from ", configLocation);
378 	File f = File(configLocation, "rt");
379 	return f.byLine(KeepTerminator.no)
380 		.filter!(a => a.length > 0 && a[0] != '#' && existanceCheck(a))
381 		.map!(a => a.idup)
382 		.array();
383 }
384 
385 /**
386  * Implements the --help switch.
387  */
388 void printHelp(string programName)
389 {
390     writefln(
391 `
392     Usage: %s options
393 
394 options:
395     -I PATH
396         Includes PATH in the listing of paths that are searched for file
397         imports.
398 
399     --help | -h
400         Prints this help message.
401 
402     --version
403         Prints the version number and then exits.
404 
405     --port PORTNUMBER | -pPORTNUMBER
406         Listens on PORTNUMBER instead of the default port 9166 when TCP sockets
407         are used.
408 
409     --logLevel LEVEL
410         The logging level. Valid values are 'all', 'trace', 'info', 'warning',
411         'error', 'critical', 'fatal', and 'off'.
412 
413     --tcp
414         Listen on a TCP socket instead of a UNIX domain socket. This switch
415         has no effect on Windows.
416 
417     --socketFile FILENAME
418         Use the given FILENAME as the path to the UNIX domain socket. Using
419         this switch is an error on Windows.`, programName);
420 }