1 module workspaced.com.dcd; 2 3 import std.file : tempDir; 4 5 import std.path; 6 import std.json; 7 import std.conv; 8 import std.stdio; 9 import std.string; 10 import std.random; 11 import std.process; 12 import std.datetime; 13 import std.algorithm; 14 import core.thread; 15 16 import painlessjson; 17 18 import workspaced.api; 19 20 version (OSX) version = haveUnixSockets; 21 version (linux) version = haveUnixSockets; 22 version (BSD) version = haveUnixSockets; 23 version (FreeBSD) version = haveUnixSockets; 24 25 @component("dcd") : 26 /// Load function for dcd. Call with `{"cmd": "load", "components": ["dcd"]}` 27 /// This will start dcd-server and load all import paths specified by previously loaded modules such as dub if autoStart is true. 28 /// It also checks for the version. All dcd methods are used with `"cmd": "dcd"` 29 /// Note: This will block any incoming requests while loading. 30 @load void start(string dir, string clientPath = "dcd-client", 31 string serverPath = "dcd-server", ushort port = 9166, bool autoStart = true) 32 { 33 .cwd = dir; 34 .serverPath = serverPath; 35 .clientPath = clientPath; 36 .port = port; 37 installedVersion = .clientPath.getVersionAndFixPath; 38 if (.serverPath.getVersionAndFixPath != installedVersion) 39 throw new Exception("client & server version mismatch"); 40 version (haveUnixSockets) 41 hasUnixDomainSockets = supportsUnixDomainSockets(installedVersion); 42 if (autoStart) 43 startServer(); 44 if (!checkVersion(installedVersion, [0, 9, 0])) 45 broadcast(JSONValue([ 46 "type": JSONValue("outdated"), 47 "component": JSONValue("dcd") 48 ])); 49 } 50 51 bool supportsUnixDomainSockets(string ver) 52 { 53 return checkVersion(ver, [0, 8, 0]); 54 } 55 56 unittest 57 { 58 assert(supportsUnixDomainSockets("0.8.0-beta2+9ec55f40a26f6bb3ca95dc9232a239df6ed25c37")); 59 assert(!supportsUnixDomainSockets("0.7.9-beta3")); 60 assert(!supportsUnixDomainSockets("0.7.0")); 61 assert(supportsUnixDomainSockets("1.0.0")); 62 } 63 64 /// This stops the dcd-server instance safely and waits for it to exit 65 @unload void stop() 66 { 67 stopServerSync(); 68 Thread.sleep(100.msecs); 69 killServer(); 70 } 71 72 /// This will start the dcd-server and load import paths from the current provider 73 /// Call_With: `{"subcmd": "setup-server"}` 74 @arguments("subcmd", "setup-server") 75 void setupServer(string[] additionalImports = []) 76 { 77 startServer(importPathProvider() ~ additionalImports); 78 } 79 80 /// This will start the dcd-server 81 /// Call_With: `{"subcmd": "start-server"}` 82 @arguments("subcmd", "start-server") 83 void startServer(string[] additionalImports = []) 84 { 85 if (isPortRunning(port)) 86 throw new Exception("Already running dcd on port " ~ port.to!string); 87 string[] imports; 88 foreach (i; additionalImports) 89 imports ~= "-I" ~ i; 90 .runningPort = port; 91 .socketFile = buildPath(tempDir, "workspace-d-sock" ~ thisProcessID.to!string(36)); 92 serverPipes = raw([serverPath] ~ clientArgs ~ imports, 93 Redirect.stdin | Redirect.stderr | Redirect.stdoutToStderr); 94 while (!serverPipes.stderr.eof) 95 { 96 string line = serverPipes.stderr.readln(); 97 stderr.writeln("Server: ", line); 98 stderr.flush(); 99 if (line.canFind(" Startup completed in ")) 100 break; 101 } 102 new Thread({ 103 while (!serverPipes.stderr.eof) 104 { 105 stderr.writeln("Server: ", serverPipes.stderr.readln()); 106 } 107 stderr.writeln("DCD-Server stopped with code ", serverPipes.pid.wait()); 108 }).start(); 109 } 110 111 void stopServerSync() 112 { 113 while (!serverPipes.pid.tryWait().terminated) 114 execClient(["--shutdown"]); 115 } 116 117 /// This stops the dcd-server asynchronously 118 /// Returns: null 119 /// Call_With: `{"subcmd": "stop-server"}` 120 @async @arguments("subcmd", "stop-server") 121 void stopServer(AsyncCallback cb) 122 { 123 new Thread({ /**/ 124 try 125 { 126 stopServerSync(); 127 cb(null, JSONValue(null)); 128 } 129 catch (Throwable t) 130 { 131 cb(t, JSONValue(null)); 132 } 133 }).start(); 134 } 135 136 /// This will kill the process associated with the dcd-server instance 137 /// Call_With: `{"subcmd": "kill-server"}` 138 @arguments("subcmd", "kill-server") 139 void killServer() 140 { 141 if (!serverPipes.pid.tryWait().terminated) 142 serverPipes.pid.kill(); 143 } 144 145 /// This will stop the dcd-server safely and restart it again using setup-server asynchronously 146 /// Returns: null 147 /// Call_With: `{"subcmd": "restart-server"}` 148 @async @arguments("subcmd", "restart-server") 149 void restartServer(AsyncCallback cb) 150 { 151 new Thread({ /**/ 152 try 153 { 154 stopServerSync(); 155 setupServer(); 156 cb(null, JSONValue(null)); 157 } 158 catch (Throwable t) 159 { 160 cb(t, JSONValue(null)); 161 } 162 }).start(); 163 } 164 165 /// This will query the current dcd-server status 166 /// Returns: `{isRunning: bool}` If the dcd-server process is not running anymore it will return isRunning: false. Otherwise it will check for server status using `dcd-client --query` 167 /// Call_With: `{"subcmd": "status"}` 168 @arguments("subcmd", "status") 169 auto serverStatus() @property 170 { 171 DCDServerStatus status; 172 if (serverPipes.pid && serverPipes.pid.tryWait().terminated) 173 status.isRunning = false; 174 else if (hasUnixDomainSockets) 175 status.isRunning = true; 176 else 177 status.isRunning = isPortRunning(runningPort); 178 return status; 179 } 180 181 /// Searches for a symbol across all files using `dcd-client --search` 182 /// Returns: `[{file: string, position: int, type: string}]` 183 /// Call_With: `{"subcmd": "search-symbol"}` 184 @arguments("subcmd", "search-symbol") 185 @async auto searchSymbol(AsyncCallback cb, string query) 186 { 187 new Thread({ 188 try 189 { 190 auto pipes = doClient(["--search", query]); 191 scope (exit) 192 { 193 pipes.pid.wait(); 194 pipes.destroy(); 195 } 196 pipes.stdin.close(); 197 DCDSearchResult[] results; 198 while (pipes.stdout.isOpen && !pipes.stdout.eof) 199 { 200 string line = pipes.stdout.readln(); 201 if (line.length == 0) 202 continue; 203 string[] splits = line.chomp.split('\t'); 204 results ~= DCDSearchResult(splits[0], splits[2].to!int, splits[1]); 205 } 206 cb(null, results.toJSON); 207 } 208 catch (Throwable t) 209 { 210 cb(t, JSONValue(null)); 211 } 212 }).start(); 213 } 214 215 /// Reloads import paths from the current provider. Call reload there before calling it here. 216 /// Call_With: `{"subcmd": "refresh-imports"}` 217 @arguments("subcmd", "refresh-imports") 218 void refreshImports() 219 { 220 addImports(importPathProvider()); 221 } 222 223 /// Manually adds import paths as string array 224 /// Call_With: `{"subcmd": "add-imports"}` 225 @arguments("subcmd", "add-imports") 226 void addImports(string[] imports) 227 { 228 knownImports ~= imports; 229 updateImports(); 230 } 231 232 /// Searches for an open port to spawn dcd-server in asynchronously starting with `port`, always increasing by one. 233 /// Returns: null if not available, otherwise the port as number 234 /// Call_With: `{"subcmd": "find-and-select-port"}` 235 @arguments("subcmd", "find-and-select-port") 236 @async void findAndSelectPort(AsyncCallback cb, ushort port = 9166) 237 { 238 if (hasUnixDomainSockets) 239 { 240 cb(null, JSONValue(null)); 241 return; 242 } 243 new Thread({ /**/ 244 try 245 { 246 auto newPort = findOpen(port); 247 .port = newPort; 248 cb(null, .port.toJSON()); 249 } 250 catch (Throwable t) 251 { 252 cb(t, JSONValue(null)); 253 } 254 }).start(); 255 } 256 257 /// Finds the declaration of the symbol at position `pos` in the code 258 /// Returns: `[0: file: string, 1: position: int]` 259 /// Call_With: `{"subcmd": "find-declaration"}` 260 @arguments("subcmd", "find-declaration") 261 @async void findDeclaration(AsyncCallback cb, string code, int pos) 262 { 263 new Thread({ 264 try 265 { 266 auto pipes = doClient(["-c", pos.to!string, "--symbolLocation"]); 267 scope (exit) 268 { 269 pipes.pid.wait(); 270 pipes.destroy(); 271 } 272 pipes.stdin.write(code); 273 pipes.stdin.close(); 274 string line = pipes.stdout.readln(); 275 if (line.length == 0) 276 { 277 cb(null, JSONValue(null)); 278 return; 279 } 280 string[] splits = line.chomp.split('\t'); 281 if (splits.length != 2) 282 { 283 cb(null, JSONValue(null)); 284 return; 285 } 286 cb(null, JSONValue([JSONValue(splits[0]), JSONValue(splits[1].to!int)])); 287 } 288 catch (Throwable t) 289 { 290 cb(t, JSONValue(null)); 291 } 292 }).start(); 293 } 294 295 /// Finds the documentation of the symbol at position `pos` in the code 296 /// Returns: `[string]` 297 /// Call_With: `{"subcmd": "get-documentation"}` 298 @arguments("subcmd", "get-documentation") 299 @async void getDocumentation(AsyncCallback cb, string code, int pos) 300 { 301 new Thread({ 302 try 303 { 304 auto pipes = doClient(["--doc", "-c", pos.to!string]); 305 scope (exit) 306 { 307 pipes.pid.wait(); 308 pipes.destroy(); 309 } 310 pipes.stdin.write(code); 311 pipes.stdin.close(); 312 string data; 313 while (pipes.stdout.isOpen && !pipes.stdout.eof) 314 { 315 string line = pipes.stdout.readln(); 316 if (line.length) 317 data ~= line.chomp; 318 } 319 cb(null, JSONValue(data.replace("\\n", "\n"))); 320 } 321 catch (Throwable t) 322 { 323 cb(t, JSONValue(null)); 324 } 325 }).start(); 326 } 327 328 /// Returns the used socket file. Only available on OSX, linux and BSD with DCD >= 0.8.0 329 /// Throws an error if not available. 330 @arguments("subcmd", "get-socketfile") 331 string getSocketFile() 332 { 333 if (!hasUnixDomainSockets) 334 throw new Exception("Unix domain sockets not supported"); 335 return socketFile; 336 } 337 338 /// Returns the used running port. Throws an error if using unix sockets instead 339 @arguments("subcmd", "get-port") 340 ushort getRunningPort() 341 { 342 if (hasUnixDomainSockets) 343 throw new Exception("Using unix domain sockets instead of a port"); 344 return runningPort; 345 } 346 347 /// Queries for code completion at position `pos` in code 348 /// Returns: `{type:string}` where type is either identifiers, calltips or raw. 349 /// When identifiers: `{type:"identifiers", identifiers:[{identifier:string, type:string}]}` 350 /// When calltips: `{type:"calltips", calltips:[string]}` 351 /// When raw: `{type:"raw", raw:[string]}` 352 /// Raw is anything else than identifiers and calltips which might not be implemented by this point. 353 /// Call_With: `{"subcmd": "list-completion"}` 354 @arguments("subcmd", "list-completion") 355 @async void listCompletion(AsyncCallback cb, string code, int pos) 356 { 357 new Thread({ 358 try 359 { 360 auto pipes = doClient(["-c", pos.to!string]); 361 scope (exit) 362 { 363 pipes.pid.wait(); 364 pipes.destroy(); 365 } 366 pipes.stdin.write(code); 367 pipes.stdin.close(); 368 string[] data; 369 while (pipes.stdout.isOpen && !pipes.stdout.eof) 370 { 371 string line = pipes.stdout.readln(); 372 if (line.length == 0) 373 continue; 374 data ~= line.chomp; 375 } 376 int[] emptyArr; 377 if (data.length == 0) 378 { 379 cb(null, JSONValue(["type" : JSONValue("identifiers"), "identifiers" : emptyArr.toJSON()])); 380 return; 381 } 382 if (data[0] == "calltips") 383 { 384 cb(null, JSONValue(["type" : JSONValue("calltips"), "calltips" : data[1 .. $].toJSON()])); 385 return; 386 } 387 else if (data[0] == "identifiers") 388 { 389 DCDIdentifier[] identifiers; 390 foreach (line; data[1 .. $]) 391 { 392 string[] splits = line.split('\t'); 393 identifiers ~= DCDIdentifier(splits[0], splits[1]); 394 } 395 cb(null, JSONValue(["type" : JSONValue("identifiers"), "identifiers" 396 : identifiers.toJSON()])); 397 return; 398 } 399 else 400 { 401 cb(null, JSONValue(["type" : JSONValue("raw"), "raw" : data.toJSON()])); 402 return; 403 } 404 } 405 catch (Throwable e) 406 { 407 cb(e, JSONValue(null)); 408 } 409 }).start(); 410 } 411 412 void updateImports() 413 { 414 string[] args; 415 foreach (path; knownImports) 416 args ~= "-I" ~ path; 417 execClient(args); 418 } 419 420 private: 421 422 __gshared 423 { 424 string clientPath, serverPath, cwd; 425 string installedVersion; 426 bool hasUnixDomainSockets = false; 427 ProcessPipes serverPipes; 428 ushort port, runningPort; 429 string socketFile; 430 string[] knownImports; 431 } 432 433 string[] clientArgs() 434 { 435 if (hasUnixDomainSockets) 436 return ["--socketFile", socketFile]; 437 else 438 return ["--port", runningPort.to!string]; 439 } 440 441 auto doClient(string[] args) 442 { 443 return raw([clientPath] ~ clientArgs ~ args); 444 } 445 446 auto raw(string[] args, Redirect redirect = Redirect.all) 447 { 448 return pipeProcess(args, redirect, null, Config.none, cwd); 449 } 450 451 auto execClient(string[] args) 452 { 453 return rawExec([clientPath] ~ clientArgs ~ args); 454 } 455 456 auto rawExec(string[] args) 457 { 458 return execute(args, null, Config.none, size_t.max, cwd); 459 } 460 461 bool isPortRunning(ushort port) 462 { 463 if (hasUnixDomainSockets) 464 return false; 465 auto ret = execute([clientPath, "-q", "--port", port.to!string]); 466 return ret.status == 0; 467 } 468 469 ushort findOpen(ushort port) 470 { 471 --port; 472 bool isRunning; 473 do 474 { 475 isRunning = isPortRunning(++port); 476 } 477 while (isRunning); 478 return port; 479 } 480 481 private struct DCDServerStatus 482 { 483 bool isRunning; 484 } 485 486 private struct DCDIdentifier 487 { 488 string identifier; 489 string type; 490 } 491 492 private struct DCDSearchResult 493 { 494 string file; 495 int position; 496 string type; 497 }