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 }