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 client.client; 20 21 import std.socket; 22 import std.stdio; 23 import std.getopt; 24 import std.array; 25 import std.process; 26 import std.algorithm; 27 import std.path; 28 import std.file; 29 import std.conv; 30 import std.string; 31 import std.experimental.logger; 32 33 import common.messages; 34 import common.dcd_version; 35 import common.socket; 36 37 int main(string[] args) 38 { 39 sharedLog.fatalHandler = () {}; 40 41 size_t cursorPos = size_t.max; 42 string[] importPaths; 43 ushort port = 9166; 44 bool help; 45 bool shutdown; 46 bool clearCache; 47 bool symbolLocation; 48 bool doc; 49 bool query; 50 bool printVersion; 51 bool listImports; 52 string search; 53 version(Windows) 54 { 55 bool useTCP = true; 56 string socketFile; 57 } 58 else 59 { 60 bool useTCP = false; 61 string socketFile = generateSocketName(); 62 } 63 64 try 65 { 66 getopt(args, "cursorPos|c", &cursorPos, "I", &importPaths, 67 "port|p", &port, "help|h", &help, "shutdown", &shutdown, 68 "clearCache", &clearCache, "symbolLocation|l", &symbolLocation, 69 "doc|d", &doc, "query|status|q", &query, "search|s", &search, 70 "version", &printVersion, "listImports", &listImports, 71 "tcp", &useTCP, "socketFile", &socketFile); 72 } 73 catch (ConvException e) 74 { 75 fatal(e.msg); 76 printHelp(args[0]); 77 return 1; 78 } 79 80 AutocompleteRequest request; 81 82 if (help) 83 { 84 printHelp(args[0]); 85 return 0; 86 } 87 88 if (printVersion) 89 { 90 version (Windows) 91 writeln(DCD_VERSION); 92 else version(built_with_dub) 93 writeln(DCD_VERSION); 94 else 95 write(DCD_VERSION, " ", GIT_HASH); 96 return 0; 97 } 98 99 version (Windows) if (socketFile !is null) 100 { 101 fatal("UNIX domain sockets not supported on Windows"); 102 return 1; 103 } 104 105 if (useTCP) 106 socketFile = null; 107 108 if (query) 109 { 110 if (serverIsRunning(useTCP, socketFile, port)) 111 { 112 writeln("Server is running"); 113 return 0; 114 } 115 else 116 { 117 writeln("Server is not running"); 118 return 1; 119 } 120 } 121 else if (shutdown || clearCache) 122 { 123 if (shutdown) 124 request.kind = RequestKind.shutdown; 125 else if (clearCache) 126 request.kind = RequestKind.clearCache; 127 Socket socket = createSocket(socketFile, port); 128 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 129 return sendRequest(socket, request) ? 0 : 1; 130 } 131 else if (importPaths.length > 0) 132 { 133 request.kind |= RequestKind.addImport; 134 request.importPaths = importPaths.map!(a => absolutePath(a)).array; 135 if (cursorPos == size_t.max) 136 { 137 Socket socket = createSocket(socketFile, port); 138 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 139 if (!sendRequest(socket, request)) 140 return 1; 141 return 0; 142 } 143 } 144 else if (listImports) 145 { 146 request.kind |= RequestKind.listImports; 147 Socket socket = createSocket(socketFile, port); 148 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 149 sendRequest(socket, request); 150 AutocompleteResponse response = getResponse(socket); 151 printImportList(response); 152 return 0; 153 } 154 else if (search == null && cursorPos == size_t.max) 155 { 156 // cursor position is a required argument 157 printHelp(args[0]); 158 return 1; 159 } 160 161 // Read in the source 162 immutable bool usingStdin = args.length <= 1; 163 string fileName = usingStdin ? "stdin" : args[1]; 164 if (!usingStdin && !exists(args[1])) 165 { 166 stderr.writefln("%s does not exist", args[1]); 167 return 1; 168 } 169 ubyte[] sourceCode; 170 if (usingStdin) 171 { 172 ubyte[4096] buf; 173 while (true) 174 { 175 auto b = stdin.rawRead(buf); 176 if (b.length == 0) 177 break; 178 sourceCode ~= b; 179 } 180 } 181 else 182 { 183 if (!exists(args[1])) 184 { 185 stderr.writeln("Could not find ", args[1]); 186 return 1; 187 } 188 File f = File(args[1]); 189 sourceCode = uninitializedArray!(ubyte[])(to!size_t(f.size)); 190 f.rawRead(sourceCode); 191 } 192 193 request.fileName = fileName; 194 request.importPaths = importPaths; 195 request.sourceCode = sourceCode; 196 request.cursorPosition = cursorPos; 197 request.searchName = search; 198 199 if (symbolLocation) 200 request.kind |= RequestKind.symbolLocation; 201 else if (doc) 202 request.kind |= RequestKind.doc; 203 else if(search) 204 request.kind |= RequestKind.search; 205 else 206 request.kind |= RequestKind.autocomplete; 207 208 // Send message to server 209 Socket socket = createSocket(socketFile, port); 210 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 211 if (!sendRequest(socket, request)) 212 return 1; 213 214 AutocompleteResponse response = getResponse(socket); 215 216 if (symbolLocation) 217 printLocationResponse(response); 218 else if (doc) 219 printDocResponse(response); 220 else if (search !is null) 221 printSearchResponse(response); 222 else 223 printCompletionResponse(response); 224 225 return 0; 226 } 227 228 private: 229 230 void printHelp(string programName) 231 { 232 writefln( 233 ` 234 Usage: %1$s [Options] [FILENAME] 235 236 A file name is optional. If it is given, autocomplete information will be 237 given for the file specified. If it is missing, input will be read from 238 stdin instead. 239 240 Source code is assumed to be UTF-8 encoded and must not exceed 4 megabytes. 241 242 Options: 243 --help | -h 244 Displays this help message 245 246 --cursorPos | -c position 247 Provides auto-completion at the given cursor position. The cursor 248 position is measured in bytes from the beginning of the source code. 249 250 --clearCache 251 Instructs the server to clear out its autocompletion cache. 252 253 --shutdown 254 Instructs the server to shut down. 255 256 --symbolLocation | -l 257 Get the file name and position that the symbol at the cursor location 258 was defined. 259 260 --doc | -d 261 Gets documentation comments associated with the symbol at the cursor 262 location. 263 264 --search | -s symbolName 265 Searches for symbolName in both stdin / the given file name as well as 266 others files cached by the server. 267 268 --query | -q | --status 269 Query the server statis. Returns 0 if the server is running. Returns 270 1 if the server could not be contacted. 271 272 -I PATH 273 Instructs the server to add PATH to its list of paths searched for 274 imported modules. 275 276 --version 277 Prints the version number and then exits. 278 279 --port PORTNUMBER | -p PORTNUMBER 280 Uses PORTNUMBER to communicate with the server instead of the default 281 port 9166. Only used on Windows or when the --tcp option is set. 282 283 --tcp 284 Send requests on a TCP socket instead of a UNIX domain socket. This 285 switch has no effect on Windows. 286 287 --socketFile FILENAME 288 Use the given FILENAME as the path to the UNIX domain socket. Using 289 this switch is an error on Windows.`, programName); 290 } 291 292 Socket createSocket(string socketFile, ushort port) 293 { 294 import core.time : dur; 295 296 Socket socket; 297 if (socketFile is null) 298 { 299 socket = new TcpSocket(AddressFamily.INET); 300 socket.connect(new InternetAddress("localhost", port)); 301 } 302 else 303 { 304 version(Windows) 305 { 306 // should never be called with non-null socketFile on Windows 307 assert(false); 308 } 309 else 310 { 311 socket = new Socket(AddressFamily.UNIX, SocketType.STREAM); 312 socket.connect(new UnixAddress(socketFile)); 313 } 314 } 315 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5)); 316 socket.blocking = true; 317 return socket; 318 } 319 320 void printDocResponse(AutocompleteResponse response) 321 { 322 import std.array: join; 323 response.docComments.join(r"\n\n").writeln; 324 } 325 326 void printLocationResponse(AutocompleteResponse response) 327 { 328 if (response.symbolFilePath is null) 329 writeln("Not found"); 330 else 331 writefln("%s\t%d", response.symbolFilePath, response.symbolLocation); 332 } 333 334 void printCompletionResponse(AutocompleteResponse response) 335 { 336 if (response.completions.length > 0) 337 { 338 writeln(response.completionType); 339 auto app = appender!(string[])(); 340 if (response.completionType == CompletionType.identifiers) 341 { 342 for (size_t i = 0; i < response.completions.length; i++) 343 app.put(format("%s\t%s", response.completions[i], response.completionKinds[i])); 344 } 345 else 346 { 347 foreach (completion; response.completions) 348 { 349 app.put(completion); 350 } 351 } 352 // Deduplicate overloaded methods 353 foreach (line; app.data.sort().uniq) 354 writeln(line); 355 } 356 } 357 358 void printSearchResponse(const AutocompleteResponse response) 359 { 360 foreach(i; 0 .. response.completions.length) 361 { 362 writefln("%s\t%s\t%s", response.completions[i], response.completionKinds[i], 363 response.locations[i]); 364 } 365 } 366 367 void printImportList(const AutocompleteResponse response) 368 { 369 import std.algorithm.iteration : each; 370 371 response.importPaths.each!(a => writeln(a)); 372 }