1 module workspaced.api; 2 3 // debug = Tasks; 4 5 import standardpaths; 6 7 import std.algorithm : all; 8 import std.array : array; 9 import std.conv; 10 import std.file : exists, thisExePath; 11 import std.path : baseName, chainPath, dirName; 12 import std.regex : ctRegex, matchFirst; 13 import std.string : indexOf, indexOfAny, strip; 14 import std.traits; 15 16 public import workspaced.backend; 17 public import workspaced.future; 18 19 version (unittest) 20 { 21 package import std.experimental.logger : trace; 22 } 23 else 24 { 25 // dummy 26 package void trace(Args...)(lazy Args) 27 { 28 } 29 } 30 31 /// 32 alias ImportPathProvider = string[] delegate() nothrow; 33 /// 34 alias IdentifierListProvider = string[] delegate() nothrow; 35 /// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails) 36 /// Params: 37 /// instance = the instance for which the component was attempted to initialize (or null for global component registration) 38 /// factory = the factory on which the error occured with 39 /// error = the stacktrace that was catched on the bind call 40 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance instance, 41 ComponentFactory factory, Exception error); 42 43 interface IMessageHandler 44 { 45 void warn(WorkspaceD.Instance instance, string component, int id, string message, string details = null); 46 void error(WorkspaceD.Instance instance, string component, int id, string message, string details = null); 47 void handleCrash(WorkspaceD.Instance instance, string component, ComponentWrapper componentInstance); 48 } 49 50 /// UDA; will never try to call this function from rpc 51 enum ignoredFunc; 52 53 /// Component call 54 struct ComponentInfoParams 55 { 56 /// Name of the component 57 string name; 58 } 59 60 ComponentInfoParams component(string name) 61 { 62 return ComponentInfoParams(name); 63 } 64 65 struct ComponentInfo 66 { 67 ComponentInfoParams params; 68 TypeInfo type; 69 70 alias params this; 71 } 72 73 void traceTaskLog(lazy string msg) 74 { 75 import std.stdio : stderr; 76 77 debug (Tasks) 78 stderr.writeln(msg); 79 } 80 81 static immutable traceTask = `traceTaskLog("new task in " ~ __PRETTY_FUNCTION__); scope (exit) traceTaskLog(__PRETTY_FUNCTION__ ~ " exited");`; 82 83 mixin template DefaultComponentWrapper(bool withDtor = true) 84 { 85 @ignoredFunc 86 { 87 import std.algorithm : min, max; 88 import std.parallelism : TaskPool, Task, task, defaultPoolThreads; 89 90 WorkspaceD workspaced; 91 WorkspaceD.Instance refInstance; 92 93 TaskPool _threads; 94 95 static if (withDtor) 96 { 97 ~this() 98 { 99 shutdown(true); 100 } 101 } 102 103 TaskPool gthreads() 104 { 105 return workspaced.gthreads; 106 } 107 108 TaskPool threads(int minSize, int maxSize) 109 { 110 if (!_threads) 111 synchronized (this) 112 if (!_threads) 113 { 114 _threads = new TaskPool(max(minSize, min(maxSize, defaultPoolThreads))); 115 _threads.isDaemon = true; 116 } 117 return _threads; 118 } 119 120 inout(WorkspaceD.Instance) instance() inout @property 121 { 122 if (refInstance) 123 return refInstance; 124 else 125 throw new Exception("Attempted to access instance in a global context"); 126 } 127 128 WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property 129 { 130 return refInstance = instance; 131 } 132 133 string[] importPaths() const @property 134 { 135 return instance.importPathProvider ? instance.importPathProvider() : []; 136 } 137 138 string[] stringImportPaths() const @property 139 { 140 return instance.stringImportPathProvider ? instance.stringImportPathProvider() : []; 141 } 142 143 string[] importFiles() const @property 144 { 145 return instance.importFilesProvider ? instance.importFilesProvider() : []; 146 } 147 148 /// Lists the project defined version identifiers, if provided by any identifier 149 string[] projectVersions() const @property 150 { 151 return instance.projectVersionsProvider ? instance.projectVersionsProvider() : []; 152 } 153 154 /// Lists the project defined debug specification identifiers, if provided by any provider 155 string[] debugSpecifications() const @property 156 { 157 return instance.debugSpecificationsProvider ? instance.debugSpecificationsProvider() : []; 158 } 159 160 ref inout(ImportPathProvider) importPathProvider() @property inout 161 { 162 return instance.importPathProvider; 163 } 164 165 ref inout(ImportPathProvider) stringImportPathProvider() @property inout 166 { 167 return instance.stringImportPathProvider; 168 } 169 170 ref inout(ImportPathProvider) importFilesProvider() @property inout 171 { 172 return instance.importFilesProvider; 173 } 174 175 ref inout(IdentifierListProvider) projectVersionsProvider() @property inout 176 { 177 return instance.projectVersionsProvider; 178 } 179 180 ref inout(IdentifierListProvider) debugSpecificationsProvider() @property inout 181 { 182 return instance.debugSpecificationsProvider; 183 } 184 185 ref inout(Configuration) config() @property inout 186 { 187 if (refInstance) 188 return refInstance.config; 189 else if (workspaced) 190 return workspaced.globalConfiguration; 191 else 192 assert(false, "Unbound component trying to access config."); 193 } 194 195 bool has(T)() 196 { 197 if (refInstance) 198 return refInstance.has!T; 199 else if (workspaced) 200 return workspaced.has!T; 201 else 202 assert(false, "Unbound component trying to check for component " ~ T.stringof ~ "."); 203 } 204 205 T get(T)() 206 { 207 if (refInstance) 208 return refInstance.get!T; 209 else if (workspaced) 210 return workspaced.get!T; 211 else 212 assert(false, "Unbound component trying to get component " ~ T.stringof ~ "."); 213 } 214 215 string cwd() @property const 216 { 217 return instance.cwd; 218 } 219 220 override void shutdown(bool dtor = false) 221 { 222 if (!dtor && _threads) 223 _threads.finish(); 224 } 225 226 override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance) 227 { 228 this.workspaced = workspaced; 229 this.instance = instance; 230 static if (__traits(hasMember, typeof(this).init, "load")) 231 load(); 232 } 233 } 234 } 235 236 interface ComponentWrapper 237 { 238 void bind(WorkspaceD workspaced, WorkspaceD.Instance instance); 239 void shutdown(bool dtor = false); 240 } 241 242 interface ComponentFactory 243 { 244 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow; 245 ComponentInfo info() @property const nothrow; 246 } 247 248 struct ComponentFactoryInstance 249 { 250 ComponentFactory factory; 251 bool autoRegister; 252 alias factory this; 253 } 254 255 struct ComponentWrapperInstance 256 { 257 ComponentWrapper wrapper; 258 ComponentInfo info; 259 } 260 261 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory 262 { 263 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) nothrow 264 { 265 auto wrapper = new T(); 266 try 267 { 268 wrapper.bind(workspaced, instance); 269 return wrapper; 270 } 271 catch (Exception e) 272 { 273 error = e; 274 return null; 275 } 276 } 277 278 ComponentInfo info() @property const nothrow 279 { 280 alias udas = getUDAs!(T, ComponentInfoParams); 281 static assert(udas.length == 1, "Can't construct default component factory for " 282 ~ T.stringof ~ ", expected exactly 1 ComponentInfoParams instance attached to the type"); 283 return ComponentInfo(udas[0], typeid(T)); 284 } 285 } 286 287 /// Describes what to insert/replace/delete to do something 288 struct CodeReplacement 289 { 290 /// Range what to replace. If both indices are the same its inserting. 291 size_t[2] range; 292 /// Content to replace it with. Empty means remove. 293 string content; 294 295 /// Applies this edit to a string. 296 string apply(string code) 297 { 298 size_t min = range[0]; 299 size_t max = range[1]; 300 if (min > max) 301 { 302 min = range[1]; 303 max = range[0]; 304 } 305 if (min >= code.length) 306 return code ~ content; 307 if (max >= code.length) 308 return code[0 .. min] ~ content; 309 return code[0 .. min] ~ content ~ code[max .. $]; 310 } 311 } 312 313 /// Code replacements mapped to a file 314 struct FileChanges 315 { 316 /// File path to change. 317 string file; 318 /// Replacements to apply. 319 CodeReplacement[] replacements; 320 } 321 322 package bool getConfigPath(string file, ref string retPath) 323 { 324 foreach (dir; standardPaths(StandardPath.config, "workspace-d")) 325 { 326 auto path = chainPath(dir, file); 327 if (path.exists) 328 { 329 retPath = path.array; 330 return true; 331 } 332 } 333 return false; 334 } 335 336 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`; 337 bool checkVersion(string ver, int[3] target) 338 { 339 auto match = ver.matchFirst(verRegex); 340 if (!match) 341 return false; 342 const major = match[1].to!int; 343 const minor = match[2].to!int; 344 const patch = match[3].to!int; 345 return checkVersion([major, minor, patch], target); 346 } 347 348 bool checkVersion(int[3] ver, int[3] target) 349 { 350 if (ver[0] > target[0]) 351 return true; 352 if (ver[0] == target[0] && ver[1] > target[1]) 353 return true; 354 if (ver[0] == target[0] && ver[1] == target[1] && ver[2] >= target[2]) 355 return true; 356 return false; 357 } 358 359 package string getVersionAndFixPath(ref string execPath) 360 { 361 import std.process; 362 363 try 364 { 365 return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath); 366 } 367 catch (ProcessException e) 368 { 369 auto newPath = chainPath(thisExePath.dirName, execPath.baseName); 370 if (exists(newPath)) 371 { 372 execPath = newPath.array; 373 return execute([execPath, "--version"]).output.strip.orDubFetchFallback(execPath); 374 } 375 throw new Exception("Failed running program ['" 376 ~ execPath ~ "' '--version'] and no alternative existed in '" 377 ~ newPath.array.idup ~ "'.", e); 378 } 379 } 380 381 /// Set for some reason when compiling with `dub fetch` / `dub run` or sometimes 382 /// on self compilation. 383 /// Known strings: vbin, vdcd, vDCD 384 package bool isLocallyCompiledDCD(string v) 385 { 386 import std.uni : sicmp; 387 388 return sicmp(v, "vbin") == 0 || sicmp(v, "vdcd") == 0; 389 } 390 391 /// returns the version that is given or the version extracted from dub path if path is a dub path 392 package string orDubFetchFallback(string v, string path) 393 { 394 if (v.isLocallyCompiledDCD) 395 { 396 auto dub = path.indexOf(`dub/packages`); 397 if (dub == -1) 398 dub = path.indexOf(`dub\packages`); 399 400 if (dub != -1) 401 { 402 dub += `dub/packages/`.length; 403 auto end = path.indexOfAny(`\/`, dub); 404 405 if (end != -1) 406 { 407 path = path[dub .. end]; 408 auto semver = extractPathSemver(path); 409 if (semver.length) 410 return semver; 411 } 412 } 413 } 414 return v; 415 } 416 417 unittest 418 { 419 assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1/dcd/bin/dcd-server`) == "0.13.1"); 420 assert("vbin".orDubFetchFallback(`/path/to/home/.dub/packages/dcd-0.13.1-beta.4/dcd/bin/dcd-server`) == "0.13.1-beta.4"); 421 assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1\dcd\bin\dcd-server`) == "0.13.1"); 422 assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-0.13.1-beta.4\dcd\bin\dcd-server`) == "0.13.1-beta.4"); 423 assert("vbin".orDubFetchFallback(`C:\path\to\appdata\dub\packages\dcd-master\dcd\bin\dcd-server`) == "vbin"); 424 } 425 426 /// searches for a semver in the given string starting after a - character, 427 /// returns everything until the end. 428 package string extractPathSemver(string s) 429 { 430 import std.ascii; 431 432 foreach (start; 0 .. s.length) 433 { 434 // states: 435 // -1 = error 436 // 0 = expect - 437 // 1 = expect major 438 // 2 = expect major or . 439 // 3 = expect minor 440 // 4 = expect minor or . 441 // 5 = expect patch 442 // 6 = expect patch or - or + (valid) 443 // 7 = skip (valid) 444 int state = 0; 445 foreach (i; start .. s.length) 446 { 447 auto c = s[i]; 448 switch (state) 449 { 450 case 0: 451 if (c == '-') 452 state++; 453 else 454 state = -1; 455 break; 456 case 1: 457 case 3: 458 case 5: 459 if (c.isDigit) 460 state++; 461 else 462 state = -1; 463 break; 464 case 2: 465 case 4: 466 if (c == '.') 467 state++; 468 else if (!c.isDigit) 469 state = -1; 470 break; 471 case 6: 472 if (c == '+' || c == '-') 473 state = 7; 474 else if (!c.isDigit) 475 state = -1; 476 break; 477 default: 478 break; 479 } 480 481 if (state == -1) 482 break; 483 } 484 485 if (state >= 6) 486 return s[start + 1 .. $]; 487 } 488 489 return null; 490 } 491 492 unittest 493 { 494 assert(extractPathSemver("foo-v1.0.0") is null); 495 assert(extractPathSemver("foo-1.0.0") == "1.0.0"); 496 assert(extractPathSemver("foo-1.0.0-alpha.1-x") == "1.0.0-alpha.1-x"); 497 assert(extractPathSemver("foo-1.0.x") is null); 498 assert(extractPathSemver("foo-x.0.0") is null); 499 assert(extractPathSemver("foo-1.x.0") is null); 500 assert(extractPathSemver("foo-1x.0.0") is null); 501 assert(extractPathSemver("foo-1.0x.0") is null); 502 assert(extractPathSemver("foo-1.0.0x") is null); 503 assert(extractPathSemver("-1.0.0") == "1.0.0"); 504 } 505 506 version (unittest) 507 package string normLF(scope string str) 508 { 509 import std.string : replace; 510 511 return str 512 .replace("\r\n", "\n") 513 .replace("\r", "\n"); 514 }