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 }