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