1 module workspaced.backend; 2 3 import dparse.lexer : StringCache; 4 5 import std.algorithm : canFind, map, max, min, remove, startsWith; 6 import std.array : array; 7 import std.conv; 8 import std.file : exists, mkdir, mkdirRecurse, rmdirRecurse, tempDir, write; 9 import std.parallelism : defaultPoolThreads, TaskPool; 10 import std.path : buildNormalizedPath, buildPath; 11 import std.range : chain; 12 import std.sumtype : match, SumType, This; 13 import std.traits : getUDAs, isSomeString; 14 15 import workspaced.api; 16 17 struct Configuration 18 { 19 alias ValueT = SumType!(typeof(null), string, bool, long, double, This[], This[string]); 20 21 private static ValueT toValueT(T)(T value) 22 { 23 static if (is(immutable T == immutable ValueT)) 24 return value; 25 else static if (is(T : U[], U)) 26 { 27 ValueT[] ret = new ValueT[value.length]; 28 foreach (i, v; value) 29 ret[i] = toValueT(v); 30 return ValueT(ret); 31 } 32 else static if (is(T : U[string], U)) 33 { 34 ValueT[string] ret; 35 foreach (k, v; value) 36 ret[k] = toValueT(v); 37 return ValueT(ret); 38 } 39 else 40 { 41 return ValueT(value); 42 } 43 } 44 45 static T toType(T)(ValueT value) 46 { 47 return value.match!( 48 (typeof(null) n) { 49 if (false) return T.init; // make return type T 50 51 static if (__traits(compiles, T.init is null) && T.init is null) 52 return T.init; 53 else 54 throw new Exception("Cannot convert null to type " ~ T.stringof); 55 }, 56 (ValueT[] v) { 57 if (false) return T.init; // make return type T 58 59 static if (is(typeof(T.init[0]))) 60 { 61 T ret; 62 ret.reserve(v.length); 63 foreach (i; v) 64 ret ~= toType!(typeof(T.init[0]))(i); 65 return ret; 66 } 67 else 68 throw new Exception("Cannot convert array to type " ~ T.stringof); 69 }, 70 (ValueT[string] m) { 71 if (false) return T.init; // make return type T 72 73 static if (is(typeof(T.init[""]))) 74 { 75 T ret; 76 foreach (k, v; m) 77 ret[k] = toType!(typeof(ret[k]))(v); 78 return ret; 79 } 80 else 81 throw new Exception("Cannot convert map to type " ~ T.stringof); 82 }, 83 (s) { 84 if (false) return T.init; // make return type T 85 86 static if (is(T : typeof(s))) 87 return cast(T) s; 88 else static if (__traits(compiles, s.to!T)) 89 return s.to!T; 90 else 91 throw new Exception("Cannot convert " ~ typeof(s).stringof 92 ~ " to type " ~ T.stringof); 93 } 94 ); 95 } 96 97 static struct Section 98 { 99 ValueT[string] values; 100 } 101 102 /// base configuration formatted as {[component]:{key:value pairs}} 103 Section[string] base; 104 105 bool get(string component, string key, out ValueT val) const 106 { 107 auto com = component in base; 108 if (!com) 109 return false; 110 auto v = key in com.values; 111 if (!v) 112 return false; 113 val = *v; 114 return true; 115 } 116 117 T get(T)(string component, string key, T defaultValue = T.init) inout 118 { 119 ValueT ret; 120 if (!get(component, key, ret)) 121 return defaultValue; 122 return toType!T(ret); 123 } 124 125 bool set(T)(string component, string key, T value) 126 { 127 if (auto com = component in base) 128 { 129 com.values[key] = toValueT(value); 130 } 131 else 132 { 133 ValueT[string] val; 134 val[key] = toValueT(value); 135 base[component] = Section(val); 136 } 137 return true; 138 } 139 140 /// Same as init but might make nicer code. 141 enum none = Configuration.init; 142 143 /// Loads unset keys from global, keeps existing keys 144 void loadBase(Configuration global) 145 { 146 if (base is null) 147 base = global.base.dup; 148 else 149 { 150 foreach (component, config; global.base) 151 { 152 auto existing = component in base; 153 if (!existing) 154 base[component] = config.deepCopy; 155 else 156 { 157 foreach (key, value; config.values) 158 { 159 auto existingValue = key in existing.values; 160 if (!existingValue) 161 existing.values[key] = value.deepCopy; 162 } 163 } 164 } 165 } 166 } 167 } 168 169 private T deepCopy(T)(T value) 170 { 171 static if (is(T : typeof(null)) || __traits(isPOD, T)) 172 return value; 173 else static if (is(T == Configuration.ValueT)) 174 { 175 return value.match!(v => deepCopy(v)); 176 } 177 else static if (is(T : Configuration.Section)) 178 { 179 return Configuration.Section(deepCopy(value.values)); 180 } 181 else static if (is(T : Configuration.ValueT[])) 182 { 183 auto copy = new Configuration.ValueT[value.length]; 184 foreach (i, ref v; copy) 185 v = deepCopy(value[i]); 186 return copy; 187 } 188 else static if (is(T : Configuration.ValueT[string])) 189 { 190 Configuration.ValueT[string] copy; 191 foreach (k, v; value) 192 copy[k] = deepCopy(v); 193 return copy; 194 } 195 else 196 return value.dup; 197 } 198 199 /// WorkspaceD instance holding plugins. 200 class WorkspaceD 201 { 202 static class Instance 203 { 204 string cwd; 205 ComponentWrapperInstance[] instanceComponents; 206 Configuration config; 207 208 string[] importPaths() const @property nothrow 209 { 210 return importPathProvider ? importPathProvider() : []; 211 } 212 213 string[] stringImportPaths() const @property nothrow 214 { 215 return stringImportPathProvider ? stringImportPathProvider() : []; 216 } 217 218 string[] importFiles() const @property nothrow 219 { 220 return importFilesProvider ? importFilesProvider() : []; 221 } 222 223 void shutdown(bool dtor = false) 224 { 225 foreach (ref com; instanceComponents) 226 com.wrapper.shutdown(dtor); 227 instanceComponents = null; 228 } 229 230 ImportPathProvider importPathProvider; 231 ImportPathProvider stringImportPathProvider; 232 ImportPathProvider importFilesProvider; 233 IdentifierListProvider projectVersionsProvider; 234 IdentifierListProvider debugSpecificationsProvider; 235 236 /* virtual */ 237 void onBeforeAccessComponent(ComponentInfo) const 238 { 239 } 240 241 /* virtual */ 242 bool checkHasComponent(ComponentInfo info) const nothrow 243 { 244 foreach (com; instanceComponents) 245 if (com.info.name == info.name) 246 return true; 247 return false; 248 } 249 250 inout(T) get(T)() inout 251 { 252 auto info = getUDAs!(T, ComponentInfoParams)[0]; 253 onBeforeAccessComponent(ComponentInfo(info, typeid(T))); 254 foreach (com; instanceComponents) 255 if (com.info.name == info.name) 256 return cast(inout T) com.wrapper; 257 throw new Exception( 258 "Attempted to get unknown instance component " ~ T.stringof 259 ~ " in instance cwd:" ~ cwd); 260 } 261 262 bool has(T)() const nothrow 263 { 264 auto info = getUDAs!(T, ComponentInfoParams)[0]; 265 return checkHasComponent(ComponentInfo(info, typeid(T))); 266 } 267 268 /// Shuts down an attached component and removes it from this component 269 /// list. If you plan to remove all components, call $(LREF shutdown) 270 /// instead. 271 /// Returns: `true` if the component was loaded and is now unloaded and 272 /// removed or `false` if the component wasn't found. 273 bool detach(T)() 274 { 275 auto info = getUDAs!(T, ComponentInfoParams)[0]; 276 return detach(ComponentInfo(info, typeid(T))); 277 } 278 279 /// ditto 280 bool detach(ComponentInfo info) 281 { 282 foreach (i, com; instanceComponents) 283 if (com.info.name == info.name) 284 { 285 instanceComponents = instanceComponents.remove(i); 286 com.wrapper.shutdown(false); 287 return true; 288 } 289 return false; 290 } 291 292 /// Loads a registered component which didn't have auto register on just for this instance. 293 /// Returns: false instead of using the onBindFail callback on failure. 294 /// Throws: Exception if component was not registered in workspaced. 295 bool attach(T)(WorkspaceD workspaced) 296 { 297 string info = getUDAs!(T, ComponentInfoParams)[0]; 298 return attach(workspaced, ComponentInfo(info, typeid(T))); 299 } 300 301 /// ditto 302 bool attach(WorkspaceD workspaced, ComponentInfo info) 303 { 304 foreach (factory; workspaced.components) 305 { 306 if (factory.info.name == info.name) 307 { 308 Exception e; 309 auto inst = factory.create(workspaced, this, e); 310 if (inst) 311 { 312 attachComponent(ComponentWrapperInstance(inst, info)); 313 return true; 314 } 315 else 316 return false; 317 } 318 } 319 throw new Exception("Component not found"); 320 } 321 322 void attachComponent(ComponentWrapperInstance component) 323 { 324 instanceComponents ~= component; 325 } 326 } 327 328 import std.typecons : BlackHole; 329 /// Called from components to push messages to the app. 330 IMessageHandler messageHandler = new BlackHole!IMessageHandler; 331 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails) 332 /// See_Also: $(LREF ComponentBindFailCallback) 333 ComponentBindFailCallback onBindFail; 334 335 Instance[] instances; 336 /// Base global configuration for new instances, does not modify existing ones. 337 Configuration globalConfiguration; 338 ComponentWrapperInstance[] globalComponents; 339 ComponentFactoryInstance[] components; 340 StringCache stringCache; 341 342 TaskPool _gthreads; 343 344 this() 345 { 346 stringCache = StringCache(StringCache.defaultBucketCount * 4); 347 } 348 349 ~this() 350 { 351 shutdown(true); 352 } 353 354 void shutdown(bool dtor = false) 355 { 356 foreach (ref instance; instances) 357 instance.shutdown(dtor); 358 instances = null; 359 foreach (ref com; globalComponents) 360 com.wrapper.shutdown(dtor); 361 globalComponents = null; 362 components = null; 363 if (_gthreads) 364 _gthreads.finish(true); 365 _gthreads = null; 366 } 367 368 Instance getInstance(string cwd) nothrow 369 { 370 cwd = buildNormalizedPath(cwd); 371 foreach (instance; instances) 372 if (instance.cwd == cwd) 373 return instance; 374 return null; 375 } 376 377 Instance getBestInstanceByDependency(WithComponent)(string file) nothrow 378 { 379 Instance best; 380 size_t bestLength; 381 foreach (instance; instances) 382 { 383 foreach (folder; chain(instance.importPaths, instance.importFiles, 384 instance.stringImportPaths)) 385 { 386 if (folder.length > bestLength && file.startsWith(folder) 387 && instance.has!WithComponent) 388 { 389 best = instance; 390 bestLength = folder.length; 391 } 392 } 393 } 394 return best; 395 } 396 397 Instance getBestInstanceByDependency(string file) nothrow 398 { 399 Instance best; 400 size_t bestLength; 401 foreach (instance; instances) 402 { 403 foreach (folder; chain(instance.importPaths, instance.importFiles, 404 instance.stringImportPaths)) 405 { 406 if (folder.length > bestLength && file.startsWith(folder)) 407 { 408 best = instance; 409 bestLength = folder.length; 410 } 411 } 412 } 413 return best; 414 } 415 416 Instance getBestInstance(WithComponent)(string file, bool fallback = true) nothrow 417 { 418 file = buildNormalizedPath(file); 419 Instance ret = null; 420 size_t best; 421 foreach (instance; instances) 422 { 423 if (instance.cwd.length > best && file.startsWith(instance.cwd) 424 && instance.has!WithComponent) 425 { 426 ret = instance; 427 best = instance.cwd.length; 428 } 429 } 430 if (!ret && fallback) 431 { 432 ret = getBestInstanceByDependency!WithComponent(file); 433 if (ret) 434 return ret; 435 foreach (instance; instances) 436 if (instance.has!WithComponent) 437 return instance; 438 } 439 return ret; 440 } 441 442 Instance getBestInstance(string file, bool fallback = true) nothrow 443 { 444 file = buildNormalizedPath(file); 445 Instance ret = null; 446 size_t best; 447 foreach (instance; instances) 448 { 449 if (instance.cwd.length > best && file.startsWith(instance.cwd)) 450 { 451 ret = instance; 452 best = instance.cwd.length; 453 } 454 } 455 if (!ret && fallback && instances.length) 456 { 457 ret = getBestInstanceByDependency(file); 458 if (!ret) 459 ret = instances[0]; 460 } 461 return ret; 462 } 463 464 /* virtual */ 465 void onBeforeAccessGlobalComponent(ComponentInfo) const 466 { 467 } 468 469 /* virtual */ 470 bool checkHasGlobalComponent(ComponentInfo info) const 471 { 472 foreach (com; globalComponents) 473 if (com.info.name == info.name) 474 return true; 475 return false; 476 } 477 478 T get(T)() 479 { 480 auto info = getUDAs!(T, ComponentInfoParams)[0]; 481 onBeforeAccessGlobalComponent(ComponentInfo(info, typeid(T))); 482 foreach (com; globalComponents) 483 if (com.info.name == info.name) 484 return cast(T) com.wrapper; 485 throw new Exception("Attempted to get unknown global component " ~ T.stringof); 486 } 487 488 bool has(T)() 489 { 490 auto info = getUDAs!(T, ComponentInfoParams)[0]; 491 return checkHasGlobalComponent(ComponentInfo(info, typeid(T))); 492 } 493 494 T get(T)(string cwd) 495 { 496 if (!cwd.length) 497 return this.get!T; 498 auto inst = getInstance(cwd); 499 if (inst is null) 500 throw new Exception("cwd '" ~ cwd ~ "' not found"); 501 return inst.get!T; 502 } 503 504 bool has(T)(string cwd) 505 { 506 auto inst = getInstance(cwd); 507 if (inst is null) 508 return false; 509 return inst.has!T; 510 } 511 512 T best(T)(string file, bool fallback = true) 513 { 514 if (!file.length) 515 return this.get!T; 516 auto inst = getBestInstance!T(file); 517 if (inst is null) 518 throw new Exception("cwd for '" ~ file ~ "' not found"); 519 return inst.get!T; 520 } 521 522 bool hasBest(T)(string cwd, bool fallback = true) 523 { 524 auto inst = getBestInstance!T(cwd); 525 if (inst is null) 526 return false; 527 return inst.has!T; 528 } 529 530 void onRegisterComponent(ref ComponentFactory factory, bool autoRegister) 531 { 532 components ~= ComponentFactoryInstance(factory, autoRegister); 533 auto info = factory.info; 534 Exception error; 535 auto glob = factory.create(this, null, error); 536 if (glob) 537 globalComponents ~= ComponentWrapperInstance(glob, info); 538 else if (onBindFail) 539 onBindFail(null, factory, error); 540 541 if (autoRegister) 542 foreach (ref instance; instances) 543 { 544 auto inst = factory.create(this, instance, error); 545 if (inst) 546 instance.attachComponent(ComponentWrapperInstance(inst, info)); 547 else if (onBindFail) 548 onBindFail(instance, factory, error); 549 } 550 } 551 552 ComponentFactory register(T)(bool autoRegister = true) 553 { 554 ComponentFactory factory; 555 static foreach (attr; __traits(getAttributes, T)) 556 static if (is(attr == class) && is(attr : ComponentFactory)) 557 factory = new attr; 558 if (factory is null) 559 factory = new DefaultComponentFactory!T; 560 561 onRegisterComponent(factory, autoRegister); 562 563 static if (__traits(compiles, T.registered(this))) 564 T.registered(this); 565 else static if (__traits(compiles, T.registered())) 566 T.registered(); 567 return factory; 568 } 569 570 protected Instance createInstance(string cwd, Configuration config) 571 { 572 auto inst = new Instance(); 573 inst.cwd = cwd; 574 inst.config = config; 575 return inst; 576 } 577 578 protected void preloadComponents(Instance inst, string[] preloadComponents) 579 { 580 foreach (name; preloadComponents) 581 { 582 foreach (factory; components) 583 { 584 if (!factory.autoRegister && factory.info.name == name) 585 { 586 Exception error; 587 auto wrap = factory.create(this, inst, error); 588 if (wrap) 589 inst.attachComponent(ComponentWrapperInstance(wrap, factory.info)); 590 else if (onBindFail) 591 onBindFail(inst, factory, error); 592 break; 593 } 594 } 595 } 596 } 597 598 protected void autoRegisterComponents(Instance inst) 599 { 600 foreach (factory; components) 601 { 602 if (factory.autoRegister) 603 { 604 Exception error; 605 auto wrap = factory.create(this, inst, error); 606 if (wrap) 607 inst.attachComponent(ComponentWrapperInstance(wrap, factory.info)); 608 else if (onBindFail) 609 onBindFail(inst, factory, error); 610 } 611 } 612 } 613 614 /// Creates a new workspace with the given cwd with optional config overrides and preload components for non-autoRegister components. 615 /// Throws: Exception if normalized cwd already exists as instance. 616 Instance addInstance(string cwd, Configuration configOverrides = Configuration.none, 617 string[] preloadComponents = []) 618 { 619 cwd = buildNormalizedPath(cwd); 620 if (instances.canFind!(a => a.cwd == cwd)) 621 throw new Exception("Instance with cwd '" ~ cwd ~ "' already exists!"); 622 configOverrides.loadBase(globalConfiguration); 623 auto inst = createInstance(cwd, configOverrides); 624 this.preloadComponents(inst, preloadComponents); 625 this.autoRegisterComponents(inst); 626 instances ~= inst; 627 return inst; 628 } 629 630 bool removeInstance(string cwd) 631 { 632 cwd = buildNormalizedPath(cwd); 633 foreach (i, instance; instances) 634 if (instance.cwd == cwd) 635 { 636 foreach (com; instance.instanceComponents) 637 destroy(com.wrapper); 638 destroy(instance); 639 instances = instances.remove(i); 640 return true; 641 } 642 return false; 643 } 644 645 deprecated("Use overload taking an out Exception error or attachSilent instead") 646 final bool attach(Instance instance, string component) 647 { 648 return attachSilent(instance, component); 649 } 650 651 final bool attachSilent(Instance instance, string component) 652 { 653 Exception error; 654 return attach(instance, component, error); 655 } 656 657 bool attach(Instance instance, string component, out Exception error) 658 { 659 foreach (factory; components) 660 { 661 if (factory.info.name == component) 662 { 663 auto wrap = factory.create(this, instance, error); 664 if (wrap) 665 { 666 instance.attachComponent(ComponentWrapperInstance(wrap, factory.info)); 667 return true; 668 } 669 else 670 return false; 671 } 672 } 673 return false; 674 } 675 676 TaskPool gthreads() 677 { 678 if (!_gthreads) 679 synchronized (this) 680 if (!_gthreads) 681 { 682 _gthreads = new TaskPool(max(2, min(6, defaultPoolThreads))); 683 _gthreads.isDaemon = true; 684 } 685 return _gthreads; 686 } 687 } 688 689 version (unittest) 690 { 691 struct TestingWorkspace 692 { 693 string directory; 694 695 @disable this(this); 696 697 this(string path) 698 { 699 if (path.exists) 700 throw new Exception("Path already exists"); 701 directory = path; 702 mkdir(path); 703 } 704 705 ~this() 706 { 707 rmdirRecurse(directory); 708 } 709 710 string getPath(string path) 711 { 712 return buildPath(directory, path); 713 } 714 715 void createDir(string dir) 716 { 717 mkdirRecurse(getPath(dir)); 718 } 719 720 void writeFile(string path, string content) 721 { 722 write(getPath(path), content); 723 } 724 } 725 726 TestingWorkspace makeTemporaryTestingWorkspace() 727 { 728 import std.random; 729 730 return TestingWorkspace(buildPath(tempDir, 731 "workspace-d-test-" ~ uniform(0, long.max).to!string(36))); 732 } 733 }