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