1 module workspaced.com.dcd;
2 
3 import std.file : tempDir;
4 
5 import core.thread;
6 import std.algorithm;
7 import std.conv;
8 import std.datetime;
9 import std.json;
10 import std.path;
11 import std.process;
12 import std.random;
13 import std.stdio;
14 import std.string;
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 class DCDComponent : ComponentWrapper
27 {
28 	mixin DefaultComponentWrapper;
29 
30 	enum latestKnownVersion = [0, 9, 8];
31 	void load()
32 	{
33 		string clientPath = this.clientPath;
34 		string serverPath = this.serverPath;
35 
36 		installedVersion = clientPath.getVersionAndFixPath;
37 		stderr.writeln("Detected dcd-client ", installedVersion);
38 		if (serverPath.getVersionAndFixPath != installedVersion)
39 			throw new Exception("client & server version mismatch");
40 
41 		config.set("dcd", "clientPath", clientPath);
42 		config.set("dcd", "serverPath", serverPath);
43 
44 		assert(this.clientPath == clientPath);
45 		assert(this.serverPath == serverPath);
46 
47 		version (haveUnixSockets)
48 			hasUnixDomainSockets = supportsUnixDomainSockets(installedVersion);
49 
50 		//dfmt off
51 		if (isOutdated)
52 			workspaced.broadcast(refInstance, JSONValue([
53 				"type": JSONValue("outdated"),
54 				"component": JSONValue("dcd")
55 			]));
56 		//dfmt on
57 		supportsFullOutput = rawExec([clientPath, "--help"]).output.canFind("--extended");
58 	}
59 
60 	/// Returns: true if DCD version is less than latestKnownVersion or if server and client mismatch or if it doesn't exist.
61 	bool isOutdated()
62 	{
63 		if (!installedVersion)
64 		{
65 			string clientPath = this.clientPath;
66 			string serverPath = this.serverPath;
67 
68 			try
69 			{
70 				installedVersion = clientPath.getVersionAndFixPath;
71 				if (serverPath.getVersionAndFixPath != installedVersion)
72 					return true;
73 			}
74 			catch (ProcessException)
75 			{
76 				return true;
77 			}
78 		}
79 		return !checkVersion(installedVersion, latestKnownVersion);
80 	}
81 
82 	/// Returns: the current detected installed version of dcd-client.
83 	string clientInstalledVersion() @property const
84 	{
85 		return installedVersion;
86 	}
87 
88 	~this()
89 	{
90 		shutdown();
91 	}
92 
93 	/// This stops the dcd-server instance safely and waits for it to exit
94 	override void shutdown()
95 	{
96 		stopServerSync();
97 	}
98 
99 	/// This will start the dcd-server and load import paths from the current provider
100 	void setupServer(string[] additionalImports = [])
101 	{
102 		startServer(importPaths ~ importFiles ~ additionalImports);
103 	}
104 
105 	/// This will start the dcd-server
106 	void startServer(string[] additionalImports = [])
107 	{
108 		if (isPortRunning(port))
109 			throw new Exception("Already running dcd on port " ~ port.to!string);
110 		string[] imports;
111 		foreach (i; additionalImports)
112 			if (i.length)
113 				imports ~= "-I" ~ i;
114 		this.runningPort = port;
115 		this.socketFile = buildPath(tempDir, "workspace-d-sock" ~ thisProcessID.to!string ~ "-" ~ uniform!ulong.to!string(36));
116 		serverPipes = raw([serverPath] ~ clientArgs ~ imports,
117 				Redirect.stdin | Redirect.stderr | Redirect.stdoutToStderr);
118 		while (!serverPipes.stderr.eof)
119 		{
120 			string line = serverPipes.stderr.readln();
121 			stderr.writeln("Server: ", line);
122 			stderr.flush();
123 			if (line.canFind("Startup completed in "))
124 				break;
125 		}
126 		running = true;
127 		new Thread({
128 			while (!serverPipes.stderr.eof)
129 			{
130 				stderr.writeln("Server: ", serverPipes.stderr.readln());
131 			}
132 			auto code = serverPipes.pid.wait();
133 			stderr.writeln("DCD-Server stopped with code ", code);
134 			if (code != 0)
135 			{
136 				stderr.writeln("Broadcasting dcd server crash.");
137 				workspaced.broadcast(refInstance, JSONValue(["type"
138 					: JSONValue("crash"), "component" : JSONValue("dcd")]));
139 				running = false;
140 			}
141 		}).start();
142 	}
143 
144 	void stopServerSync()
145 	{
146 		if (!running || serverPipes.pid.tryWait().terminated)
147 			return;
148 		int i = 0;
149 		running = false;
150 		doClient(["--shutdown"]).pid.wait;
151 		while (!serverPipes.pid.tryWait().terminated)
152 		{
153 			Thread.sleep(10.msecs);
154 			if (++i > 200) // Kill after 2 seconds
155 			{
156 				killServer();
157 				return;
158 			}
159 		}
160 	}
161 
162 	/// This stops the dcd-server asynchronously
163 	/// Returns: null
164 	Future!void stopServer()
165 	{
166 		auto ret = new Future!void();
167 		new Thread({ /**/
168 			try
169 			{
170 				stopServerSync();
171 				ret.finish();
172 			}
173 			catch (Throwable t)
174 			{
175 				ret.error(t);
176 			}
177 		}).start();
178 		return ret;
179 	}
180 
181 	/// This will kill the process associated with the dcd-server instance
182 	void killServer()
183 	{
184 		if (!serverPipes.pid.tryWait().terminated)
185 			serverPipes.pid.kill();
186 	}
187 
188 	/// This will stop the dcd-server safely and restart it again using setup-server asynchronously
189 	/// Returns: null
190 	Future!void restartServer()
191 	{
192 		auto ret = new Future!void;
193 		new Thread({ /**/
194 			try
195 			{
196 				stopServerSync();
197 				setupServer();
198 				ret.finish();
199 			}
200 			catch (Throwable t)
201 			{
202 				ret.error(t);
203 			}
204 		}).start();
205 		return ret;
206 	}
207 
208 	/// This will query the current dcd-server status
209 	/// 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`
210 	auto serverStatus() @property
211 	{
212 		DCDServerStatus status;
213 		if (serverPipes.pid && serverPipes.pid.tryWait().terminated)
214 			status.isRunning = false;
215 		else if (hasUnixDomainSockets)
216 			status.isRunning = true;
217 		else
218 			status.isRunning = isPortRunning(runningPort);
219 		return status;
220 	}
221 
222 	/// Searches for a symbol across all files using `dcd-client --search`
223 	Future!(DCDSearchResult[]) searchSymbol(string query)
224 	{
225 		auto ret = new Future!(DCDSearchResult[]);
226 		new Thread({
227 			try
228 			{
229 				if (!running)
230 				{
231 					ret.finish(null);
232 					return;
233 				}
234 				auto pipes = doClient(["--search", query]);
235 				scope (exit)
236 				{
237 					pipes.pid.wait();
238 					pipes.destroy();
239 				}
240 				pipes.stdin.close();
241 				DCDSearchResult[] results;
242 				while (pipes.stdout.isOpen && !pipes.stdout.eof)
243 				{
244 					string line = pipes.stdout.readln();
245 					if (line.length == 0)
246 						continue;
247 					string[] splits = line.chomp.split('\t');
248 					if (splits.length >= 3)
249 						results ~= DCDSearchResult(splits[0], splits[2].to!int, splits[1]);
250 				}
251 				ret.finish(results);
252 			}
253 			catch (Throwable t)
254 			{
255 				ret.error(t);
256 			}
257 		}).start();
258 		return ret;
259 	}
260 
261 	/// Reloads import paths from the current provider. Call reload there before calling it here.
262 	void refreshImports()
263 	{
264 		addImports(importPaths ~ importFiles);
265 	}
266 
267 	/// Manually adds import paths as string array
268 	void addImports(string[] imports)
269 	{
270 		knownImports ~= imports;
271 		updateImports();
272 	}
273 
274 	string clientPath() @property @ignoredFunc
275 	{
276 		return config.get("dcd", "clientPath", "dcd-client");
277 	}
278 
279 	string serverPath() @property @ignoredFunc
280 	{
281 		return config.get("dcd", "serverPath", "dcd-server");
282 	}
283 
284 	ushort port() @property @ignoredFunc
285 	{
286 		return cast(ushort) config.get!int("dcd", "port", 9166);
287 	}
288 
289 	/// Searches for an open port to spawn dcd-server in asynchronously starting with `port`, always increasing by one.
290 	/// Returns: 0 if not available, otherwise the port as number
291 	Future!ushort findAndSelectPort(ushort port = 9166)
292 	{
293 		if (hasUnixDomainSockets)
294 		{
295 			return Future!ushort.fromResult(0);
296 		}
297 		auto ret = new Future!ushort;
298 		new Thread({ /**/
299 			try
300 			{
301 				auto newPort = findOpen(port);
302 				port = newPort;
303 				ret.finish(port);
304 			}
305 			catch (Throwable t)
306 			{
307 				ret.error(t);
308 			}
309 		}).start();
310 		return ret;
311 	}
312 
313 	/// Finds the declaration of the symbol at position `pos` in the code
314 	Future!DCDDeclaration findDeclaration(string code, int pos)
315 	{
316 		auto ret = new Future!DCDDeclaration;
317 		new Thread({
318 			try
319 			{
320 				if (!running)
321 				{
322 					ret.finish(DCDDeclaration.init);
323 					return;
324 				}
325 				auto pipes = doClient(["-c", pos.to!string, "--symbolLocation"]);
326 				scope (exit)
327 				{
328 					pipes.pid.wait();
329 					pipes.destroy();
330 				}
331 				pipes.stdin.write(code);
332 				pipes.stdin.close();
333 				string line = pipes.stdout.readln();
334 				if (line.length == 0)
335 				{
336 					ret.finish(DCDDeclaration.init);
337 					return;
338 				}
339 				string[] splits = line.chomp.split('\t');
340 				if (splits.length != 2)
341 				{
342 					ret.finish(DCDDeclaration.init);
343 					return;
344 				}
345 				ret.finish(DCDDeclaration(splits[0], splits[1].to!int));
346 			}
347 			catch (Throwable t)
348 			{
349 				ret.error(t);
350 			}
351 		}).start();
352 		return ret;
353 	}
354 
355 	/// Finds the documentation of the symbol at position `pos` in the code
356 	Future!string getDocumentation(string code, int pos)
357 	{
358 		auto ret = new Future!string;
359 		new Thread({
360 			try
361 			{
362 				if (!running)
363 				{
364 					ret.finish("");
365 					return;
366 				}
367 				auto pipes = doClient(["--doc", "-c", pos.to!string]);
368 				scope (exit)
369 				{
370 					pipes.pid.wait();
371 					pipes.destroy();
372 				}
373 				pipes.stdin.write(code);
374 				pipes.stdin.close();
375 				string data;
376 				while (pipes.stdout.isOpen && !pipes.stdout.eof)
377 				{
378 					string line = pipes.stdout.readln();
379 					if (line.length)
380 						data ~= line.chomp;
381 				}
382 				ret.finish(data.unescapeTabs);
383 			}
384 			catch (Throwable t)
385 			{
386 				ret.error(t);
387 			}
388 		}).start();
389 		return ret;
390 	}
391 
392 	/// Returns the used socket file. Only available on OSX, linux and BSD with DCD >= 0.8.0
393 	/// Throws an error if not available.
394 	string getSocketFile()
395 	{
396 		if (!hasUnixDomainSockets)
397 			throw new Exception("Unix domain sockets not supported");
398 		return socketFile;
399 	}
400 
401 	/// Returns the used running port. Throws an error if using unix sockets instead
402 	ushort getRunningPort()
403 	{
404 		if (hasUnixDomainSockets)
405 			throw new Exception("Using unix domain sockets instead of a port");
406 		return runningPort;
407 	}
408 
409 	/// Queries for code completion at position `pos` in code
410 	/// Raw is anything else than identifiers and calltips which might not be implemented by this point.
411 	/// calltips.symbols and identifiers.definition, identifiers.file, identifiers.location and identifiers.documentation are only available with dcd ~master as of now.
412 	Future!DCDCompletions listCompletion(string code, int pos)
413 	{
414 		auto ret = new Future!DCDCompletions;
415 		new Thread({
416 			try
417 			{
418 				DCDCompletions completions;
419 				if (!running)
420 				{
421 					stderr.writeln("DCD not running!");
422 					ret.finish(completions);
423 					return;
424 				}
425 				auto pipes = doClient((supportsFullOutput ? ["--extended"] : []) ~ ["-c", pos.to!string]);
426 				scope (exit)
427 				{
428 					pipes.pid.wait();
429 					pipes.destroy();
430 				}
431 				pipes.stdin.write(code);
432 				pipes.stdin.close();
433 				string[] data;
434 				while (pipes.stdout.isOpen && !pipes.stdout.eof)
435 				{
436 					string line = pipes.stdout.readln();
437 					stderr.writeln("DCD Client: ", line);
438 					if (line.length == 0)
439 						continue;
440 					data ~= line.chomp;
441 				}
442 				completions.raw = data;
443 				int[] emptyArr;
444 				if (data.length == 0)
445 				{
446 					completions.type = DCDCompletions.Type.identifiers;
447 					ret.finish(completions);
448 					return;
449 				}
450 				if (data[0] == "calltips")
451 				{
452 					if (supportsFullOutput)
453 					{
454 						foreach (line; data[1 .. $])
455 						{
456 							auto parts = line.split("\t");
457 							if (parts.length < 5)
458 								continue;
459 							completions._calltips ~= parts[2];
460 							string location = parts[3];
461 							string file;
462 							int index;
463 							if (location.length)
464 							{
465 								auto space = location.indexOf(' ');
466 								if (space != -1)
467 								{
468 									file = location[0 .. space];
469 									index = location[space + 1 .. $].to!int;
470 								}
471 							}
472 							completions._symbols ~= DCDCompletions.Symbol(file, index, parts[4].unescapeTabs);
473 						}
474 					}
475 					else
476 					{
477 						completions._calltips = data[1 .. $];
478 						completions._symbols.length = completions._calltips.length;
479 					}
480 					completions.type = DCDCompletions.Type.calltips;
481 					ret.finish(completions);
482 					return;
483 				}
484 				else if (data[0] == "identifiers")
485 				{
486 					DCDIdentifier[] identifiers;
487 					foreach (line; data[1 .. $])
488 					{
489 						string[] splits = line.split('\t');
490 						DCDIdentifier symbol;
491 						if (supportsFullOutput)
492 						{
493 							if (splits.length < 5)
494 								continue;
495 							string location = splits[3];
496 							string file;
497 							int index;
498 							if (location.length)
499 							{
500 								auto space = location.indexOf(' ');
501 								if (space != -1)
502 								{
503 									file = location[0 .. space];
504 									index = location[space + 1 .. $].to!int;
505 								}
506 							}
507 							symbol = DCDIdentifier(splits[0], splits[1], splits[2], file,
508 								index, splits[4].unescapeTabs);
509 						}
510 						else
511 						{
512 							if (splits.length < 2)
513 								continue;
514 							symbol = DCDIdentifier(splits[0], splits[1]);
515 						}
516 						identifiers ~= symbol;
517 					}
518 					completions.type = DCDCompletions.Type.identifiers;
519 					completions._identifiers = identifiers;
520 					ret.finish(completions);
521 					return;
522 				}
523 				else
524 				{
525 					completions.type = DCDCompletions.Type.raw;
526 					ret.finish(completions);
527 					return;
528 				}
529 			}
530 			catch (Throwable e)
531 			{
532 				ret.error(e);
533 			}
534 		}).start();
535 		return ret;
536 	}
537 
538 	void updateImports()
539 	{
540 		if (!running)
541 			return;
542 		string[] args;
543 		foreach (path; knownImports)
544 			if (path.length)
545 				args ~= "-I" ~ path;
546 		execClient(args);
547 	}
548 
549 	bool fromRunning(bool supportsFullOutput, string socketFile, ushort runningPort)
550 	{
551 		if (socketFile.length ? isSocketRunning(socketFile) : isPortRunning(runningPort))
552 		{
553 			running = true;
554 			this.supportsFullOutput = supportsFullOutput;
555 			this.socketFile = socketFile;
556 			this.runningPort = runningPort;
557 			this.hasUnixDomainSockets = !!socketFile.length;
558 			return true;
559 		}
560 		else
561 			return false;
562 	}
563 
564 	bool getSupportsFullOutput() @property
565 	{
566 		return supportsFullOutput;
567 	}
568 
569 	bool isUsingUnixDomainSockets() @property
570 	{
571 		return hasUnixDomainSockets;
572 	}
573 
574 	bool isActive() @property
575 	{
576 		return running;
577 	}
578 
579 private:
580 	string installedVersion;
581 	bool supportsFullOutput;
582 	bool hasUnixDomainSockets = false;
583 	bool running = false;
584 	ProcessPipes serverPipes;
585 	ushort runningPort;
586 	string socketFile;
587 	string[] knownImports;
588 
589 	string[] clientArgs()
590 	{
591 		if (hasUnixDomainSockets)
592 			return ["--socketFile", socketFile];
593 		else
594 			return ["--port", runningPort.to!string];
595 	}
596 
597 	auto doClient(string[] args)
598 	{
599 		return raw([clientPath] ~ clientArgs ~ args);
600 	}
601 
602 	auto raw(string[] args, Redirect redirect = Redirect.all)
603 	{
604 		return pipeProcess(args, redirect, null, Config.none, refInstance ? instance.cwd : null);
605 	}
606 
607 	auto execClient(string[] args)
608 	{
609 		return rawExec([clientPath] ~ clientArgs ~ args);
610 	}
611 
612 	auto rawExec(string[] args)
613 	{
614 		return execute(args, null, Config.none, size_t.max, refInstance ? instance.cwd : null);
615 	}
616 
617 	bool isSocketRunning(string socket)
618 	{
619 		if (!hasUnixDomainSockets)
620 			return false;
621 		auto ret = execute([clientPath, "-q", "--socketFile", socket]);
622 		return ret.status == 0;
623 	}
624 
625 	bool isPortRunning(ushort port)
626 	{
627 		if (hasUnixDomainSockets)
628 			return false;
629 		auto ret = execute([clientPath, "-q", "--port", port.to!string]);
630 		return ret.status == 0;
631 	}
632 
633 	ushort findOpen(ushort port)
634 	{
635 		--port;
636 		bool isRunning;
637 		do
638 		{
639 			isRunning = isPortRunning(++port);
640 		}
641 		while (isRunning);
642 		return port;
643 	}
644 }
645 
646 bool supportsUnixDomainSockets(string ver)
647 {
648 	return checkVersion(ver, [0, 8, 0]);
649 }
650 
651 unittest
652 {
653 	assert(supportsUnixDomainSockets("0.8.0-beta2+9ec55f40a26f6bb3ca95dc9232a239df6ed25c37"));
654 	assert(!supportsUnixDomainSockets("0.7.9-beta3"));
655 	assert(!supportsUnixDomainSockets("0.7.0"));
656 	assert(supportsUnixDomainSockets("v0.9.8 c7ea7e081ed9ad2d85e9f981fd047d7fcdb2cf51"));
657 	assert(supportsUnixDomainSockets("1.0.0"));
658 }
659 
660 private string unescapeTabs(string val)
661 {
662 	return val.replace("\\t", "\t").replace("\\n", "\n").replace("\\\\", "\\");
663 }
664 
665 /// Returned by findDeclaration
666 struct DCDDeclaration
667 {
668 	string file;
669 	int position;
670 }
671 
672 /// Returned by listCompletion
673 /// When identifiers: `{type:"identifiers", identifiers:[{identifier:string, type:string, definition:string, file:string, location:number, documentation:string}]}`
674 /// When calltips: `{type:"calltips", calltips:[string], symbols:[{file:string, location:number, documentation:string}]}`
675 /// When raw: `{type:"raw", raw:[string]}`
676 struct DCDCompletions
677 {
678 	/// Type of a completion
679 	enum Type
680 	{
681 		/// Unknown/Unimplemented raw output
682 		raw,
683 		/// Completion after a dot or a variable name
684 		identifiers,
685 		/// Completion for arguments in a function call
686 		calltips,
687 	}
688 
689 	struct Symbol
690 	{
691 		string file;
692 		int location;
693 		string documentation;
694 	}
695 
696 	/// Type of the completion (identifiers, calltips, raw)
697 	Type type;
698 	/// Contains the raw DCD output
699 	string[] raw;
700 	union
701 	{
702 		DCDIdentifier[] _identifiers;
703 		struct
704 		{
705 			string[] _calltips;
706 			Symbol[] _symbols;
707 		}
708 	}
709 
710 	enum DCDCompletions empty = DCDCompletions(Type.identifiers);
711 
712 	/// Only set with type==identifiers.
713 	inout(DCDIdentifier[]) identifiers() inout @property
714 	{
715 		if (type != Type.identifiers)
716 			throw new Exception("Type is not identifiers but attempted to access identifiers");
717 		return _identifiers;
718 	}
719 
720 	/// Only set with type==calltips.
721 	inout(string[]) calltips() inout @property
722 	{
723 		if (type != Type.calltips)
724 			throw new Exception("Type is not calltips but attempted to access calltips");
725 		return _calltips;
726 	}
727 
728 	/// Only set with type==calltips.
729 	inout(Symbol[]) symbols() inout @property
730 	{
731 		if (type != Type.calltips)
732 			throw new Exception("Type is not calltips but attempted to access symbols");
733 		return _symbols;
734 	}
735 }
736 
737 /// Returned by status
738 struct DCDServerStatus
739 {
740 	///
741 	bool isRunning;
742 }
743 
744 /// Type of the identifiers value in listCompletion
745 struct DCDIdentifier
746 {
747 	///
748 	string identifier;
749 	///
750 	string type;
751 	///
752 	string definition;
753 	///
754 	string file;
755 	/// byte location
756 	int location;
757 	///
758 	string documentation;
759 }
760 
761 /// Returned by search-symbol
762 struct DCDSearchResult
763 {
764 	///
765 	string file;
766 	///
767 	int position;
768 	///
769 	string type;
770 }