1 module workspaced.api;
2 
3 // debug = Tasks;
4 
5 import standardpaths;
6 
7 import std.algorithm : all;
8 import std.array : array;
9 import std.conv;
10 import std.file : exists, thisExePath;
11 import std.json : JSONType, JSONValue;
12 import std.path : baseName, chainPath, dirName;
13 import std.regex : ctRegex, matchFirst;
14 import std.string : indexOf, indexOfAny, strip;
15 import std.traits;
16 
17 public import workspaced.backend;
18 public import workspaced.future;
19 
20 version (unittest)
21 {
22 	package import std.experimental.logger : trace;
23 }
24 else
25 {
26 	// dummy
27 	package void trace(Args...)(lazy Args)
28 	{
29 	}
30 }
31 
32 ///
33 alias ImportPathProvider = string[] delegate() nothrow;
34 ///
35 alias IdentifierListProvider = string[] delegate() nothrow;
36 ///
37 alias BroadcastCallback = void delegate(WorkspaceD, WorkspaceD.Instance, JSONValue);
38 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails)
39 /// Params:
40 /// 	instance = the instance for which the component was attempted to initialize (or null for global component registration)
41 /// 	factory = the factory on which the error occured with
42 /// 	error = the stacktrace that was catched on the bind call
43 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance instance,
44 		ComponentFactory factory, Exception error);
45 
46 /// UDA; will never try to call this function from rpc
47 enum ignoredFunc;
48 
49 /// Component call
50 struct ComponentInfoParams
51 {
52 	/// Name of the component
53 	string name;
54 }
55 
56 ComponentInfoParams component(string name)
57 {
58 	return ComponentInfoParams(name);
59 }
60 
61 struct ComponentInfo
62 {
63 	ComponentInfoParams params;
64 	TypeInfo type;
65 
66 	alias params this;
67 }
68 
69 void traceTaskLog(lazy string msg)
70 {
71 	import std.stdio : stderr;
72 
73 	debug (Tasks)
74 		stderr.writeln(msg);
75 }
76 
77 static immutable traceTask = `traceTaskLog("new task in " ~ __PRETTY_FUNCTION__); scope (exit) traceTaskLog(__PRETTY_FUNCTION__ ~ " exited");`;
78 
79 mixin template DefaultComponentWrapper(bool withDtor = true)
80 {
81 	@ignoredFunc
82 	{
83 		import std.algorithm : min, max;
84 		import std.parallelism : TaskPool, Task, task, defaultPoolThreads;
85 
86 		WorkspaceD workspaced;
87 		WorkspaceD.Instance refInstance;
88 
89 		TaskPool _threads;
90 
91 		static if (withDtor)
92 		{
93 			~this()
94 			{
95 				shutdown(true);
96 			}
97 		}
98 
99 		TaskPool gthreads()
100 		{
101 			return workspaced.gthreads;
102 		}
103 
104 		TaskPool threads(int minSize, int maxSize)
105 		{
106 			if (!_threads)
107 				synchronized (this)
108 					if (!_threads)
109 					{
110 						_threads = new TaskPool(max(minSize, min(maxSize, defaultPoolThreads)));
111 						_threads.isDaemon = true;
112 					}
113 			return _threads;
114 		}
115 
116 		inout(WorkspaceD.Instance) instance() inout @property
117 		{
118 			if (refInstance)
119 				return refInstance;
120 			else
121 				throw new Exception("Attempted to access instance in a global context");
122 		}
123 
124 		WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property
125 		{
126 			return refInstance = instance;
127 		}
128 
129 		string[] importPaths() const @property
130 		{
131 			return instance.importPathProvider ? instance.importPathProvider() : [];
132 		}
133 
134 		string[] stringImportPaths() const @property
135 		{
136 			return instance.stringImportPathProvider ? instance.stringImportPathProvider() : [];
137 		}
138 
139 		string[] importFiles() const @property
140 		{
141 			return instance.importFilesProvider ? instance.importFilesProvider() : [];
142 		}
143 
144 		/// Lists the project defined version identifiers, if provided by any identifier
145 		string[] projectVersions() const @property
146 		{
147 			return instance.projectVersionsProvider ? instance.projectVersionsProvider() : [];
148 		}
149 
150 		/// Lists the project defined debug specification identifiers, if provided by any provider 
151 		string[] debugSpecifications() const @property
152 		{
153 			return instance.debugSpecificationsProvider ? instance.debugSpecificationsProvider() : [];
154 		}
155 
156 		ref inout(ImportPathProvider) importPathProvider() @property inout
157 		{
158 			return instance.importPathProvider;
159 		}
160 
161 		ref inout(ImportPathProvider) stringImportPathProvider() @property inout
162 		{
163 			return instance.stringImportPathProvider;
164 		}
165 
166 		ref inout(ImportPathProvider) importFilesProvider() @property inout
167 		{
168 			return instance.importFilesProvider;
169 		}
170 
171 		ref inout(IdentifierListProvider) projectVersionsProvider() @property inout
172 		{
173 			return instance.projectVersionsProvider;
174 		}
175 
176 		ref inout(IdentifierListProvider) debugSpecificationsProvider() @property inout
177 		{
178 			return instance.debugSpecificationsProvider;
179 		}
180 
181 		ref inout(Configuration) config() @property inout
182 		{
183 			if (refInstance)
184 				return refInstance.config;
185 			else if (workspaced)
186 				return workspaced.globalConfiguration;
187 			else
188 				assert(false, "Unbound component trying to access config.");
189 		}
190 
191 		bool has(T)()
192 		{
193 			if (refInstance)
194 				return refInstance.has!T;
195 			else if (workspaced)
196 				return workspaced.has!T;
197 			else
198 				assert(false, "Unbound component trying to check for component " ~ T.stringof ~ ".");
199 		}
200 
201 		T get(T)()
202 		{
203 			if (refInstance)
204 				return refInstance.get!T;
205 			else if (workspaced)
206 				return workspaced.get!T;
207 			else
208 				assert(false, "Unbound component trying to get component " ~ T.stringof ~ ".");
209 		}
210 
211 		string cwd() @property const
212 		{
213 			return instance.cwd;
214 		}
215 
216 		override void shutdown(bool dtor = false)
217 		{
218 			if (!dtor && _threads)
219 				_threads.finish();
220 		}
221 
222 		override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance)
223 		{
224 			this.workspaced = workspaced;
225 			this.instance = instance;
226 			static if (__traits(hasMember, typeof(this).init, "load"))
227 				load();
228 		}
229 
230 		import std.conv;
231 		import std.json : JSONValue;
232 		import std.traits : isFunction, hasUDA, ParameterDefaults, Parameters, ReturnType;
233 		import painlessjson;
234 
235 		override Future!JSONValue run(string method, JSONValue[] args)
236 		{
237 			static foreach (member; __traits(derivedMembers, typeof(this)))
238 			{
239 				static if (member[0] != '_'
240 						&& __traits(compiles, __traits(getMember, typeof(this).init, member))
241 						&& __traits(getProtection, __traits(getMember, typeof(this).init, member)) == "public"
242 						&& __traits(compiles, isFunction!(__traits(getMember, typeof(this) .init, member)))
243 						&& isFunction!(__traits(getMember, typeof(this).init, member))
244 						&& !hasUDA!(__traits(getMember, typeof(this).init, member), ignoredFunc)
245 						&& !__traits(isTemplate, __traits(getMember, typeof(this).init, member)))
246 				{
247 					if (method == member)
248 						return runMethod!member(args);
249 				}
250 			}
251 			throw new Exception("Method " ~ method ~ " not found.");
252 		}
253 
254 		Future!JSONValue runMethod(string method)(JSONValue[] args)
255 		{
256 			int matches;
257 			static foreach (overload; __traits(getOverloads, typeof(this), method))
258 			{
259 				if (matchesOverload!overload(args))
260 					matches++;
261 			}
262 			if (matches == 0)
263 				throw new Exception("No suitable overload found for " ~ method ~ ".");
264 			if (matches > 1)
265 				throw new Exception("Multiple overloads found for " ~ method ~ ".");
266 			static foreach (overload; __traits(getOverloads, typeof(this), method))
267 			{
268 				if (matchesOverload!overload(args))
269 					return runOverload!overload(args);
270 			}
271 			assert(false);
272 		}
273 
274 		Future!JSONValue runOverload(alias fun)(JSONValue[] args)
275 		{
276 			mixin(generateOverloadCall!fun);
277 		}
278 
279 		static string generateOverloadCall(alias fun)()
280 		{
281 			string retarg;
282 			string decl;
283 			string call = "fun(";
284 			string arg;
285 			static foreach (i, T; Parameters!fun)
286 			{
287 				static if (is(T : const(char)[]))
288 					arg = "args[" ~ i.to!string ~ "].str";
289 				else
290 					arg = "args[" ~ i.to!string ~ "].fromJSON!(" ~ T.stringof ~ ")";
291 
292 				static if (isRefOrOutParam!(fun, i))
293 				{
294 					decl ~= "auto arg_" ~ i.stringof ~ " = " ~ arg ~ ";";
295 					if (retarg.length)
296 						assert(false, "only a single ref/out parameter is allowed");
297 					retarg = "arg_" ~ i.stringof;
298 					call ~= "arg_" ~ i.stringof ~ ", ";
299 				}
300 				else
301 				{
302 					call ~= arg ~ ", ";
303 				}
304 			}
305 			call ~= ")";
306 			static if (is(ReturnType!fun : Future!T, T))
307 			{
308 				assert(!retarg.length, "async functions may not use ref/out parameters");
309 				static if (is(T == void))
310 					string conv = "ret.finish(JSONValue(null));";
311 				else
312 					string conv = "ret.finish(v.value.toJSON);";
313 				return decl ~ "auto ret = new Future!JSONValue; auto v = " ~ call
314 					~ "; v.onDone = { if (v.exception) ret.error(v.exception); else "
315 					~ conv ~ " }; return ret;";
316 			}
317 			else static if (is(ReturnType!fun == void))
318 			{
319 				if (retarg.length)
320 					return decl ~ call ~ "; return Future!JSONValue.fromResult(" ~ retarg ~ ".toJSON);";
321 				else
322 					return decl ~ call ~ "; return Future!JSONValue.fromResult(JSONValue(null));";
323 			}
324 			else
325 			{
326 				assert(!retarg.length, "functions with ref/out parameter may not return any value");
327 				return decl ~ "return Future!JSONValue.fromResult(" ~ call ~ ".toJSON);";
328 			}
329 		}
330 	}
331 }
332 
333 bool isRefOrOutParam(alias fun, size_t i)()
334 {
335 	static foreach (sc; __traits(getParameterStorageClasses, fun, i))
336 		if (sc == "ref" || sc == "out")
337 			return true;
338 	return false;
339 }
340 
341 bool matchesOverload(alias fun)(JSONValue[] args)
342 {
343 	if (args.length > Parameters!fun.length)
344 		return false;
345 	static foreach (i, def; ParameterDefaults!fun)
346 	{
347 		static if (is(def == void))
348 		{
349 			if (i >= args.length)
350 				return false;
351 			else if (!checkType!(Parameters!fun[i])(args[i]))
352 				return false;
353 		}
354 	}
355 	return true;
356 }
357 
358 bool checkType(T)(JSONValue value)
359 {
360 	final switch (value.type)
361 	{
362 	case JSONType.array:
363 		static if (isStaticArray!T)
364 			return T.length == value.array.length
365 				&& value.array.all!(checkType!(typeof(T.init[0])));
366 		else static if (isDynamicArray!T)
367 			return value.array.all!(checkType!(typeof(T.init[0])));
368 		else static if (is(T : Tuple!Args, Args...))
369 		{
370 			if (value.array.length != Args.length)
371 				return false;
372 			static foreach (i, Arg; Args)
373 				if (!checkType!Arg(value.array[i]))
374 					return false;
375 			return true;
376 		}
377 		else
378 			return false;
379 	case JSONType.false_:
380 	case JSONType.true_:
381 		return is(T : bool);
382 	case JSONType.float_:
383 		return isNumeric!T;
384 	case JSONType.integer:
385 	case JSONType.uinteger:
386 		return isIntegral!T;
387 	case JSONType.null_:
388 		static if (is(T == class) || isArray!T || isPointer!T
389 				|| is(T : Nullable!U, U))
390 			return true;
391 		else
392 			return false;
393 	case JSONType.object:
394 		return is(T == class) || is(T == struct);
395 	case JSONType..string:
396 		return isSomeString!T;
397 	}
398 }
399 
400 interface ComponentWrapper
401 {
402 	void bind(WorkspaceD workspaced, WorkspaceD.Instance instance);
403 	Future!JSONValue run(string method, JSONValue[] args);
404 	void shutdown(bool dtor = false);
405 }
406 
407 interface ComponentFactory
408 {
409 	ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow;
410 	ComponentInfo info() @property const nothrow;
411 }
412 
413 struct ComponentFactoryInstance
414 {
415 	ComponentFactory factory;
416 	bool autoRegister;
417 	alias factory this;
418 }
419 
420 struct ComponentWrapperInstance
421 {
422 	ComponentWrapper wrapper;
423 	ComponentInfo info;
424 }
425 
426 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory
427 {
428 	ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow
429 	{
430 		auto wrapper = new T();
431 		try
432 		{
433 			wrapper.bind(workspaced, instance);
434 			return wrapper;
435 		}
436 		catch (Exception e)
437 		{
438 			error = e;
439 			return null;
440 		}
441 	}
442 
443 	ComponentInfo info() @property const nothrow
444 	{
445 		alias udas = getUDAs!(T, ComponentInfoParams);
446 		static assert(udas.length == 1, "Can't construct default component factory for "
447 				~ T.stringof ~ ", expected exactly 1 ComponentInfoParams instance attached to the type");
448 		return ComponentInfo(udas[0], typeid(T));
449 	}
450 }
451 
452 /// Describes what to insert/replace/delete to do something
453 struct CodeReplacement
454 {
455 	/// Range what to replace. If both indices are the same its inserting.
456 	size_t[2] range;
457 	/// Content to replace it with. Empty means remove.
458 	string content;
459 
460 	/// Applies this edit to a string.
461 	string apply(string code)
462 	{
463 		size_t min = range[0];
464 		size_t max = range[1];
465 		if (min > max)
466 		{
467 			min = range[1];
468 			max = range[0];
469 		}
470 		if (min >= code.length)
471 			return code ~ content;
472 		if (max >= code.length)
473 			return code[0 .. min] ~ content;
474 		return code[0 .. min] ~ content ~ code[max .. $];
475 	}
476 }
477 
478 /// Code replacements mapped to a file
479 struct FileChanges
480 {
481 	/// File path to change.
482 	string file;
483 	/// Replacements to apply.
484 	CodeReplacement[] replacements;
485 }
486 
487 package bool getConfigPath(string file, ref string retPath)
488 {
489 	foreach (dir; standardPaths(StandardPath.config, "workspace-d"))
490 	{
491 		auto path = chainPath(dir, file);
492 		if (path.exists)
493 		{
494 			retPath = path.array;
495 			return true;
496 		}
497 	}
498 	return false;
499 }
500 
501 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`;
502 bool checkVersion(string ver, int[3] target)
503 {
504 	auto match = ver.matchFirst(verRegex);
505 	if (!match)
506 		return false;
507 	const major = match[1].to!int;
508 	const minor = match[2].to!int;
509 	const patch = match[3].to!int;
510 	return checkVersion([major, minor, patch], target);
511 }
512 
513 bool checkVersion(int[3] ver, int[3] target)
514 {
515 	if (ver[0] > target[0])
516 		return true;
517 	if (ver[0] == target[0] && ver[1] > target[1])
518 		return true;
519 	if (ver[0] == target[0] && ver[1] == target[1] && ver[2] >= target[2])
520 		return true;
521 	return false;
522 }
523 
524 package string getVersionAndFixPath(ref string execPath)
525 {
526 	import std.process;
527 
528 	try
529 	{
530 		return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath);
531 	}
532 	catch (ProcessException e)
533 	{
534 		auto newPath = chainPath(thisExePath.dirName, execPath.baseName);
535 		if (exists(newPath))
536 		{
537 			execPath = newPath.array;
538 			return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath);
539 		}
540 		throw new Exception("Failed running program ['"
541 			~ execPath ~ "' '--version'] and no alternative existed in '"
542 			~ newPath.array.idup ~ "'.", e);
543 	}
544 }
545 
546 /// Set for some reason when compiling with `dub fetch` / `dub run` or sometimes
547 /// on self compilation.
548 /// Known strings: vbin, vdcd, vDCD
549 package bool isLocallyCompiledDCD(string v)
550 {
551 	import std.uni : sicmp;
552 
553 	return sicmp(v, "vbin") == 0 || sicmp(v, "vdcd") == 0;
554 }
555 
556 /// returns the version that is given or the version extracted from dub path if path is a dub path
557 package string orDubFetchFallback(string v, string path)
558 {
559 	if (v.isLocallyCompiledDCD)
560 	{
561 		auto dub = path.indexOf(`dub/packages`);
562 		if (dub == -1)
563 			dub = path.indexOf(`dub\packages`);
564 
565 		if (dub != -1)
566 		{
567 			dub += `dub/packages/`.length;
568 			auto end = path.indexOfAny(`\/`, dub);
569 
570 			if (end != -1)
571 			{
572 				path = path[dub .. end];
573 				auto semver = extractPathSemver(path);
574 				if (semver.length)
575 					return semver;
576 			}
577 		}
578 	}
579 	return v;
580 }
581 
582 unittest
583 {
584 	assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1/dcd/bin/dcd-server`) == "0.13.1");
585 	assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1-beta.4/dcd/bin/dcd-server`) == "0.13.1-beta.4");
586 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1\dcd\bin\dcd-server`) == "0.13.1");
587 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1-beta.4\dcd\bin\dcd-server`) == "0.13.1-beta.4");
588 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-master\dcd\bin\dcd-server`) == "vbin");
589 }
590 
591 /// searches for a semver in the given string starting after a - character,
592 /// returns everything until the end.
593 package string extractPathSemver(string s)
594 {
595 	import std.ascii;
596 
597 	foreach (start; 0 .. s.length)
598 	{
599 		// states:
600 		// -1 = error
601 		// 0 = expect -
602 		// 1 = expect major
603 		// 2 = expect major or .
604 		// 3 = expect minor
605 		// 4 = expect minor or .
606 		// 5 = expect patch
607 		// 6 = expect patch or - or + (valid)
608 		// 7 = skip (valid)
609 		int state = 0;
610 		foreach (i; start .. s.length)
611 		{
612 			auto c = s[i];
613 			switch (state)
614 			{
615 			case 0:
616 				if (c == '-')
617 					state++;
618 				else
619 					state = -1;
620 				break;
621 			case 1:
622 			case 3:
623 			case 5:
624 				if (c.isDigit)
625 					state++;
626 				else
627 					state = -1;
628 				break;
629 			case 2:
630 			case 4:
631 				if (c == '.')
632 					state++;
633 				else if (!c.isDigit)
634 					state = -1;
635 				break;
636 			case 6:
637 				if (c == '+' || c == '-')
638 					state = 7;
639 				else if (!c.isDigit)
640 					state = -1;
641 				break;
642 			default:
643 				break;
644 			}
645 
646 			if (state == -1)
647 				break;
648 		}
649 
650 		if (state >= 6)
651 			return s[start + 1 .. $];
652 	}
653 
654 	return null;
655 }
656 
657 unittest
658 {
659 	assert(extractPathSemver("foo-v1.0.0") is null);
660 	assert(extractPathSemver("foo-1.0.0") == "1.0.0");
661 	assert(extractPathSemver("foo-1.0.0-alpha.1-x") == "1.0.0-alpha.1-x");
662 	assert(extractPathSemver("foo-1.0.x") is null);
663 	assert(extractPathSemver("foo-x.0.0") is null);
664 	assert(extractPathSemver("foo-1.x.0") is null);
665 	assert(extractPathSemver("foo-1x.0.0") is null);
666 	assert(extractPathSemver("foo-1.0x.0") is null);
667 	assert(extractPathSemver("foo-1.0.0x") is null);
668 	assert(extractPathSemver("-1.0.0") == "1.0.0");
669 }