1 module app; 2 3 import core.exception; 4 import core.sync.mutex; 5 import core.time; 6 7 import painlessjson; 8 import standardpaths; 9 10 import workspaced.api; 11 import workspaced.coms; 12 13 import std.algorithm; 14 import std.bitmanip; 15 import std.datetime.stopwatch : StopWatch; 16 import std.exception; 17 import std.functional; 18 import std.process; 19 import std.stdio : File, stderr; 20 import std.traits; 21 22 static import std.conv; 23 import std.json; 24 import std.meta; 25 import std.stdio; 26 import std.string; 27 28 import source.workspaced.info; 29 30 __gshared File stdin, stdout; 31 shared static this() 32 { 33 stdin = std.stdio.stdin; 34 stdout = std.stdio.stdout; 35 version (Windows) 36 std.stdio.stdin = File("NUL", "r"); 37 else version (Posix) 38 std.stdio.stdin = File("/dev/null", "r"); 39 else 40 stderr.writeln("warning: no /dev/null implementation on this OS"); 41 std.stdio.stdout = stderr; 42 } 43 44 __gshared Mutex writeMutex, commandMutex; 45 46 void sendResponse(int id, JSONValue message) 47 { 48 synchronized (writeMutex) 49 { 50 ubyte[] data = nativeToBigEndian(id) ~ (cast(ubyte[]) message.toString()); 51 stdout.rawWrite(nativeToBigEndian(cast(int) data.length) ~ data); 52 stdout.flush(); 53 } 54 } 55 56 void sendException(int id, Throwable t) 57 { 58 JSONValue[string] message; 59 message["error"] = JSONValue(true); 60 message["msg"] = JSONValue(t.msg); 61 message["exception"] = JSONValue(t.toString); 62 sendResponse(id, JSONValue(message)); 63 } 64 65 void broadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue message) 66 { 67 sendResponse(0x7F000000, JSONValue([ 68 "workspace": JSONValue(instance ? instance.cwd : null), 69 "data": message 70 ])); 71 } 72 73 void bindFail(WorkspaceD.Instance instance, ComponentFactory component, Exception error) 74 { 75 sendResponse(0x7F000000, JSONValue([ 76 "workspace": JSONValue(instance ? instance.cwd : null), 77 "data": JSONValue([ 78 "component": JSONValue(component.info.name), 79 "type": JSONValue("bindfail"), 80 "msg": JSONValue(error.msg), 81 "trace": JSONValue(error.toString) 82 ]) 83 ])); 84 } 85 86 WorkspaceD engine; 87 88 void handleRequest(int id, JSONValue request) 89 { 90 if (request.type != JSONType.object || "cmd" !in request 91 || request["cmd"].type != JSONType..string) 92 { 93 goto printUsage; 94 } 95 else if (request["cmd"].str == "version") 96 { 97 sendResponse(id, getVersionInfoJson); 98 } 99 else if (request["cmd"].str == "load") 100 { 101 if ("component" !in request || request["component"].type != JSONType..string) 102 { 103 sendException(id, 104 new Exception( 105 `Expected load message to be in format {"cmd":"load", "component":string, ("autoregister":bool)}`)); 106 } 107 else 108 { 109 bool autoRegister = true; 110 if (auto v = "autoregister" in request) 111 autoRegister = v.type != JSONType.false_; 112 string[] allComponents; 113 static foreach (Component; AllComponents) 114 allComponents ~= getUDAs!(Component, ComponentInfo)[0].name; 115 ComponentSwitch: 116 switch (request["component"].str) 117 { 118 static foreach (Component; AllComponents) 119 { 120 case getUDAs!(Component, ComponentInfo)[0].name: 121 engine.register!Component(autoRegister); 122 break ComponentSwitch; 123 } 124 default: 125 sendException(id, 126 new Exception( 127 "Unknown Component '" ~ request["component"].str ~ "', built-in are " ~ allComponents.join( 128 ", "))); 129 return; 130 } 131 sendResponse(id, JSONValue(true)); 132 } 133 } 134 else if (request["cmd"].str == "new") 135 { 136 if ("cwd" !in request || request["cwd"].type != JSONType..string) 137 { 138 sendException(id, 139 new Exception( 140 `Expected new message to be in format {"cmd":"new", "cwd":string, ("config":object)}`)); 141 } 142 else 143 { 144 string cwd = request["cwd"].str; 145 if ("config" in request) 146 engine.addInstance(cwd, Configuration(request["config"])); 147 else 148 engine.addInstance(cwd); 149 sendResponse(id, JSONValue(true)); 150 } 151 } 152 else if (request["cmd"].str == "config-set") 153 { 154 if ("config" !in request || request["config"].type != JSONType.object) 155 { 156 configSetFail: 157 sendException(id, 158 new Exception( 159 `Expected new message to be in format {"cmd":"config-set", ("cwd":string), "config":object}`)); 160 } 161 else 162 { 163 if ("cwd" in request) 164 { 165 if (request["cwd"].type != JSONType..string) 166 goto configSetFail; 167 else 168 engine.getInstance(request["cwd"].str).config.base = request["config"]; 169 } 170 else 171 engine.globalConfiguration.base = request["config"]; 172 sendResponse(id, JSONValue(true)); 173 } 174 } 175 else if (request["cmd"].str == "config-get") 176 { 177 if ("cwd" in request) 178 { 179 if (request["cwd"].type != JSONType..string) 180 sendException(id, 181 new Exception( 182 `Expected new message to be in format {"cmd":"config-get", ("cwd":string)}`)); 183 else 184 sendResponse(id, engine.getInstance(request["cwd"].str).config.base); 185 } 186 else 187 sendResponse(id, engine.globalConfiguration.base); 188 } 189 else if (request["cmd"].str == "call") 190 { 191 JSONValue[] params; 192 if ("params" in request) 193 { 194 if (request["params"].type != JSONType.array) 195 goto callFail; 196 params = request["params"].array; 197 } 198 if ("method" !in request || request["method"].type != JSONType..string 199 || "component" !in request || request["component"].type != JSONType..string) 200 { 201 callFail: 202 sendException(id, new Exception(`Expected call message to be in format {"cmd":"call", "component":string, "method":string, ("cwd":string), ("params":object[])}`)); 203 } 204 else 205 { 206 Future!JSONValue ret; 207 string component = request["component"].str; 208 string method = request["method"].str; 209 if ("cwd" in request) 210 { 211 if (request["cwd"].type != JSONType..string) 212 { 213 goto callFail; 214 } 215 else 216 { 217 string cwd = request["cwd"].str; 218 ret = engine.run(cwd, component, method, params); 219 } 220 } 221 else 222 ret = engine.run(component, method, params); 223 224 ret.onDone = { 225 if (ret.exception) 226 sendException(id, ret.exception); 227 else 228 sendResponse(id, ret.value); 229 }; 230 } 231 } 232 else if (request["cmd"].str == "import-paths") 233 { 234 if ("cwd" !in request || request["cwd"].type != JSONType..string) 235 sendException(id, 236 new Exception(`Expected new message to be in format {"cmd":"import-paths", "cwd":string}`)); 237 else 238 sendResponse(id, engine.getInstance(request["cwd"].str).importPaths.toJSON); 239 } 240 else if (request["cmd"].str == "import-files") 241 { 242 if ("cwd" !in request || request["cwd"].type != JSONType..string) 243 sendException(id, 244 new Exception(`Expected new message to be in format {"cmd":"import-files", "cwd":string}`)); 245 else 246 sendResponse(id, engine.getInstance(request["cwd"].str).importFiles.toJSON); 247 } 248 else if (request["cmd"].str == "string-import-paths") 249 { 250 if ("cwd" !in request || request["cwd"].type != JSONType..string) 251 sendException(id, 252 new Exception( 253 `Expected new message to be in format {"cmd":"string-import-paths", "cwd":string}`)); 254 else 255 sendResponse(id, engine.getInstance(request["cwd"].str).stringImportPaths.toJSON); 256 } 257 else 258 { 259 printUsage: 260 sendException(id, new Exception("Invalid request, must contain a cmd string key with one of the values [version, load, new, config-get, config-set, call, import-paths, import-files, string-import-paths]")); 261 } 262 } 263 264 void processException(int id, Throwable e) 265 { 266 stderr.writeln(e); 267 // dfmt off 268 sendResponse(id, JSONValue([ 269 "error": JSONValue(true), 270 "msg": JSONValue(e.msg), 271 "exception": JSONValue(e.toString()) 272 ])); 273 // dfmt on 274 } 275 276 void processException(int id, JSONValue request, Throwable e) 277 { 278 stderr.writeln(e); 279 // dfmt off 280 sendResponse(id, JSONValue([ 281 "error": JSONValue(true), 282 "msg": JSONValue(e.msg), 283 "exception": JSONValue(e.toString()), 284 "request": request 285 ])); 286 // dfmt on 287 } 288 289 int main(string[] args) 290 { 291 import std.file; 292 import etc.linux.memoryerror; 293 294 version (unittest) 295 { 296 } 297 else 298 { 299 version (DigitalMars) 300 static if (is(typeof(registerMemoryErrorHandler))) 301 registerMemoryErrorHandler(); 302 303 if (args.length > 1 && (args[1] == "-v" || args[1] == "--version" || args[1] == "-version")) 304 { 305 stdout.writeln(getVersionInfoString); 306 return 0; 307 } 308 309 engine = new WorkspaceD(); 310 engine.onBroadcast = (&broadcast).toDelegate; 311 engine.onBindFail = (&bindFail).toDelegate; 312 scope (exit) 313 engine.shutdown(); 314 315 writeMutex = new Mutex; 316 commandMutex = new Mutex; 317 318 int length = 0; 319 int id = 0; 320 ubyte[4] intBuffer; 321 ubyte[] dataBuffer; 322 JSONValue data; 323 324 int gcCollects; 325 StopWatch gcInterval; 326 gcInterval.start(); 327 328 scope (exit) 329 handleRequest(int.min, JSONValue(["cmd": "unload", "components": "*"])); 330 331 stderr.writeln("Config files stored in ", standardPaths(StandardPath.config, "workspace-d")); 332 333 while (stdin.isOpen && stdout.isOpen && !stdin.eof) 334 { 335 dataBuffer = stdin.rawRead(intBuffer); 336 assert(dataBuffer.length == 4, "Unexpected buffer data"); 337 length = bigEndianToNative!int(dataBuffer[0 .. 4]); 338 339 assert(length >= 4, "Invalid request"); 340 341 dataBuffer = stdin.rawRead(intBuffer); 342 assert(dataBuffer.length == 4, "Unexpected buffer data"); 343 id = bigEndianToNative!int(dataBuffer[0 .. 4]); 344 345 dataBuffer.length = length - 4; 346 dataBuffer = stdin.rawRead(dataBuffer); 347 348 try 349 { 350 data = parseJSON(cast(string) dataBuffer); 351 } 352 catch (Exception e) 353 { 354 processException(id, e); 355 } 356 catch (AssertError e) 357 { 358 processException(id, e); 359 } 360 361 try 362 { 363 handleRequest(id, data); 364 } 365 catch (Exception e) 366 { 367 processException(id, data, e); 368 } 369 catch (AssertError e) 370 { 371 processException(id, data, e); 372 } 373 stdout.flush(); 374 375 if (gcInterval.peek >= 1.minutes) 376 { 377 import core.memory : GC; 378 379 auto before = GC.stats(); 380 StopWatch gcSpeed; 381 gcSpeed.start(); 382 GC.collect(); 383 gcSpeed.stop(); 384 auto after = GC.stats(); 385 if (before != after) 386 stderr.writefln("GC run in %s. Freed %s bytes (%s bytes allocated, %s bytes available)", gcSpeed.peek, 387 cast(long) before.usedSize - cast(long) after.usedSize, 388 after.usedSize, after.freeSize); 389 else 390 stderr.writeln("GC run in ", gcSpeed.peek); 391 gcInterval.reset(); 392 393 gcCollects++; 394 if (gcCollects > 5) 395 { 396 gcSpeed.reset(); 397 gcSpeed.start(); 398 GC.minimize(); 399 gcSpeed.stop(); 400 stderr.writeln("GC minimized in ", gcSpeed.peek); 401 gcCollects = 0; 402 } 403 } 404 } 405 } 406 return 0; 407 }