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.path : baseName, chainPath, dirName;
12 import std.regex : ctRegex, matchFirst;
13 import std.string : indexOf, indexOfAny, strip;
14 import std.traits;
15 
16 public import workspaced.backend;
17 public import workspaced.future;
18 
19 version (unittest)
20 {
21 	package import std.experimental.logger : trace;
22 }
23 else
24 {
25 	// dummy
26 	package void trace(Args...)(lazy Args)
27 	{
28 	}
29 }
30 
31 ///
32 alias ImportPathProvider = string[] delegate() nothrow;
33 ///
34 alias IdentifierListProvider = string[] delegate() nothrow;
35 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails)
36 /// Params:
37 /// 	instance = the instance for which the component was attempted to initialize (or null for global component registration)
38 /// 	factory = the factory on which the error occured with
39 /// 	error = the stacktrace that was catched on the bind call
40 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance instance,
41 		ComponentFactory factory, Exception error);
42 
43 interface IMessageHandler
44 {
45 	void warn(WorkspaceD.Instance instance, string component, int id, string message, string details = null);
46 	void error(WorkspaceD.Instance instance, string component, int id, string message, string details = null);
47 	void handleCrash(WorkspaceD.Instance instance, string component, ComponentWrapper componentInstance);
48 }
49 
50 /// UDA; will never try to call this function from rpc
51 enum ignoredFunc;
52 
53 /// Component call
54 struct ComponentInfoParams
55 {
56 	/// Name of the component
57 	string name;
58 }
59 
60 ComponentInfoParams component(string name)
61 {
62 	return ComponentInfoParams(name);
63 }
64 
65 struct ComponentInfo
66 {
67 	ComponentInfoParams params;
68 	TypeInfo type;
69 
70 	alias params this;
71 }
72 
73 void traceTaskLog(lazy string msg)
74 {
75 	import std.stdio : stderr;
76 
77 	debug (Tasks)
78 		stderr.writeln(msg);
79 }
80 
81 static immutable traceTask = `traceTaskLog("new task in " ~ __PRETTY_FUNCTION__); scope (exit) traceTaskLog(__PRETTY_FUNCTION__ ~ " exited");`;
82 
83 mixin template DefaultComponentWrapper(bool withDtor = true)
84 {
85 	@ignoredFunc
86 	{
87 		import std.algorithm : min, max;
88 		import std.parallelism : TaskPool, Task, task, defaultPoolThreads;
89 
90 		WorkspaceD workspaced;
91 		WorkspaceD.Instance refInstance;
92 
93 		TaskPool _threads;
94 
95 		static if (withDtor)
96 		{
97 			~this()
98 			{
99 				shutdown(true);
100 			}
101 		}
102 
103 		TaskPool gthreads()
104 		{
105 			return workspaced.gthreads;
106 		}
107 
108 		TaskPool threads(int minSize, int maxSize)
109 		{
110 			if (!_threads)
111 				synchronized (this)
112 					if (!_threads)
113 					{
114 						_threads = new TaskPool(max(minSize, min(maxSize, defaultPoolThreads)));
115 						_threads.isDaemon = true;
116 					}
117 			return _threads;
118 		}
119 
120 		inout(WorkspaceD.Instance) instance() inout @property
121 		{
122 			if (refInstance)
123 				return refInstance;
124 			else
125 				throw new Exception("Attempted to access instance in a global context");
126 		}
127 
128 		WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property
129 		{
130 			return refInstance = instance;
131 		}
132 
133 		string[] importPaths() const @property
134 		{
135 			return instance.importPathProvider ? instance.importPathProvider() : [];
136 		}
137 
138 		string[] stringImportPaths() const @property
139 		{
140 			return instance.stringImportPathProvider ? instance.stringImportPathProvider() : [];
141 		}
142 
143 		string[] importFiles() const @property
144 		{
145 			return instance.importFilesProvider ? instance.importFilesProvider() : [];
146 		}
147 
148 		/// Lists the project defined version identifiers, if provided by any identifier
149 		string[] projectVersions() const @property
150 		{
151 			return instance.projectVersionsProvider ? instance.projectVersionsProvider() : [];
152 		}
153 
154 		/// Lists the project defined debug specification identifiers, if provided by any provider 
155 		string[] debugSpecifications() const @property
156 		{
157 			return instance.debugSpecificationsProvider ? instance.debugSpecificationsProvider() : [];
158 		}
159 
160 		ref inout(ImportPathProvider) importPathProvider() @property inout
161 		{
162 			return instance.importPathProvider;
163 		}
164 
165 		ref inout(ImportPathProvider) stringImportPathProvider() @property inout
166 		{
167 			return instance.stringImportPathProvider;
168 		}
169 
170 		ref inout(ImportPathProvider) importFilesProvider() @property inout
171 		{
172 			return instance.importFilesProvider;
173 		}
174 
175 		ref inout(IdentifierListProvider) projectVersionsProvider() @property inout
176 		{
177 			return instance.projectVersionsProvider;
178 		}
179 
180 		ref inout(IdentifierListProvider) debugSpecificationsProvider() @property inout
181 		{
182 			return instance.debugSpecificationsProvider;
183 		}
184 
185 		ref inout(Configuration) config() @property inout
186 		{
187 			if (refInstance)
188 				return refInstance.config;
189 			else if (workspaced)
190 				return workspaced.globalConfiguration;
191 			else
192 				assert(false, "Unbound component trying to access config.");
193 		}
194 
195 		bool has(T)()
196 		{
197 			if (refInstance)
198 				return refInstance.has!T;
199 			else if (workspaced)
200 				return workspaced.has!T;
201 			else
202 				assert(false, "Unbound component trying to check for component " ~ T.stringof ~ ".");
203 		}
204 
205 		T get(T)()
206 		{
207 			if (refInstance)
208 				return refInstance.get!T;
209 			else if (workspaced)
210 				return workspaced.get!T;
211 			else
212 				assert(false, "Unbound component trying to get component " ~ T.stringof ~ ".");
213 		}
214 
215 		string cwd() @property const
216 		{
217 			return instance.cwd;
218 		}
219 
220 		override void shutdown(bool dtor = false)
221 		{
222 			if (!dtor && _threads)
223 				_threads.finish();
224 		}
225 
226 		override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance)
227 		{
228 			this.workspaced = workspaced;
229 			this.instance = instance;
230 			static if (__traits(hasMember, typeof(this).init, "load"))
231 				load();
232 		}
233 	}
234 }
235 
236 interface ComponentWrapper
237 {
238 	void bind(WorkspaceD workspaced, WorkspaceD.Instance instance);
239 	void shutdown(bool dtor = false);
240 }
241 
242 interface ComponentFactory
243 {
244 	ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow;
245 	ComponentInfo info() @property const nothrow;
246 }
247 
248 struct ComponentFactoryInstance
249 {
250 	ComponentFactory factory;
251 	bool autoRegister;
252 	alias factory this;
253 }
254 
255 struct ComponentWrapperInstance
256 {
257 	ComponentWrapper wrapper;
258 	ComponentInfo info;
259 }
260 
261 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory
262 {
263 	ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow
264 	{
265 		auto wrapper = new T();
266 		try
267 		{
268 			wrapper.bind(workspaced, instance);
269 			return wrapper;
270 		}
271 		catch (Exception e)
272 		{
273 			error = e;
274 			return null;
275 		}
276 	}
277 
278 	ComponentInfo info() @property const nothrow
279 	{
280 		alias udas = getUDAs!(T, ComponentInfoParams);
281 		static assert(udas.length == 1, "Can't construct default component factory for "
282 				~ T.stringof ~ ", expected exactly 1 ComponentInfoParams instance attached to the type");
283 		return ComponentInfo(udas[0], typeid(T));
284 	}
285 }
286 
287 /// Describes what to insert/replace/delete to do something
288 struct CodeReplacement
289 {
290 	/// Range what to replace. If both indices are the same its inserting.
291 	size_t[2] range;
292 	/// Content to replace it with. Empty means remove.
293 	string content;
294 
295 	/// Applies this edit to a string.
296 	string apply(string code)
297 	{
298 		size_t min = range[0];
299 		size_t max = range[1];
300 		if (min > max)
301 		{
302 			min = range[1];
303 			max = range[0];
304 		}
305 		if (min >= code.length)
306 			return code ~ content;
307 		if (max >= code.length)
308 			return code[0 .. min] ~ content;
309 		return code[0 .. min] ~ content ~ code[max .. $];
310 	}
311 }
312 
313 /// Code replacements mapped to a file
314 struct FileChanges
315 {
316 	/// File path to change.
317 	string file;
318 	/// Replacements to apply.
319 	CodeReplacement[] replacements;
320 }
321 
322 package bool getConfigPath(string file, ref string retPath)
323 {
324 	foreach (dir; standardPaths(StandardPath.config, "workspace-d"))
325 	{
326 		auto path = chainPath(dir, file);
327 		if (path.exists)
328 		{
329 			retPath = path.array;
330 			return true;
331 		}
332 	}
333 	return false;
334 }
335 
336 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`;
337 bool checkVersion(string ver, int[3] target)
338 {
339 	auto match = ver.matchFirst(verRegex);
340 	if (!match)
341 		return false;
342 	const major = match[1].to!int;
343 	const minor = match[2].to!int;
344 	const patch = match[3].to!int;
345 	return checkVersion([major, minor, patch], target);
346 }
347 
348 bool checkVersion(int[3] ver, int[3] target)
349 {
350 	if (ver[0] > target[0])
351 		return true;
352 	if (ver[0] == target[0] && ver[1] > target[1])
353 		return true;
354 	if (ver[0] == target[0] && ver[1] == target[1] && ver[2] >= target[2])
355 		return true;
356 	return false;
357 }
358 
359 package string getVersionAndFixPath(ref string execPath)
360 {
361 	import std.process;
362 
363 	try
364 	{
365 		return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath);
366 	}
367 	catch (ProcessException e)
368 	{
369 		auto newPath = chainPath(thisExePath.dirName, execPath.baseName);
370 		if (exists(newPath))
371 		{
372 			execPath = newPath.array;
373 			return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath);
374 		}
375 		throw new Exception("Failed running program ['"
376 			~ execPath ~ "' '--version'] and no alternative existed in '"
377 			~ newPath.array.idup ~ "'.", e);
378 	}
379 }
380 
381 /// Set for some reason when compiling with `dub fetch` / `dub run` or sometimes
382 /// on self compilation.
383 /// Known strings: vbin, vdcd, vDCD
384 package bool isLocallyCompiledDCD(string v)
385 {
386 	import std.uni : sicmp;
387 
388 	return sicmp(v, "vbin") == 0 || sicmp(v, "vdcd") == 0;
389 }
390 
391 /// returns the version that is given or the version extracted from dub path if path is a dub path
392 package string orDubFetchFallback(string v, string path)
393 {
394 	if (v.isLocallyCompiledDCD)
395 	{
396 		auto dub = path.indexOf(`dub/packages`);
397 		if (dub == -1)
398 			dub = path.indexOf(`dub\packages`);
399 
400 		if (dub != -1)
401 		{
402 			dub += `dub/packages/`.length;
403 			auto end = path.indexOfAny(`\/`, dub);
404 
405 			if (end != -1)
406 			{
407 				path = path[dub .. end];
408 				auto semver = extractPathSemver(path);
409 				if (semver.length)
410 					return semver;
411 			}
412 		}
413 	}
414 	return v;
415 }
416 
417 unittest
418 {
419 	assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1/dcd/bin/dcd-server`) == "0.13.1");
420 	assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1-beta.4/dcd/bin/dcd-server`) == "0.13.1-beta.4");
421 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1\dcd\bin\dcd-server`) == "0.13.1");
422 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1-beta.4\dcd\bin\dcd-server`) == "0.13.1-beta.4");
423 	assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-master\dcd\bin\dcd-server`) == "vbin");
424 }
425 
426 /// searches for a semver in the given string starting after a - character,
427 /// returns everything until the end.
428 package string extractPathSemver(string s)
429 {
430 	import std.ascii;
431 
432 	foreach (start; 0 .. s.length)
433 	{
434 		// states:
435 		// -1 = error
436 		// 0 = expect -
437 		// 1 = expect major
438 		// 2 = expect major or .
439 		// 3 = expect minor
440 		// 4 = expect minor or .
441 		// 5 = expect patch
442 		// 6 = expect patch or - or + (valid)
443 		// 7 = skip (valid)
444 		int state = 0;
445 		foreach (i; start .. s.length)
446 		{
447 			auto c = s[i];
448 			switch (state)
449 			{
450 			case 0:
451 				if (c == '-')
452 					state++;
453 				else
454 					state = -1;
455 				break;
456 			case 1:
457 			case 3:
458 			case 5:
459 				if (c.isDigit)
460 					state++;
461 				else
462 					state = -1;
463 				break;
464 			case 2:
465 			case 4:
466 				if (c == '.')
467 					state++;
468 				else if (!c.isDigit)
469 					state = -1;
470 				break;
471 			case 6:
472 				if (c == '+' || c == '-')
473 					state = 7;
474 				else if (!c.isDigit)
475 					state = -1;
476 				break;
477 			default:
478 				break;
479 			}
480 
481 			if (state == -1)
482 				break;
483 		}
484 
485 		if (state >= 6)
486 			return s[start + 1 .. $];
487 	}
488 
489 	return null;
490 }
491 
492 unittest
493 {
494 	assert(extractPathSemver("foo-v1.0.0") is null);
495 	assert(extractPathSemver("foo-1.0.0") == "1.0.0");
496 	assert(extractPathSemver("foo-1.0.0-alpha.1-x") == "1.0.0-alpha.1-x");
497 	assert(extractPathSemver("foo-1.0.x") is null);
498 	assert(extractPathSemver("foo-x.0.0") is null);
499 	assert(extractPathSemver("foo-1.x.0") is null);
500 	assert(extractPathSemver("foo-1x.0.0") is null);
501 	assert(extractPathSemver("foo-1.0x.0") is null);
502 	assert(extractPathSemver("foo-1.0.0x") is null);
503 	assert(extractPathSemver("-1.0.0") == "1.0.0");
504 }
505 
506 version (unittest)
507 package string normLF(scope string str)
508 {
509 	import std.string : replace;
510 
511 	return str
512 		.replace("\r\n", "\n")
513 		.replace("\r", "\n");
514 }