1 module workspaced.api; 2 3 // debug = Tasks; 4 5 import core.time; 6 import dparse.lexer; 7 import painlessjson; 8 import standardpaths; 9 10 import std.algorithm; 11 import std.conv; 12 import std.exception; 13 import std.file; 14 import std.json; 15 import std.meta; 16 import std.parallelism; 17 import std.path; 18 import std.range; 19 import std.regex; 20 import std.string : strip; 21 import std.traits; 22 import std.typecons; 23 24 version (unittest) 25 { 26 version (Have_unit_threaded) package import unit_threaded.assertions; 27 28 package import std.experimental.logger : trace; 29 } 30 else 31 { 32 // dummy 33 pragma(inline, true) package void trace(Args...)(lazy Args) 34 { 35 } 36 } 37 38 /// 39 alias ImportPathProvider = string[] delegate() nothrow; 40 /// 41 alias BroadcastCallback = void delegate(WorkspaceD, WorkspaceD.Instance, JSONValue); 42 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails) 43 /// Params: 44 /// instance = the instance for which the component was attempted to initialize (or null for global component registration) 45 /// factory = the factory on which the error occured with 46 /// error = the stacktrace that was catched on the bind call 47 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance instance, 48 ComponentFactory factory, Exception error); 49 50 /// Will never call this function 51 enum ignoredFunc; 52 53 /// Component call 54 struct ComponentInfo 55 { 56 /// Name of the component 57 string name; 58 } 59 60 ComponentInfo component(string name) 61 { 62 return ComponentInfo(name); 63 } 64 65 void traceTaskLog(lazy string msg) 66 { 67 import std.stdio : stderr; 68 69 debug (Tasks) 70 stderr.writeln(msg); 71 } 72 73 static immutable traceTask = `traceTaskLog("new task in " ~ __PRETTY_FUNCTION__); scope (exit) traceTaskLog(__PRETTY_FUNCTION__ ~ " exited");`; 74 75 mixin template DefaultComponentWrapper(bool withDtor = true) 76 { 77 @ignoredFunc 78 { 79 import std.algorithm : min, max; 80 import std.parallelism : TaskPool, Task, task, defaultPoolThreads; 81 82 WorkspaceD workspaced; 83 WorkspaceD.Instance refInstance; 84 85 TaskPool _threads; 86 87 static if (withDtor) 88 { 89 ~this() 90 { 91 shutdown(true); 92 } 93 } 94 95 TaskPool gthreads() 96 { 97 return workspaced.gthreads; 98 } 99 100 TaskPool threads(int minSize, int maxSize) 101 { 102 if (!_threads) 103 synchronized (this) 104 if (!_threads) 105 _threads = new TaskPool(max(minSize, min(maxSize, defaultPoolThreads))); 106 return _threads; 107 } 108 109 WorkspaceD.Instance instance() const @property 110 { 111 if (refInstance) 112 return cast() refInstance; 113 else 114 throw new Exception("Attempted to access instance in a global context"); 115 } 116 117 WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property 118 { 119 return refInstance = instance; 120 } 121 122 string[] importPaths() const @property 123 { 124 return instance.importPathProvider ? instance.importPathProvider() : []; 125 } 126 127 string[] stringImportPaths() const @property 128 { 129 return instance.stringImportPathProvider ? instance.stringImportPathProvider() : []; 130 } 131 132 string[] importFiles() const @property 133 { 134 return instance.importFilesProvider ? instance.importFilesProvider() : []; 135 } 136 137 ref ImportPathProvider importPathProvider() @property 138 { 139 return instance.importPathProvider; 140 } 141 142 ref ImportPathProvider stringImportPathProvider() @property 143 { 144 return instance.stringImportPathProvider; 145 } 146 147 ref ImportPathProvider importFilesProvider() @property 148 { 149 return instance.importFilesProvider; 150 } 151 152 ref Configuration config() @property 153 { 154 if (refInstance) 155 return refInstance.config; 156 else if (workspaced) 157 return workspaced.globalConfiguration; 158 else 159 assert(false, "Unbound component trying to access config."); 160 } 161 162 bool has(T)() 163 { 164 if (refInstance) 165 return refInstance.has!T; 166 else if (workspaced) 167 return workspaced.has!T; 168 else 169 assert(false, "Unbound component trying to check for component " ~ T.stringof ~ "."); 170 } 171 172 T get(T)() 173 { 174 if (refInstance) 175 return refInstance.get!T; 176 else if (workspaced) 177 return workspaced.get!T; 178 else 179 assert(false, "Unbound component trying to get component " ~ T.stringof ~ "."); 180 } 181 182 string cwd() @property const 183 { 184 return instance.cwd; 185 } 186 187 override void shutdown(bool dtor = false) 188 { 189 if (!dtor && _threads) 190 _threads.finish(); 191 } 192 193 override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance) 194 { 195 this.workspaced = workspaced; 196 this.instance = instance; 197 static if (__traits(hasMember, typeof(this).init, "load")) 198 load(); 199 } 200 201 import std.conv; 202 import std.json : JSONValue; 203 import std.traits : isFunction, hasUDA, ParameterDefaults, Parameters, ReturnType; 204 import painlessjson; 205 206 override Future!JSONValue run(string method, JSONValue[] args) 207 { 208 static foreach (member; __traits(derivedMembers, typeof(this))) 209 static if (member[0] != '_' && __traits(compiles, __traits(getMember, 210 typeof(this).init, member)) && __traits(getProtection, __traits(getMember, typeof(this).init, 211 member)) == "public" && __traits(compiles, isFunction!(__traits(getMember, 212 typeof(this).init, member))) && isFunction!(__traits(getMember, 213 typeof(this).init, member)) && !hasUDA!(__traits(getMember, typeof(this).init, 214 member), ignoredFunc) && !__traits(isTemplate, __traits(getMember, 215 typeof(this).init, member))) 216 if (method == member) 217 return runMethod!member(args); 218 throw new Exception("Method " ~ method ~ " not found."); 219 } 220 221 Future!JSONValue runMethod(string method)(JSONValue[] args) 222 { 223 int matches; 224 static foreach (overload; __traits(getOverloads, typeof(this), method)) 225 { 226 if (matchesOverload!overload(args)) 227 matches++; 228 } 229 if (matches == 0) 230 throw new Exception("No suitable overload found for " ~ method ~ "."); 231 if (matches > 1) 232 throw new Exception("Multiple overloads found for " ~ method ~ "."); 233 static foreach (overload; __traits(getOverloads, typeof(this), method)) 234 { 235 if (matchesOverload!overload(args)) 236 return runOverload!overload(args); 237 } 238 assert(false); 239 } 240 241 Future!JSONValue runOverload(alias fun)(JSONValue[] args) 242 { 243 mixin(generateOverloadCall!fun); 244 } 245 246 static string generateOverloadCall(alias fun)() 247 { 248 string call = "fun("; 249 static foreach (i, T; Parameters!fun) 250 { 251 static if (is(T : const(char)[])) 252 call ~= "args[" ~ i.to!string ~ "].str, "; 253 else 254 call ~= "args[" ~ i.to!string ~ "].fromJSON!(" ~ T.stringof ~ "), "; 255 } 256 call ~= ")"; 257 static if (is(ReturnType!fun : Future!T, T)) 258 { 259 static if (is(T == void)) 260 string conv = "ret.finish(JSONValue(null));"; 261 else 262 string conv = "ret.finish(v.value.toJSON);"; 263 return "auto ret = new Future!JSONValue; auto v = " ~ call 264 ~ "; v.onDone = { if (v.exception) ret.error(v.exception); else " 265 ~ conv ~ " }; return ret;"; 266 } 267 else static if (is(ReturnType!fun == void)) 268 return call ~ "; return Future!JSONValue.fromResult(JSONValue(null));"; 269 else 270 return "return Future!JSONValue.fromResult(" ~ call ~ ".toJSON);"; 271 } 272 } 273 } 274 275 bool matchesOverload(alias fun)(JSONValue[] args) 276 { 277 if (args.length > Parameters!fun.length) 278 return false; 279 static foreach (i, def; ParameterDefaults!fun) 280 { 281 static if (is(def == void)) 282 { 283 if (i >= args.length) 284 return false; 285 else if (!checkType!(Parameters!fun[i])(args[i])) 286 return false; 287 } 288 } 289 return true; 290 } 291 292 bool checkType(T)(JSONValue value) 293 { 294 final switch (value.type) 295 { 296 case JSONType.array: 297 static if (isStaticArray!T) 298 return T.length == value.array.length 299 && value.array.all!(checkType!(typeof(T.init[0]))); 300 else static if (isDynamicArray!T) 301 return value.array.all!(checkType!(typeof(T.init[0]))); 302 else static if (is(T : Tuple!Args, Args...)) 303 { 304 if (value.array.length != Args.length) 305 return false; 306 static foreach (i, Arg; Args) 307 if (!checkType!Arg(value.array[i])) 308 return false; 309 return true; 310 } 311 else 312 return false; 313 case JSONType.false_: 314 case JSONType.true_: 315 return is(T : bool); 316 case JSONType.float_: 317 return isNumeric!T; 318 case JSONType.integer: 319 case JSONType.uinteger: 320 return isIntegral!T; 321 case JSONType.null_: 322 static if (is(T == class) || isArray!T || isPointer!T || is(T : Nullable!U, U)) 323 return true; 324 else 325 return false; 326 case JSONType.object: 327 return is(T == class) || is(T == struct); 328 case JSONType..string: 329 return isSomeString!T; 330 } 331 } 332 333 interface ComponentWrapper 334 { 335 void bind(WorkspaceD workspaced, WorkspaceD.Instance instance); 336 Future!JSONValue run(string method, JSONValue[] args); 337 void shutdown(bool dtor = false); 338 } 339 340 interface ComponentFactory 341 { 342 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error); 343 ComponentInfo info() @property; 344 } 345 346 struct ComponentFactoryInstance 347 { 348 ComponentFactory factory; 349 bool autoRegister; 350 alias factory this; 351 } 352 353 struct ComponentWrapperInstance 354 { 355 ComponentWrapper wrapper; 356 ComponentInfo info; 357 } 358 359 struct Configuration 360 { 361 /// JSON containing base configuration formatted as {[component]:{key:value pairs}} 362 JSONValue base; 363 364 bool get(string component, string key, out JSONValue val) 365 { 366 if (base.type != JSONType.object) 367 { 368 JSONValue[string] tmp; 369 base = JSONValue(tmp); 370 } 371 auto com = component in base.object; 372 if (!com) 373 return false; 374 auto v = key in *com; 375 if (!v) 376 return false; 377 val = *v; 378 return true; 379 } 380 381 T get(T)(string component, string key, T defaultValue = T.init) 382 { 383 JSONValue ret; 384 if (!get(component, key, ret)) 385 return defaultValue; 386 return ret.fromJSON!T; 387 } 388 389 bool set(T)(string component, string key, T value) 390 { 391 if (base.type != JSONType.object) 392 { 393 JSONValue[string] tmp; 394 base = JSONValue(tmp); 395 } 396 auto com = component in base.object; 397 if (!com) 398 { 399 JSONValue[string] val; 400 val[key] = value.toJSON; 401 base.object[component] = JSONValue(val); 402 } 403 else 404 { 405 com.object[key] = value.toJSON; 406 } 407 return true; 408 } 409 410 /// Same as init but might make nicer code. 411 static immutable Configuration none = Configuration.init; 412 413 /// Loads unset keys from global, keeps existing keys 414 void loadBase(Configuration global) 415 { 416 if (global.base.type != JSONType.object) 417 return; 418 419 if (base.type != JSONType.object) 420 base = global.base.dupJson; 421 else 422 { 423 foreach (component, config; global.base.object) 424 { 425 auto existing = component in base.object; 426 if (!existing || config.type != JSONType.object) 427 base.object[component] = config.dupJson; 428 else 429 { 430 foreach (key, value; config.object) 431 { 432 auto existingValue = key in *existing; 433 if (!existingValue) 434 (*existing)[key] = value.dupJson; 435 } 436 } 437 } 438 } 439 } 440 } 441 442 private JSONValue dupJson(JSONValue v) 443 { 444 switch (v.type) 445 { 446 case JSONType.object: 447 return JSONValue(v.object.dup); 448 case JSONType.array: 449 return JSONValue(v.array.dup); 450 default: 451 return v; 452 } 453 } 454 455 /// WorkspaceD instance holding plugins. 456 class WorkspaceD 457 { 458 static class Instance 459 { 460 string cwd; 461 ComponentWrapperInstance[] instanceComponents; 462 Configuration config; 463 464 string[] importPaths() const @property nothrow 465 { 466 return importPathProvider ? importPathProvider() : []; 467 } 468 469 string[] stringImportPaths() const @property nothrow 470 { 471 return stringImportPathProvider ? stringImportPathProvider() : []; 472 } 473 474 string[] importFiles() const @property nothrow 475 { 476 return importFilesProvider ? importFilesProvider() : []; 477 } 478 479 void shutdown(bool dtor = false) 480 { 481 foreach (ref com; instanceComponents) 482 com.wrapper.shutdown(dtor); 483 instanceComponents = null; 484 } 485 486 ImportPathProvider importPathProvider; 487 ImportPathProvider stringImportPathProvider; 488 ImportPathProvider importFilesProvider; 489 490 Future!JSONValue run(WorkspaceD workspaced, string component, string method, JSONValue[] args) 491 { 492 foreach (ref com; instanceComponents) 493 if (com.info.name == component) 494 return com.wrapper.run(method, args); 495 throw new Exception("Component '" ~ component ~ "' not found"); 496 } 497 498 inout(T) get(T)() inout 499 { 500 auto name = getUDAs!(T, ComponentInfo)[0].name; 501 foreach (com; instanceComponents) 502 if (com.info.name == name) 503 return cast(inout T) com.wrapper; 504 throw new Exception( 505 "Attempted to get unknown instance component " ~ T.stringof ~ " in instance cwd:" ~ cwd); 506 } 507 508 bool has(T)() const 509 { 510 auto name = getUDAs!(T, ComponentInfo)[0].name; 511 foreach (com; instanceComponents) 512 if (com.info.name == name) 513 return true; 514 return false; 515 } 516 517 /// Loads a registered component which didn't have auto register on just for this instance. 518 /// Returns: false instead of using the onBindFail callback on failure. 519 /// Throws: Exception if component was not registered in workspaced. 520 bool attach(T)(WorkspaceD workspaced) 521 { 522 string name = getUDAs!(T, ComponentInfo)[0].name; 523 foreach (factory; workspaced.components) 524 { 525 if (factory.info.name == name) 526 { 527 auto inst = factory.create(workspaced, this); 528 if (inst) 529 { 530 instanceComponents ~= ComponentWrapperInstance(inst, info); 531 return true; 532 } 533 else 534 return false; 535 } 536 } 537 throw new Exception("Component not found"); 538 } 539 } 540 541 /// Event which is called when $(LREF broadcast) is called 542 BroadcastCallback onBroadcast; 543 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails) 544 /// See_Also: $(LREF ComponentBindFailCallback) 545 ComponentBindFailCallback onBindFail; 546 547 Instance[] instances; 548 /// Base global configuration for new instances, does not modify existing ones. 549 Configuration globalConfiguration; 550 ComponentWrapperInstance[] globalComponents; 551 ComponentFactoryInstance[] components; 552 StringCache stringCache; 553 554 TaskPool _gthreads; 555 556 this() 557 { 558 stringCache = StringCache(StringCache.defaultBucketCount * 4); 559 } 560 561 ~this() 562 { 563 shutdown(true); 564 } 565 566 void shutdown(bool dtor = false) 567 { 568 foreach (ref instance; instances) 569 instance.shutdown(dtor); 570 instances = null; 571 foreach (ref com; globalComponents) 572 com.wrapper.shutdown(dtor); 573 globalComponents = null; 574 components = null; 575 if (_gthreads) 576 _gthreads.finish(true); 577 _gthreads = null; 578 } 579 580 void broadcast(WorkspaceD.Instance instance, JSONValue value) 581 { 582 if (onBroadcast) 583 onBroadcast(this, instance, value); 584 } 585 586 Instance getInstance(string cwd) nothrow 587 { 588 cwd = buildNormalizedPath(cwd); 589 foreach (instance; instances) 590 if (instance.cwd == cwd) 591 return instance; 592 return null; 593 } 594 595 Instance getBestInstanceByDependency(WithComponent)(string file) nothrow 596 { 597 Instance best; 598 size_t bestLength; 599 foreach (instance; instances) 600 { 601 foreach (folder; chain(instance.importPaths, instance.importFiles, 602 instance.stringImportPaths)) 603 { 604 if (folder.length > bestLength && file.startsWith(folder) && instance.has!WithComponent) 605 { 606 best = instance; 607 bestLength = folder.length; 608 } 609 } 610 } 611 return best; 612 } 613 614 Instance getBestInstanceByDependency(string file) nothrow 615 { 616 Instance best; 617 size_t bestLength; 618 foreach (instance; instances) 619 { 620 foreach (folder; chain(instance.importPaths, instance.importFiles, 621 instance.stringImportPaths)) 622 { 623 if (folder.length > bestLength && file.startsWith(folder)) 624 { 625 best = instance; 626 bestLength = folder.length; 627 } 628 } 629 } 630 return best; 631 } 632 633 Instance getBestInstance(WithComponent)(string file, bool fallback = true) nothrow 634 { 635 file = buildNormalizedPath(file); 636 Instance ret = null; 637 size_t best; 638 foreach (instance; instances) 639 { 640 if (instance.cwd.length > best && file.startsWith(instance.cwd) && instance 641 .has!WithComponent) 642 { 643 ret = instance; 644 best = instance.cwd.length; 645 } 646 } 647 if (!ret && fallback) 648 { 649 ret = getBestInstanceByDependency!WithComponent(file); 650 if (ret) 651 return ret; 652 foreach (instance; instances) 653 if (instance.has!WithComponent) 654 return instance; 655 } 656 return ret; 657 } 658 659 Instance getBestInstance(string file, bool fallback = true) nothrow 660 { 661 file = buildNormalizedPath(file); 662 Instance ret = null; 663 size_t best; 664 foreach (instance; instances) 665 { 666 if (instance.cwd.length > best && file.startsWith(instance.cwd)) 667 { 668 ret = instance; 669 best = instance.cwd.length; 670 } 671 } 672 if (!ret && fallback && instances.length) 673 { 674 ret = getBestInstanceByDependency(file); 675 if (!ret) 676 ret = instances[0]; 677 } 678 return ret; 679 } 680 681 T get(T)() 682 { 683 auto name = getUDAs!(T, ComponentInfo)[0].name; 684 foreach (com; globalComponents) 685 if (com.info.name == name) 686 return cast(T) com.wrapper; 687 throw new Exception("Attempted to get unknown global component " ~ T.stringof); 688 } 689 690 bool has(T)() 691 { 692 auto name = getUDAs!(T, ComponentInfo)[0].name; 693 foreach (com; globalComponents) 694 if (com.info.name == name) 695 return true; 696 return false; 697 } 698 699 T get(T)(string cwd) 700 { 701 if (!cwd.length) 702 return this.get!T; 703 auto inst = getInstance(cwd); 704 if (inst is null) 705 throw new Exception("cwd '" ~ cwd ~ "' not found"); 706 return inst.get!T; 707 } 708 709 bool has(T)(string cwd) 710 { 711 auto inst = getInstance(cwd); 712 if (inst is null) 713 return false; 714 return inst.has!T; 715 } 716 717 T best(T)(string file, bool fallback = true) 718 { 719 if (!file.length) 720 return this.get!T; 721 auto inst = getBestInstance!T(file); 722 if (inst is null) 723 throw new Exception("cwd for '" ~ file ~ "' not found"); 724 return inst.get!T; 725 } 726 727 bool hasBest(T)(string cwd, bool fallback = true) 728 { 729 auto inst = getBestInstance!T(cwd); 730 if (inst is null) 731 return false; 732 return inst.has!T; 733 } 734 735 Future!JSONValue run(string cwd, string component, string method, JSONValue[] args) 736 { 737 auto instance = getInstance(cwd); 738 if (instance is null) 739 throw new Exception("cwd '" ~ cwd ~ "' not found"); 740 return instance.run(this, component, method, args); 741 } 742 743 Future!JSONValue run(string component, string method, JSONValue[] args) 744 { 745 foreach (ref com; globalComponents) 746 if (com.info.name == component) 747 return com.wrapper.run(method, args); 748 throw new Exception("Global component '" ~ component ~ "' not found"); 749 } 750 751 ComponentFactory register(T)(bool autoRegister = true) 752 { 753 ComponentFactory factory; 754 static foreach (attr; __traits(getAttributes, T)) 755 static if (is(attr == class) && is(attr : ComponentFactory)) 756 factory = new attr; 757 if (factory is null) 758 factory = new DefaultComponentFactory!T; 759 components ~= ComponentFactoryInstance(factory, autoRegister); 760 auto info = factory.info; 761 Exception error; 762 auto glob = factory.create(this, null, error); 763 if (glob) 764 globalComponents ~= ComponentWrapperInstance(glob, info); 765 else if (onBindFail) 766 onBindFail(null, factory, error); 767 768 if (autoRegister) 769 foreach (ref instance; instances) 770 { 771 auto inst = factory.create(this, instance, error); 772 if (inst) 773 instance.instanceComponents ~= ComponentWrapperInstance(inst, info); 774 else if (onBindFail) 775 onBindFail(instance, factory, error); 776 } 777 static if (__traits(compiles, T.registered(this))) 778 T.registered(this); 779 else static if (__traits(compiles, T.registered())) 780 T.registered(); 781 return factory; 782 } 783 784 /// Creates a new workspace with the given cwd with optional config overrides and preload components for non-autoRegister components. 785 /// Throws: Exception if normalized cwd already exists as instance. 786 Instance addInstance(string cwd, 787 Configuration configOverrides = Configuration.none, string[] preloadComponents = []) 788 { 789 cwd = buildNormalizedPath(cwd); 790 if (instances.canFind!(a => a.cwd == cwd)) 791 throw new Exception("Instance with cwd '" ~ cwd ~ "' already exists!"); 792 auto inst = new Instance(); 793 inst.cwd = cwd; 794 configOverrides.loadBase(globalConfiguration); 795 inst.config = configOverrides; 796 instances ~= inst; 797 foreach (name; preloadComponents) 798 { 799 foreach (factory; components) 800 { 801 if (!factory.autoRegister && factory.info.name == name) 802 { 803 Exception error; 804 auto wrap = factory.create(this, inst, error); 805 if (wrap) 806 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 807 else if (onBindFail) 808 onBindFail(inst, factory, error); 809 break; 810 } 811 } 812 } 813 foreach (factory; components) 814 { 815 if (factory.autoRegister) 816 { 817 Exception error; 818 auto wrap = factory.create(this, inst, error); 819 if (wrap) 820 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 821 else if (onBindFail) 822 onBindFail(inst, factory, error); 823 } 824 } 825 return inst; 826 } 827 828 bool removeInstance(string cwd) 829 { 830 cwd = buildNormalizedPath(cwd); 831 foreach (i, instance; instances) 832 if (instance.cwd == cwd) 833 { 834 foreach (com; instance.instanceComponents) 835 destroy(com.wrapper); 836 destroy(instance); 837 instances = instances.remove(i); 838 return true; 839 } 840 return false; 841 } 842 843 deprecated("Use overload taking an out Exception error or attachSilent instead") bool attach( 844 Instance instance, string component) 845 { 846 return attachSilent(instance, component); 847 } 848 849 bool attachSilent(Instance instance, string component) 850 { 851 Exception error; 852 return attach(instance, component, error); 853 } 854 855 bool attach(Instance instance, string component, out Exception error) 856 { 857 foreach (factory; components) 858 { 859 if (factory.info.name == component) 860 { 861 auto wrap = factory.create(this, instance, error); 862 if (wrap) 863 { 864 instance.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 865 return true; 866 } 867 else 868 return false; 869 } 870 } 871 return false; 872 } 873 874 TaskPool gthreads() 875 { 876 if (!_gthreads) 877 synchronized (this) 878 if (!_gthreads) 879 _gthreads = new TaskPool(max(2, min(6, defaultPoolThreads))); 880 return _gthreads; 881 } 882 } 883 884 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory 885 { 886 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) 887 { 888 auto wrapper = new T(); 889 try 890 { 891 wrapper.bind(workspaced, instance); 892 return wrapper; 893 } 894 catch (Exception e) 895 { 896 error = e; 897 return null; 898 } 899 } 900 901 ComponentInfo info() @property 902 { 903 alias udas = getUDAs!(T, ComponentInfo); 904 static assert(udas.length == 1, "Can't construct default component factory for " 905 ~ T.stringof ~ ", expected exactly 1 ComponentInfo instance attached to the type"); 906 return udas[0]; 907 } 908 } 909 910 /// Describes what to insert/replace/delete to do something 911 struct CodeReplacement 912 { 913 /// Range what to replace. If both indices are the same its inserting. 914 size_t[2] range; 915 /// Content to replace it with. Empty means remove. 916 string content; 917 918 /// Applies this edit to a string. 919 string apply(string code) 920 { 921 size_t min = range[0]; 922 size_t max = range[1]; 923 if (min > max) 924 { 925 min = range[1]; 926 max = range[0]; 927 } 928 if (min >= code.length) 929 return code ~ content; 930 if (max >= code.length) 931 return code[0 .. min] ~ content; 932 return code[0 .. min] ~ content ~ code[max .. $]; 933 } 934 } 935 936 /// Code replacements mapped to a file 937 struct FileChanges 938 { 939 /// File path to change. 940 string file; 941 /// Replacements to apply. 942 CodeReplacement[] replacements; 943 } 944 945 package bool getConfigPath(string file, ref string retPath) 946 { 947 foreach (dir; standardPaths(StandardPath.config, "workspace-d")) 948 { 949 auto path = buildPath(dir, file); 950 if (path.exists) 951 { 952 retPath = path; 953 return true; 954 } 955 } 956 return false; 957 } 958 959 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`; 960 bool checkVersion(string ver, int[3] target) 961 { 962 auto match = ver.matchFirst(verRegex); 963 if (!match) 964 return false; 965 int major = match[1].to!int; 966 int minor = match[2].to!int; 967 int patch = match[3].to!int; 968 if (major > target[0]) 969 return true; 970 if (major == target[0] && minor > target[1]) 971 return true; 972 if (major == target[0] && minor == target[1] && patch >= target[2]) 973 return true; 974 return false; 975 } 976 977 package string getVersionAndFixPath(ref string execPath) 978 { 979 import std.process; 980 981 try 982 { 983 return execute([execPath, "--version"]).output.strip; 984 } 985 catch (ProcessException e) 986 { 987 auto newPath = buildPath(thisExePath.dirName, execPath.baseName); 988 if (exists(newPath)) 989 { 990 execPath = newPath; 991 return execute([execPath, "--version"]).output.strip; 992 } 993 throw e; 994 } 995 } 996 997 class Future(T) 998 { 999 import core.thread : Fiber, Thread; 1000 1001 static if (!is(T == void)) 1002 T value; 1003 Throwable exception; 1004 bool has; 1005 void delegate() _onDone; 1006 private Thread _worker; 1007 1008 /// Sets the onDone callback if no value has been set yet or calls immediately if the value has already been set or was set during setting the callback. 1009 /// Crashes with an assert error if attempting to override an existing callback (i.e. calling this function on the same object twice). 1010 void onDone(void delegate() callback) @property 1011 { 1012 assert(!_onDone); 1013 if (has) 1014 callback(); 1015 else 1016 { 1017 bool called; 1018 _onDone = { called = true; callback(); }; 1019 if (has && !called) 1020 callback(); 1021 } 1022 } 1023 1024 static if (is(T == void)) 1025 static Future!void finished() 1026 { 1027 auto ret = new Future!void; 1028 ret.has = true; 1029 return ret; 1030 } 1031 else 1032 static Future!T fromResult(T value) 1033 { 1034 auto ret = new Future!T; 1035 ret.value = value; 1036 ret.has = true; 1037 return ret; 1038 } 1039 1040 static Future!T async(T delegate() cb) 1041 { 1042 auto ret = new Future!T; 1043 ret._worker = new Thread({ 1044 try 1045 { 1046 static if (is(T == void)) 1047 { 1048 cb(); 1049 ret.finish(); 1050 } 1051 else 1052 ret.finish(cb()); 1053 } 1054 catch (Throwable t) 1055 { 1056 ret.error(t); 1057 } 1058 }).start(); 1059 return ret; 1060 } 1061 1062 static Future!T fromError(T)(Throwable error) 1063 { 1064 auto ret = new Future!T; 1065 ret.error = error; 1066 ret.has = true; 1067 return ret; 1068 } 1069 1070 static if (is(T == void)) 1071 void finish() 1072 { 1073 assert(!has); 1074 has = true; 1075 if (_onDone) 1076 _onDone(); 1077 } 1078 else 1079 void finish(T value) 1080 { 1081 assert(!has); 1082 this.value = value; 1083 has = true; 1084 if (_onDone) 1085 _onDone(); 1086 } 1087 1088 void error(Throwable t) 1089 { 1090 assert(!has); 1091 exception = t; 1092 has = true; 1093 if (_onDone) 1094 _onDone(); 1095 } 1096 1097 /// Waits for the result of this future using Thread.sleep 1098 T getBlocking(alias sleepDur = 1.msecs)() 1099 { 1100 while (!has) 1101 Thread.sleep(sleepDur); 1102 if (_worker) 1103 { 1104 _worker.join(); 1105 _worker = null; 1106 } 1107 if (exception) 1108 throw exception; 1109 static if (!is(T == void)) 1110 return value; 1111 } 1112 1113 /// Waits for the result of this future using Fiber.yield 1114 T getYield() 1115 { 1116 while (!has) 1117 Fiber.yield(); 1118 if (_worker) 1119 { 1120 _worker.join(); 1121 _worker = null; 1122 } 1123 if (exception) 1124 throw exception; 1125 static if (!is(T == void)) 1126 return value; 1127 } 1128 } 1129 1130 enum string gthreadsAsyncProxy(string call) = `auto __futureRet = new typeof(return); 1131 gthreads.create({ 1132 mixin(traceTask); 1133 try 1134 { 1135 __futureRet.finish(` ~ call ~ `); 1136 } 1137 catch (Throwable t) 1138 { 1139 __futureRet.error(t); 1140 } 1141 }); 1142 return __futureRet; 1143 `; 1144 1145 version (unittest) 1146 { 1147 struct TestingWorkspace 1148 { 1149 string directory; 1150 1151 @disable this(this); 1152 1153 this(string path) 1154 { 1155 if (path.exists) 1156 throw new Exception("Path already exists"); 1157 directory = path; 1158 mkdir(path); 1159 } 1160 1161 ~this() 1162 { 1163 rmdirRecurse(directory); 1164 } 1165 1166 string getPath(string path) 1167 { 1168 return buildPath(directory, path); 1169 } 1170 1171 void createDir(string dir) 1172 { 1173 mkdirRecurse(getPath(dir)); 1174 } 1175 1176 void writeFile(string path, string content) 1177 { 1178 write(getPath(path), content); 1179 } 1180 } 1181 1182 TestingWorkspace makeTemporaryTestingWorkspace() 1183 { 1184 import std.random; 1185 1186 return TestingWorkspace(buildPath(tempDir, "workspace-d-test-" ~ uniform(0, 1187 int.max).to!string(36))); 1188 } 1189 } 1190 1191 void create(T)(TaskPool pool, T fun) if (isCallable!T) 1192 { 1193 pool.put(task(fun)); 1194 }