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 dcd.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.stopwatch : AutoStart, StopWatch; 26 import std.exception : enforce; 27 import stdx.allocator; 28 import stdx.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 dcd.common.dcd_version; 40 import dcd.common.messages; 41 import dcd.common.socket; 42 import dsymbol.modulecache; 43 import dcd.server.autocomplete; 44 import dcd.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().total!"msecs"(), " 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 215 ptrdiff_t bytesReceived = s.receive(buffer); 216 217 sw.reset(); 218 sw.start(); 219 220 size_t messageLength; 221 // bit magic! 222 (cast(ubyte*) &messageLength)[0..size_t.sizeof] = buffer[0..size_t.sizeof]; 223 while (bytesReceived < messageLength + size_t.sizeof) 224 { 225 immutable b = s.receive(buffer[bytesReceived .. $]); 226 if (b == Socket.ERROR) 227 { 228 bytesReceived = Socket.ERROR; 229 break; 230 } 231 bytesReceived += b; 232 } 233 234 if (bytesReceived == Socket.ERROR) 235 { 236 warning("Socket recieve failed"); 237 break; 238 } 239 240 AutocompleteRequest request; 241 msgpack.unpack(buffer[size_t.sizeof .. bytesReceived], request); 242 243 if (request.kind & RequestKind.clearCache) 244 { 245 info("Clearing cache."); 246 cache.clear(); 247 } 248 else if (request.kind & RequestKind.shutdown) 249 { 250 info("Shutting down."); 251 break serverLoop; 252 } 253 else if (request.kind & RequestKind.query) 254 { 255 s.sendResponse(AutocompleteResponse.ack); 256 continue; 257 } 258 259 if (request.kind & RequestKind.addImport) 260 { 261 cache.addImportPaths(request.importPaths); 262 } 263 264 if (request.kind & RequestKind.listImports) 265 { 266 AutocompleteResponse response; 267 response.importPaths = cache.getImportPaths().map!(a => cast() a).array(); 268 info("Returning import path list"); 269 s.sendResponse(response); 270 } 271 else if (request.kind & RequestKind.autocomplete) 272 { 273 info("Getting completions"); 274 s.sendResponse(complete(request, cache)); 275 } 276 else if (request.kind & RequestKind.doc) 277 { 278 info("Getting doc comment"); 279 s.trySendResponse(getDoc(request, cache), "Could not get DDoc information"); 280 } 281 else if (request.kind & RequestKind.symbolLocation) 282 s.trySendResponse(findDeclaration(request, cache), "Could not get symbol location"); 283 else if (request.kind & RequestKind.search) 284 s.sendResponse(symbolSearch(request, cache)); 285 else if (request.kind & RequestKind.localUse) 286 s.trySendResponse(findLocalUse(request, cache), "Couldnot find local usage"); 287 288 sw.stop(); 289 info("Request processed in ", sw.peek().total!"msecs"(), " milliseconds"); 290 } 291 return 0; 292 } 293 294 /// Lazily evaluates a response with an exception handler and sends it to a socket or logs msg if evaluating response fails. 295 void trySendResponse(Socket socket, lazy AutocompleteResponse response, lazy string msg) 296 { 297 try 298 { 299 sendResponse(socket, response); 300 } 301 catch (Exception e) 302 { 303 warningf("%s: %s", msg, e.msg); 304 } 305 } 306 307 /// Packs an AutocompleteResponse and sends it to a socket. 308 void sendResponse(Socket socket, AutocompleteResponse response) 309 { 310 ubyte[] responseBytes = msgpack.pack(response); 311 socket.send(responseBytes); 312 } 313 314 /// IP v4 address as bytes and a uint 315 union IPv4Union 316 { 317 /// the bytes 318 ubyte[4] b; 319 /// the uint 320 uint i; 321 } 322 323 import std.regex : ctRegex; 324 alias envVarRegex = ctRegex!(`\$\{([_a-zA-Z][_a-zA-Z 0-9]*)\}`); 325 326 private unittest 327 { 328 import std.regex : replaceAll; 329 330 enum input = `${HOME}/aaa/${_bb_b}/ccc`; 331 332 assert(replaceAll!(m => m[1])(input, envVarRegex) == `HOME/aaa/_bb_b/ccc`); 333 } 334 335 /** 336 * Implements the --help switch. 337 */ 338 void printHelp(string programName) 339 { 340 writefln( 341 ` 342 Usage: %s options 343 344 options: 345 -I PATH 346 Includes PATH in the listing of paths that are searched for file 347 imports. 348 349 --help | -h 350 Prints this help message. 351 352 --version 353 Prints the version number and then exits. 354 355 --port PORTNUMBER | -pPORTNUMBER 356 Listens on PORTNUMBER instead of the default port 9166 when TCP sockets 357 are used. 358 359 --logLevel LEVEL 360 The logging level. Valid values are 'all', 'trace', 'info', 'warning', 361 'error', 'critical', 'fatal', and 'off'. 362 363 --tcp 364 Listen on a TCP socket instead of a UNIX domain socket. This switch 365 has no effect on Windows. 366 367 --socketFile FILENAME 368 Use the given FILENAME as the path to the UNIX domain socket. Using 369 this switch is an error on Windows.`, programName); 370 }