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 source.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, ComponentInfo)[0].name;
120 		ComponentSwitch:
121 			switch (request["component"].str)
122 			{
123 				static foreach (Component; AllComponents)
124 				{
125 			case getUDAs!(Component, ComponentInfo)[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 source.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 			assert(dataBuffer.length == 4, "Unexpected buffer data");
344 			length = bigEndianToNative!int(dataBuffer[0 .. 4]);
345 
346 			assert(length >= 4, "Invalid request");
347 
348 			dataBuffer = stdin.rawRead(intBuffer);
349 			assert(dataBuffer.length == 4, "Unexpected buffer data");
350 			id = bigEndianToNative!int(dataBuffer[0 .. 4]);
351 
352 			dataBuffer.length = length - 4;
353 			dataBuffer = stdin.rawRead(dataBuffer);
354 
355 			try
356 			{
357 				data = parseJSON(cast(string) dataBuffer);
358 			}
359 			catch (Exception e)
360 			{
361 				processException(id, e);
362 			}
363 			catch (AssertError e)
364 			{
365 				processException(id, e);
366 			}
367 
368 			try
369 			{
370 				handleRequest(id, data);
371 			}
372 			catch (Exception e)
373 			{
374 				processException(id, data, e);
375 			}
376 			catch (AssertError e)
377 			{
378 				processException(id, data, e);
379 			}
380 			stdout.flush();
381 
382 			if (gcInterval.peek >= 1.minutes)
383 			{
384 				import core.memory : GC;
385 
386 				auto before = GC.stats();
387 				StopWatch gcSpeed;
388 				gcSpeed.start();
389 				GC.collect();
390 				gcSpeed.stop();
391 				auto after = GC.stats();
392 				if (before != after)
393 					stderr.writefln("GC run in %s. Freed %s bytes (%s bytes allocated, %s bytes available)", gcSpeed.peek,
394 							cast(long) before.usedSize - cast(long) after.usedSize,
395 							after.usedSize, after.freeSize);
396 				else
397 					stderr.writeln("GC run in ", gcSpeed.peek);
398 				gcInterval.reset();
399 
400 				gcCollects++;
401 				if (gcCollects > 5)
402 				{
403 					gcSpeed.reset();
404 					gcSpeed.start();
405 					GC.minimize();
406 					gcSpeed.stop();
407 					stderr.writeln("GC minimized in ", gcSpeed.peek);
408 					gcCollects = 0;
409 				}
410 			}
411 		}
412 		return 0;
413 	}
414 }