1 module served.dcd.client;
2 
3 @safe:
4 
5 import core.time;
6 import core.sync.mutex;
7 
8 import std.algorithm;
9 import std.array;
10 import std.ascii;
11 import std.conv;
12 import std.process;
13 import std.socket;
14 import std.stdio;
15 import std.string;
16 
17 import dcd.common.messages;
18 import dcd.common.dcd_version;
19 import dcd.common.socket;
20 
21 version (OSX) version = haveUnixSockets;
22 version (linux) version = haveUnixSockets;
23 version (BSD) version = haveUnixSockets;
24 version (FreeBSD) version = haveUnixSockets;
25 
26 public import dcd.common.messages :
27 	DCDResponse = AutocompleteResponse,
28 	DCDCompletionType = CompletionType,
29 	isDCDServerRunning = serverIsRunning;
30 
31 version (haveUnixSockets)
32 	enum platformSupportsDCDUnixSockets = true;
33 else
34 	enum platformSupportsDCDUnixSockets = false;
35 
36 interface IDCDClient
37 {
38 	string socketFile() const @property;
39 	void socketFile(string) @property;
40 	ushort runningPort() const @property;
41 	void runningPort(ushort) @property;
42 	bool usingUnixDomainSockets() const @property;
43 
44 	bool queryRunning();
45 	bool shutdown();
46 	bool clearCache();
47 	bool addImportPaths(string[] importPaths);
48 	bool removeImportPaths(string[] importPaths);
49 	string[] listImportPaths();
50 	SymbolInformation requestSymbolInfo(CodeRequest loc);
51 	string[] requestDocumentation(CodeRequest loc);
52 	DCDResponse.Completion[] requestSymbolSearch(string query);
53 	LocalUse requestLocalUse(CodeRequest loc);
54 	Completion requestAutocomplete(CodeRequest loc);
55 }
56 
57 class ExternalDCDClient : IDCDClient
58 {
59 	string clientPath;
60 	ushort _runningPort;
61 	string _socketFile;
62 
63 	this(string clientPath)
64 	{
65 		this.clientPath = clientPath;
66 	}
67 
68 	string socketFile() const @property
69 	{
70 		return _socketFile;
71 	}
72 
73 	void socketFile(string value) @property
74 	{
75 		_socketFile = value;
76 	}
77 
78 	ushort runningPort() const @property
79 	{
80 		return _runningPort;
81 	}
82 
83 	void runningPort(ushort value) @property
84 	{
85 		_runningPort = value;
86 	}
87 
88 	bool usingUnixDomainSockets() const @property
89 	{
90 		version (haveUnixSockets)
91 			return true;
92 		else
93 			return false;
94 	}
95 
96 	bool queryRunning()
97 	{
98 		return doClient(["--query"]).pid.wait == 0;
99 	}
100 
101 	bool shutdown()
102 	{
103 		return doClient(["--shutdown"]).pid.wait == 0;
104 	}
105 
106 	bool clearCache()
107 	{
108 		return doClient(["--clearCache"]).pid.wait == 0;
109 	}
110 
111 	bool addImportPaths(string[] importPaths)
112 	{
113 		string[] args;
114 		foreach (path; importPaths)
115 			if (path.length)
116 				args ~= "-I" ~ path;
117 		return execClient(args).status == 0;
118 	}
119 
120 	bool removeImportPaths(string[] importPaths)
121 	{
122 		string[] args;
123 		foreach (path; importPaths)
124 			if (path.length)
125 				args ~= "-R" ~ path;
126 		return execClient(args).status == 0;
127 	}
128 
129 	string[] listImportPaths()
130 	{
131 		auto pipes = doClient(["--listImports"]);
132 		scope (exit)
133 		{
134 			pipes.pid.wait();
135 			pipes.destroy();
136 		}
137 		pipes.stdin.close();
138 		auto results = appender!(string[]);
139 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
140 		{
141 			results.put((() @trusted => pipes.stdout.readln())());
142 		}
143 		return results.data;
144 	}
145 
146 	SymbolInformation requestSymbolInfo(CodeRequest loc)
147 	{
148 		auto pipes = doClient([
149 				"-c", loc.cursorPosition.to!string, "--symbolLocation"
150 				]);
151 		scope (exit)
152 		{
153 			pipes.pid.wait();
154 			pipes.destroy();
155 		}
156 		pipes.stdin.write(loc.sourceCode);
157 		pipes.stdin.close();
158 		string line = (() @trusted => pipes.stdout.readln())();
159 		if (line.length == 0)
160 			return SymbolInformation.init;
161 		string[] splits = line.chomp.split('\t');
162 		if (splits.length != 2)
163 			return SymbolInformation.init;
164 		SymbolInformation ret;
165 		ret.declarationFilePath = splits[0];
166 		if (ret.declarationFilePath == "stdin")
167 			ret.declarationFilePath = loc.fileName;
168 		ret.declarationLocation = splits[1].to!size_t;
169 		return ret;
170 	}
171 
172 	string[] requestDocumentation(CodeRequest loc)
173 	{
174 		auto pipes = doClient(["--doc", "-c", loc.cursorPosition.to!string]);
175 		scope (exit)
176 		{
177 			pipes.pid.wait();
178 			pipes.destroy();
179 		}
180 		pipes.stdin.write(loc.sourceCode);
181 		pipes.stdin.close();
182 		string[] data;
183 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
184 		{
185 			string line = (() @trusted => pipes.stdout.readln())();
186 			if (line.length)
187 				data ~= line.chomp.unescapeTabs;
188 		}
189 		return data;
190 	}
191 
192 	DCDResponse.Completion[] requestSymbolSearch(string query)
193 	{
194 		auto pipes = doClient(["--search", query]);
195 		scope (exit)
196 		{
197 			pipes.pid.wait();
198 			pipes.destroy();
199 		}
200 		pipes.stdin.close();
201 		auto results = appender!(DCDResponse.Completion[]);
202 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
203 		{
204 			string line = (() @trusted => pipes.stdout.readln())();
205 			if (line.length == 0)
206 				continue;
207 			string[] splits = line.chomp.split('\t');
208 			if (splits.length >= 3)
209 			{
210 				DCDResponse.Completion item;
211 				item.identifier = query; // hack
212 				item.kind = splits[1] == "" ? char.init : splits[1][0];
213 				item.symbolFilePath = splits[0];
214 				item.symbolLocation = splits[2].to!size_t;
215 				results ~= item;
216 			}
217 		}
218 		return results.data;
219 	}
220 
221 	LocalUse requestLocalUse(CodeRequest loc)
222 	{
223 		auto pipes = doClient([
224 				"--localUse",
225 				"-c", loc.cursorPosition.to!string
226 			]);
227 		scope (exit)
228 		{
229 			pipes.pid.wait();
230 			pipes.destroy();
231 		}
232 		pipes.stdin.write(loc.sourceCode);
233 		pipes.stdin.close();
234 		
235 		string header = (() @trusted => pipes.stdout.readln())().chomp;
236 		if (header == "00000" || !header.length)
237 			return LocalUse.init;
238 
239 		LocalUse ret;
240 		auto headerParts = header.split('\t');
241 		if (headerParts.length < 2)
242 			return LocalUse.init;
243 
244 		ret.declarationFilePath = headerParts[0];
245 		ret.declarationLocation = headerParts[1].length ? headerParts[1].to!size_t : 0;
246 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
247 		{
248 			string line = (() @trusted => pipes.stdout.readln())().chomp;
249 			if (line.length == 0)
250 				continue;
251 			ret.uses ~= line.to!size_t;
252 		}
253 		return ret;
254 	}
255 
256 	Completion requestAutocomplete(CodeRequest loc)
257 	{
258 		auto pipes = doClient([
259 				"--extended",
260 				"-c", loc.cursorPosition.to!string
261 			]);
262 		scope (exit)
263 		{
264 			pipes.pid.wait();
265 			pipes.destroy();
266 		}
267 		pipes.stdin.write(loc.sourceCode);
268 		pipes.stdin.close();
269 		auto dataApp = appender!(string[]);
270 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
271 		{
272 			string line = (() @trusted => pipes.stdout.readln())();
273 			if (line.length == 0)
274 				continue;
275 			dataApp ~= line.chomp;
276 		}
277 
278 		string[] data = dataApp.data;
279 		auto symbols = appender!(DCDResponse.Completion[]);
280 		Completion c;
281 		if (data.length == 0)
282 		{
283 			c.type = CompletionType.identifiers;
284 			return c;
285 		}
286 		
287 		c.type = cast(CompletionType)data[0];
288 		if (c.type == CompletionType.identifiers
289 			|| c.type == CompletionType.calltips)
290 		{
291 			foreach (line; data[1 .. $])
292 			{
293 				string[] splits = line.split('\t');
294 				DCDResponse.Completion symbol;
295 				if (splits.length < 5)
296 					continue;
297 				string location = splits[3];
298 				string file;
299 				int index;
300 				if (location.length)
301 				{
302 					auto space = location.lastIndexOf(' ');
303 					if (space != -1)
304 					{
305 						file = location[0 .. space];
306 						if (location[space + 1 .. $].all!isDigit)
307 							index = location[space + 1 .. $].to!int;
308 					}
309 					else
310 						file = location;
311 				}
312 				symbol.identifier = splits[0];
313 				symbol.kind = splits[1] == "" ? char.init : splits[1][0];
314 				symbol.definition = splits[2];
315 				symbol.symbolFilePath = file;
316 				symbol.symbolLocation = index;
317 				symbol.documentation = splits[4].unescapeTabs;
318 				symbols ~= symbol;
319 			}
320 		}
321 
322 		c.completions = symbols.data;
323 		return c;
324 	}
325 
326 private:
327 	string[] clientArgs()
328 	{
329 		if (usingUnixDomainSockets)
330 			return ["--socketFile", socketFile];
331 		else
332 			return ["--port", runningPort.to!string];
333 	}
334 
335 	auto doClient(string[] args)
336 	{
337 		return raw([clientPath] ~ clientArgs ~ args);
338 	}
339 
340 	auto raw(string[] args, Redirect redirect = Redirect.all)
341 	{
342 		return pipeProcess(args, redirect, null, Config.none, null);
343 	}
344 
345 	auto execClient(string[] args)
346 	{
347 		return rawExec([clientPath] ~ clientArgs ~ args);
348 	}
349 
350 	auto rawExec(string[] args)
351 	{
352 		return execute(args, null, Config.none, size_t.max, null);
353 	}
354 }
355 
356 class BuiltinDCDClient : IDCDClient
357 {
358 	public static enum minSupportedServerInclusive = [0, 8, 0];
359 	public static enum maxSupportedServerExclusive = [0, 14, 0];
360 
361 	public static immutable clientVersion = DCD_VERSION;
362 
363 	bool useTCP;
364 	string _socketFile;
365 	ushort port = DEFAULT_PORT_NUMBER;
366 
367 	private Mutex socketMutex;
368 	private Socket socket = null;
369 
370 	this()
371 	{
372 		version (haveUnixSockets)
373 		{
374 			this((() @trusted => generateSocketName())());
375 		}
376 		else
377 		{
378 			this(DEFAULT_PORT_NUMBER);
379 		}
380 	}
381 
382 	this(string socketFile)
383 	{
384 		socketMutex = new Mutex();
385 		useTCP = false;
386 		this._socketFile = _socketFile;
387 	}
388 
389 	this(ushort port)
390 	{
391 		socketMutex = new Mutex();
392 		useTCP = true;
393 		this.port = port;
394 	}
395 
396 	string socketFile() const @property
397 	{
398 		return _socketFile;
399 	}
400 
401 	void socketFile(string value) @property
402 	{
403 		version (haveUnixSockets)
404 		{
405 			if (value.length > 0)
406 				useTCP = false;
407 		}
408 		_socketFile = value;
409 	}
410 
411 	ushort runningPort() const @property
412 	{
413 		return port;
414 	}
415 
416 	void runningPort(ushort value) @property
417 	{
418 		if (value != 0)
419 			useTCP = true;
420 		port = value;
421 	}
422 
423 	bool usingUnixDomainSockets() const @property
424 	{
425 		version (haveUnixSockets)
426 			return true;
427 		else
428 			return false;
429 	}
430 
431 	bool queryRunning() @trusted
432 	{
433 		return serverIsRunning(useTCP, socketFile, port);
434 	}
435 
436 	Socket connectForRequest()
437 	{
438 		socketMutex.lock();
439 		scope (failure)
440 		{
441 			socket = null;
442 			socketMutex.unlock();
443 		}
444 
445 		assert(socket is null, "Didn't call closeRequestConnection but attempted to connect again");
446 
447 		if (useTCP)
448 		{
449 			socket = new TcpSocket(AddressFamily.INET);
450 			socket.connect(new InternetAddress("127.0.0.1", port));
451 		}
452 		else
453 		{
454 			version (haveUnixSockets)
455 			{
456 				socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
457 				socket.connect(new UnixAddress(socketFile));
458 			}
459 			else
460 			{
461 				// should never be called with non-null socketFile on Windows
462 				assert(false);
463 			}
464 		}
465 
466 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(
467 				5));
468 		socket.blocking = true;
469 		return socket;
470 	}
471 
472 	void closeRequestConnection()
473 	{
474 		scope (exit)
475 		{
476 			socket = null;
477 			socketMutex.unlock();
478 		}
479 
480 		socket.shutdown(SocketShutdown.BOTH);
481 		socket.close();
482 	}
483 
484 	bool performNotification(AutocompleteRequest request) @trusted
485 	{
486 		auto sock = connectForRequest();
487 		scope (exit)
488 			closeRequestConnection();
489 
490 		return sendRequest(sock, request);
491 	}
492 
493 	DCDResponse performRequest(AutocompleteRequest request) @trusted
494 	{
495 		auto sock = connectForRequest();
496 		scope (exit)
497 			closeRequestConnection();
498 
499 		if (!sendRequest(sock, request))
500 			throw new Exception("Failed to send request");
501 
502 		try
503 		{
504 			return getResponse(sock);
505 		}
506 		catch (Exception e)
507 		{
508 			return DCDResponse.init;
509 		}
510 	}
511 
512 	bool shutdown()
513 	{
514 		AutocompleteRequest request;
515 		request.kind = RequestKind.shutdown;
516 		return performNotification(request);
517 	}
518 
519 	bool clearCache()
520 	{
521 		AutocompleteRequest request;
522 		request.kind = RequestKind.clearCache;
523 		return performNotification(request);
524 	}
525 
526 	bool addImportPaths(string[] importPaths)
527 	{
528 		AutocompleteRequest request;
529 		request.kind = RequestKind.addImport;
530 		request.importPaths = importPaths;
531 		return performNotification(request);
532 	}
533 
534 	bool removeImportPaths(string[] importPaths)
535 	{
536 		AutocompleteRequest request;
537 		request.kind = RequestKind.removeImport;
538 		request.importPaths = importPaths;
539 		return performNotification(request);
540 	}
541 
542 	string[] listImportPaths()
543 	{
544 		AutocompleteRequest request;
545 		request.kind = RequestKind.listImports;
546 		return performRequest(request).importPaths;
547 	}
548 
549 	SymbolInformation requestSymbolInfo(CodeRequest loc)
550 	{
551 		AutocompleteRequest request;
552 		request.kind = RequestKind.symbolLocation;
553 		loc.apply(request);
554 		return SymbolInformation(performRequest(request));
555 	}
556 
557 	string[] requestDocumentation(CodeRequest loc)
558 	{
559 		AutocompleteRequest request;
560 		request.kind = RequestKind.doc;
561 		loc.apply(request);
562 		return performRequest(request).completions.map!"a.documentation".array;
563 	}
564 
565 	DCDResponse.Completion[] requestSymbolSearch(string query)
566 	{
567 		AutocompleteRequest request;
568 		request.kind = RequestKind.search;
569 		request.searchName = query;
570 		return performRequest(request).completions;
571 	}
572 
573 	LocalUse requestLocalUse(CodeRequest loc)
574 	{
575 		AutocompleteRequest request;
576 		request.kind = RequestKind.localUse;
577 		loc.apply(request);
578 		return LocalUse(performRequest(request));
579 	}
580 
581 	Completion requestAutocomplete(CodeRequest loc)
582 	{
583 		AutocompleteRequest request;
584 		request.kind = RequestKind.autocomplete;
585 		loc.apply(request);
586 		return Completion(performRequest(request));
587 	}
588 }
589 
590 struct CodeRequest
591 {
592 	string fileName;
593 	const(char)[] sourceCode;
594 	size_t cursorPosition = size_t.max;
595 
596 	// private because sourceCode is const but in AutocompleteRequest it's not
597 	private void apply(ref AutocompleteRequest request)
598 	{
599 		request.fileName = fileName;
600 		// @trusted because the apply function is only used in places where we
601 		// know that the request is not used outside the CodeRequest scope.
602 		request.sourceCode = (() @trusted => cast(ubyte[]) sourceCode)();
603 		request.cursorPosition = cursorPosition;
604 	}
605 }
606 
607 struct SymbolInformation
608 {
609 	string declarationFilePath;
610 	size_t declarationLocation;
611 
612 	this(DCDResponse res)
613 	{
614 		declarationFilePath = res.symbolFilePath;
615 		declarationLocation = res.symbolLocation;
616 	}
617 }
618 
619 struct Completion
620 {
621 	CompletionType type;
622 	DCDResponse.Completion[] completions;
623 
624 	this(DCDResponse res)
625 	{
626 		type = cast(CompletionType) res.completionType;
627 		completions = res.completions;
628 	}
629 }
630 
631 struct LocalUse
632 {
633 	string declarationFilePath;
634 	size_t declarationLocation;
635 	size_t[] uses;
636 
637 	this(DCDResponse res)
638 	{
639 		declarationFilePath = res.symbolFilePath;
640 		declarationLocation = res.symbolLocation;
641 		uses = res.completions.map!"a.symbolLocation".array;
642 	}
643 }
644 
645 private string unescapeTabs(string val)
646 {
647 	if (!val.length)
648 		return val;
649 
650 	auto ret = appender!string;
651 	size_t i = 0;
652 	while (i < val.length)
653 	{
654 		size_t index = val.indexOf('\\', i);
655 		if (index == -1 || cast(int) index == cast(int) val.length - 1)
656 		{
657 			if (!ret.data.length)
658 			{
659 				return val;
660 			}
661 			else
662 			{
663 				ret.put(val[i .. $]);
664 				break;
665 			}
666 		}
667 		else
668 		{
669 			char c = val[index + 1];
670 			switch (c)
671 			{
672 			case 'n':
673 				c = '\n';
674 				break;
675 			case 't':
676 				c = '\t';
677 				break;
678 			default:
679 				break;
680 			}
681 			ret.put(val[i .. index]);
682 			ret.put(c);
683 			i = index + 2;
684 		}
685 	}
686 	return ret.data;
687 }
688 
689 unittest
690 {
691 	shouldEqual("hello world", "hello world".unescapeTabs);
692 	shouldEqual("hello\nworld", "hello\\nworld".unescapeTabs);
693 	shouldEqual("hello\\nworld", "hello\\\\nworld".unescapeTabs);
694 	shouldEqual("hello\\\nworld", "hello\\\\\\nworld".unescapeTabs);
695 }