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.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;
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.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 common.dcd_version;
40 import common.messages;
41 import common.socket;
42 import dsymbol.modulecache;
43 import server.autocomplete;
44 import 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().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().map!(a => cast() a).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         else if (request.kind & RequestKind.localUse)
311         {
312             try
313             {
314                 AutocompleteResponse response = findLocalUse(request, cache);
315                 ubyte[] responseBytes = msgpack.pack(response);
316                 s.send(responseBytes);
317             }
318             catch (Exception e)
319             {
320                 warning("Could not find local usage", e.msg);
321             }
322         }
323 		info("Request processed in ", requestWatch.peek().to!("msecs", float), " milliseconds");
324 	}
325 	return 0;
326 }
327 
328 /// IP v4 address as bytes and a uint
329 union IPv4Union
330 {
331 	/// the bytes
332 	ubyte[4] b;
333 	/// the uint
334 	uint i;
335 }
336 
337 import std.regex : ctRegex;
338 alias envVarRegex = ctRegex!(`\$\{([_a-zA-Z][_a-zA-Z 0-9]*)\}`);
339 
340 private unittest
341 {
342 	import std.regex : replaceAll;
343 
344 	enum input = `${HOME}/aaa/${_bb_b}/ccc`;
345 
346 	assert(replaceAll!(m => m[1])(input, envVarRegex) == `HOME/aaa/_bb_b/ccc`);
347 }
348 
349 /**
350  * Implements the --help switch.
351  */
352 void printHelp(string programName)
353 {
354     writefln(
355 `
356     Usage: %s options
357 
358 options:
359     -I PATH
360         Includes PATH in the listing of paths that are searched for file
361         imports.
362 
363     --help | -h
364         Prints this help message.
365 
366     --version
367         Prints the version number and then exits.
368 
369     --port PORTNUMBER | -pPORTNUMBER
370         Listens on PORTNUMBER instead of the default port 9166 when TCP sockets
371         are used.
372 
373     --logLevel LEVEL
374         The logging level. Valid values are 'all', 'trace', 'info', 'warning',
375         'error', 'critical', 'fatal', and 'off'.
376 
377     --tcp
378         Listen on a TCP socket instead of a UNIX domain socket. This switch
379         has no effect on Windows.
380 
381     --socketFile FILENAME
382         Use the given FILENAME as the path to the UNIX domain socket. Using
383         this switch is an error on Windows.`, programName);
384 }