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 }