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