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 }