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 }