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