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 }