1 module workspaced.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 		return LocalUse.init; // TODO: implement
224 	}
225 
226 	Completion requestAutocomplete(CodeRequest loc)
227 	{
228 		auto pipes = doClient([
229 				"--extended",
230 				"-c", loc.cursorPosition.to!string
231 			]);
232 		scope (exit)
233 		{
234 			pipes.pid.wait();
235 			pipes.destroy();
236 		}
237 		pipes.stdin.write(loc.sourceCode);
238 		pipes.stdin.close();
239 		auto dataApp = appender!(string[]);
240 		while (pipes.stdout.isOpen && !pipes.stdout.eof)
241 		{
242 			string line = (() @trusted => pipes.stdout.readln())();
243 			if (line.length == 0)
244 				continue;
245 			dataApp ~= line.chomp;
246 		}
247 
248 		string[] data = dataApp.data;
249 		auto symbols = appender!(DCDResponse.Completion[]);
250 		Completion c;
251 		if (data.length == 0)
252 		{
253 			c.type = CompletionType.identifiers;
254 			return c;
255 		}
256 		
257 		c.type = cast(CompletionType)data[0];
258 		if (c.type == CompletionType.identifiers
259 			|| c.type == CompletionType.calltips)
260 		{
261 			foreach (line; data[1 .. $])
262 			{
263 				string[] splits = line.split('\t');
264 				DCDResponse.Completion symbol;
265 				if (splits.length < 5)
266 					continue;
267 				string location = splits[3];
268 				string file;
269 				int index;
270 				if (location.length)
271 				{
272 					auto space = location.lastIndexOf(' ');
273 					if (space != -1)
274 					{
275 						file = location[0 .. space];
276 						if (location[space + 1 .. $].all!isDigit)
277 							index = location[space + 1 .. $].to!int;
278 					}
279 					else
280 						file = location;
281 				}
282 				symbol.identifier = splits[0];
283 				symbol.kind = splits[1] == "" ? char.init : splits[1][0];
284 				symbol.definition = splits[2];
285 				symbol.symbolFilePath = file;
286 				symbol.symbolLocation = index;
287 				symbol.documentation = splits[4].unescapeTabs;
288 				symbols ~= symbol;
289 			}
290 		}
291 
292 		c.completions = symbols.data;
293 		return c;
294 	}
295 
296 private:
297 	string[] clientArgs()
298 	{
299 		if (usingUnixDomainSockets)
300 			return ["--socketFile", socketFile];
301 		else
302 			return ["--port", runningPort.to!string];
303 	}
304 
305 	auto doClient(string[] args)
306 	{
307 		return raw([clientPath] ~ clientArgs ~ args);
308 	}
309 
310 	auto raw(string[] args, Redirect redirect = Redirect.all)
311 	{
312 		return pipeProcess(args, redirect, null, Config.none, null);
313 	}
314 
315 	auto execClient(string[] args)
316 	{
317 		return rawExec([clientPath] ~ clientArgs ~ args);
318 	}
319 
320 	auto rawExec(string[] args)
321 	{
322 		return execute(args, null, Config.none, size_t.max, null);
323 	}
324 }
325 
326 class BuiltinDCDClient : IDCDClient
327 {
328 	public static enum minSupportedServerInclusive = [0, 8, 0];
329 	public static enum maxSupportedServerExclusive = [0, 14, 0];
330 
331 	public static immutable clientVersion = DCD_VERSION;
332 
333 	bool useTCP;
334 	string _socketFile;
335 	ushort port = DEFAULT_PORT_NUMBER;
336 
337 	private Mutex socketMutex;
338 	private Socket socket = null;
339 
340 	this()
341 	{
342 		version (haveUnixSockets)
343 		{
344 			this((() @trusted => generateSocketName())());
345 		}
346 		else
347 		{
348 			this(DEFAULT_PORT_NUMBER);
349 		}
350 	}
351 
352 	this(string socketFile)
353 	{
354 		socketMutex = new Mutex();
355 		useTCP = false;
356 		this._socketFile = _socketFile;
357 	}
358 
359 	this(ushort port)
360 	{
361 		socketMutex = new Mutex();
362 		useTCP = true;
363 		this.port = port;
364 	}
365 
366 	string socketFile() const @property
367 	{
368 		return _socketFile;
369 	}
370 
371 	void socketFile(string value) @property
372 	{
373 		version (haveUnixSockets)
374 		{
375 			if (value.length > 0)
376 				useTCP = false;
377 		}
378 		_socketFile = value;
379 	}
380 
381 	ushort runningPort() const @property
382 	{
383 		return port;
384 	}
385 
386 	void runningPort(ushort value) @property
387 	{
388 		if (value != 0)
389 			useTCP = true;
390 		port = value;
391 	}
392 
393 	bool usingUnixDomainSockets() const @property
394 	{
395 		version (haveUnixSockets)
396 			return true;
397 		else
398 			return false;
399 	}
400 
401 	bool queryRunning() @trusted
402 	{
403 		return serverIsRunning(useTCP, socketFile, port);
404 	}
405 
406 	Socket connectForRequest()
407 	{
408 		socketMutex.lock();
409 		scope (failure)
410 		{
411 			socket = null;
412 			socketMutex.unlock();
413 		}
414 
415 		assert(socket is null, "Didn't call closeRequestConnection but attempted to connect again");
416 
417 		if (useTCP)
418 		{
419 			socket = new TcpSocket(AddressFamily.INET);
420 			socket.connect(new InternetAddress("127.0.0.1", port));
421 		}
422 		else
423 		{
424 			version (haveUnixSockets)
425 			{
426 				socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
427 				socket.connect(new UnixAddress(socketFile));
428 			}
429 			else
430 			{
431 				// should never be called with non-null socketFile on Windows
432 				assert(false);
433 			}
434 		}
435 
436 		socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(
437 				5));
438 		socket.blocking = true;
439 		return socket;
440 	}
441 
442 	void closeRequestConnection()
443 	{
444 		scope (exit)
445 		{
446 			socket = null;
447 			socketMutex.unlock();
448 		}
449 
450 		socket.shutdown(SocketShutdown.BOTH);
451 		socket.close();
452 	}
453 
454 	bool performNotification(AutocompleteRequest request) @trusted
455 	{
456 		auto sock = connectForRequest();
457 		scope (exit)
458 			closeRequestConnection();
459 
460 		return sendRequest(sock, request);
461 	}
462 
463 	DCDResponse performRequest(AutocompleteRequest request) @trusted
464 	{
465 		auto sock = connectForRequest();
466 		scope (exit)
467 			closeRequestConnection();
468 
469 		if (!sendRequest(sock, request))
470 			throw new Exception("Failed to send request");
471 
472 		try
473 		{
474 			return getResponse(sock);
475 		}
476 		catch (Exception e)
477 		{
478 			return DCDResponse.init;
479 		}
480 	}
481 
482 	bool shutdown()
483 	{
484 		AutocompleteRequest request;
485 		request.kind = RequestKind.shutdown;
486 		return performNotification(request);
487 	}
488 
489 	bool clearCache()
490 	{
491 		AutocompleteRequest request;
492 		request.kind = RequestKind.clearCache;
493 		return performNotification(request);
494 	}
495 
496 	bool addImportPaths(string[] importPaths)
497 	{
498 		AutocompleteRequest request;
499 		request.kind = RequestKind.addImport;
500 		request.importPaths = importPaths;
501 		return performNotification(request);
502 	}
503 
504 	bool removeImportPaths(string[] importPaths)
505 	{
506 		AutocompleteRequest request;
507 		request.kind = RequestKind.removeImport;
508 		request.importPaths = importPaths;
509 		return performNotification(request);
510 	}
511 
512 	string[] listImportPaths()
513 	{
514 		AutocompleteRequest request;
515 		request.kind = RequestKind.listImports;
516 		return performRequest(request).importPaths;
517 	}
518 
519 	SymbolInformation requestSymbolInfo(CodeRequest loc)
520 	{
521 		AutocompleteRequest request;
522 		request.kind = RequestKind.symbolLocation;
523 		loc.apply(request);
524 		return SymbolInformation(performRequest(request));
525 	}
526 
527 	string[] requestDocumentation(CodeRequest loc)
528 	{
529 		AutocompleteRequest request;
530 		request.kind = RequestKind.doc;
531 		loc.apply(request);
532 		return performRequest(request).completions.map!"a.documentation".array;
533 	}
534 
535 	DCDResponse.Completion[] requestSymbolSearch(string query)
536 	{
537 		AutocompleteRequest request;
538 		request.kind = RequestKind.search;
539 		request.searchName = query;
540 		return performRequest(request).completions;
541 	}
542 
543 	LocalUse requestLocalUse(CodeRequest loc)
544 	{
545 		AutocompleteRequest request;
546 		request.kind = RequestKind.localUse;
547 		loc.apply(request);
548 		return LocalUse(performRequest(request));
549 	}
550 
551 	Completion requestAutocomplete(CodeRequest loc)
552 	{
553 		AutocompleteRequest request;
554 		request.kind = RequestKind.autocomplete;
555 		loc.apply(request);
556 		return Completion(performRequest(request));
557 	}
558 }
559 
560 struct CodeRequest
561 {
562 	string fileName;
563 	const(char)[] sourceCode;
564 	size_t cursorPosition = size_t.max;
565 
566 	// private because sourceCode is const but in AutocompleteRequest it's not
567 	private void apply(ref AutocompleteRequest request)
568 	{
569 		request.fileName = fileName;
570 		// @trusted because the apply function is only used in places where we
571 		// know that the request is not used outside the CodeRequest scope.
572 		request.sourceCode = (() @trusted => cast(ubyte[]) sourceCode)();
573 		request.cursorPosition = cursorPosition;
574 	}
575 }
576 
577 struct SymbolInformation
578 {
579 	string declarationFilePath;
580 	size_t declarationLocation;
581 
582 	this(DCDResponse res)
583 	{
584 		declarationFilePath = res.symbolFilePath;
585 		declarationLocation = res.symbolLocation;
586 	}
587 }
588 
589 struct Completion
590 {
591 	CompletionType type;
592 	DCDResponse.Completion[] completions;
593 
594 	this(DCDResponse res)
595 	{
596 		type = cast(CompletionType) res.completionType;
597 		completions = res.completions;
598 	}
599 }
600 
601 struct LocalUse
602 {
603 	string declarationFilePath;
604 	size_t declarationLocation;
605 	size_t[] uses;
606 
607 	this(DCDResponse res)
608 	{
609 		declarationFilePath = res.symbolFilePath;
610 		declarationLocation = res.symbolLocation;
611 		uses = res.completions.map!"a.symbolLocation".array;
612 	}
613 }
614 
615 private string unescapeTabs(string val)
616 {
617 	if (!val.length)
618 		return val;
619 
620 	auto ret = appender!string;
621 	size_t i = 0;
622 	while (i < val.length)
623 	{
624 		size_t index = val.indexOf('\\', i);
625 		if (index == -1 || cast(int) index == cast(int) val.length - 1)
626 		{
627 			if (!ret.data.length)
628 			{
629 				return val;
630 			}
631 			else
632 			{
633 				ret.put(val[i .. $]);
634 				break;
635 			}
636 		}
637 		else
638 		{
639 			char c = val[index + 1];
640 			switch (c)
641 			{
642 			case 'n':
643 				c = '\n';
644 				break;
645 			case 't':
646 				c = '\t';
647 				break;
648 			default:
649 				break;
650 			}
651 			ret.put(val[i .. index]);
652 			ret.put(c);
653 			i = index + 2;
654 		}
655 	}
656 	return ret.data;
657 }
658 
659 unittest
660 {
661 	shouldEqual("hello world", "hello world".unescapeTabs);
662 	shouldEqual("hello\nworld", "hello\\nworld".unescapeTabs);
663 	shouldEqual("hello\\nworld", "hello\\\\nworld".unescapeTabs);
664 	shouldEqual("hello\\\nworld", "hello\\\\\\nworld".unescapeTabs);
665 }