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