1 module app;
2 
3 import core.sync.mutex;
4 import core.exception;
5 
6 import painlessjson;
7 import standardpaths;
8 
9 import workspaced.api;
10 import workspaced.coms;
11 
12 import std.algorithm;
13 import std.bitmanip;
14 import std.exception;
15 import std.functional;
16 import std.process;
17 import std.stdio : File, stderr;
18 import std.traits;
19 
20 static import std.stdio;
21 import std..string;
22 import std.json;
23 import std.meta;
24 import std.conv;
25 
26 import source.workspaced.info;
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(["workspace" : JSONValue(instance
66 			? instance.cwd : null), "data" : message]));
67 }
68 
69 void bindFail(WorkspaceD.Instance instance, ComponentFactory component, Exception error)
70 {
71 	sendResponse(0x7F000000, JSONValue(["workspace" : JSONValue(instance
72 			? instance.cwd : null), "data" : JSONValue(["component" : JSONValue(component.info.name),
73 			"type" : JSONValue("bindfail"), "msg" : JSONValue(error.msg), "trace"
74 			: JSONValue(error.toString)])]));
75 }
76 
77 WorkspaceD engine;
78 
79 void handleRequest(int id, JSONValue request)
80 {
81 	if (request.type != JSON_TYPE.OBJECT || "cmd" !in request
82 			|| request["cmd"].type != JSON_TYPE.STRING)
83 	{
84 		goto printUsage;
85 	}
86 	else if (request["cmd"].str == "version")
87 	{
88 		sendResponse(id, getVersionInfoJson);
89 	}
90 	else if (request["cmd"].str == "load")
91 	{
92 		if ("component" !in request || request["component"].type != JSON_TYPE.STRING)
93 		{
94 			sendException(id,
95 					new Exception(
96 						`Expected load message to be in format {"cmd":"load", "component":string, ("autoregister":bool)}`));
97 		}
98 		else
99 		{
100 			bool autoRegister = true;
101 			if (auto v = "autoregister" in request)
102 				autoRegister = v.type != JSON_TYPE.FALSE;
103 			string[] allComponents;
104 			static foreach (Component; AllComponents)
105 				allComponents ~= getUDAs!(Component, ComponentInfo)[0].name;
106 		ComponentSwitch:
107 			switch (request["component"].str)
108 			{
109 				static foreach (Component; AllComponents)
110 				{
111 			case getUDAs!(Component, ComponentInfo)[0].name:
112 					engine.register!Component(autoRegister);
113 					break ComponentSwitch;
114 				}
115 			default:
116 				sendException(id,
117 						new Exception(
118 							"Unknown Component '" ~ request["component"].str ~ "', built-in are " ~ allComponents.join(
119 							", ")));
120 				return;
121 			}
122 			sendResponse(id, JSONValue(true));
123 		}
124 	}
125 	else if (request["cmd"].str == "new")
126 	{
127 		if ("cwd" !in request || request["cwd"].type != JSON_TYPE.STRING)
128 		{
129 			sendException(id,
130 					new Exception(
131 						`Expected new message to be in format {"cmd":"new", "cwd":string, ("config":object)}`));
132 		}
133 		else
134 		{
135 			string cwd = request["cwd"].str;
136 			if ("config" in request)
137 				engine.addInstance(cwd, Configuration(request["config"]));
138 			else
139 				engine.addInstance(cwd);
140 			sendResponse(id, JSONValue(true));
141 		}
142 	}
143 	else if (request["cmd"].str == "config-set")
144 	{
145 		if ("config" !in request || request["config"].type != JSON_TYPE.OBJECT)
146 		{
147 		configSetFail:
148 			sendException(id,
149 					new Exception(
150 						`Expected new message to be in format {"cmd":"config-set", ("cwd":string), "config":object}`));
151 		}
152 		else
153 		{
154 			if ("cwd" in request)
155 			{
156 				if (request["cwd"].type != JSON_TYPE.STRING)
157 					goto configSetFail;
158 				else
159 					engine.getInstance(request["cwd"].str).config.base = request["config"];
160 			}
161 			else
162 				engine.globalConfiguration.base = request["config"];
163 			sendResponse(id, JSONValue(true));
164 		}
165 	}
166 	else if (request["cmd"].str == "config-get")
167 	{
168 		if ("cwd" in request)
169 		{
170 			if (request["cwd"].type != JSON_TYPE.STRING)
171 				sendException(id,
172 						new Exception(
173 							`Expected new message to be in format {"cmd":"config-get", ("cwd":string)}`));
174 			else
175 				sendResponse(id, engine.getInstance(request["cwd"].str).config.base);
176 		}
177 		else
178 			sendResponse(id, engine.globalConfiguration.base);
179 	}
180 	else if (request["cmd"].str == "call")
181 	{
182 		JSONValue[] params;
183 		if ("params" in request)
184 		{
185 			if (request["params"].type != JSON_TYPE.ARRAY)
186 				goto callFail;
187 			params = request["params"].array;
188 		}
189 		if ("method" !in request || request["method"].type != JSON_TYPE.STRING
190 				|| "component" !in request || request["component"].type != JSON_TYPE.STRING)
191 		{
192 		callFail:
193 			sendException(id, new Exception(`Expected call message to be in format {"cmd":"call", "component":string, "method":string, ("cwd":string), ("params":object[])}`));
194 		}
195 		else
196 		{
197 			Future!JSONValue ret;
198 			string component = request["component"].str;
199 			string method = request["method"].str;
200 			if ("cwd" in request)
201 			{
202 				if (request["cwd"].type != JSON_TYPE.STRING)
203 				{
204 					goto callFail;
205 				}
206 				else
207 				{
208 					string cwd = request["cwd"].str;
209 					ret = engine.run(cwd, component, method, params);
210 				}
211 			}
212 			else
213 				ret = engine.run(component, method, params);
214 
215 			ret.onDone = {
216 				if (ret.exception)
217 					sendException(id, ret.exception);
218 				else
219 					sendResponse(id, ret.value);
220 			};
221 		}
222 	}
223 	else if (request["cmd"].str == "import-paths")
224 	{
225 		if ("cwd" !in request || request["cwd"].type != JSON_TYPE.STRING)
226 			sendException(id,
227 					new Exception(`Expected new message to be in format {"cmd":"import-paths", "cwd":string}`));
228 		else
229 			sendResponse(id, engine.getInstance(request["cwd"].str).importPaths.toJSON);
230 	}
231 	else if (request["cmd"].str == "import-files")
232 	{
233 		if ("cwd" !in request || request["cwd"].type != JSON_TYPE.STRING)
234 			sendException(id,
235 					new Exception(`Expected new message to be in format {"cmd":"import-files", "cwd":string}`));
236 		else
237 			sendResponse(id, engine.getInstance(request["cwd"].str).importFiles.toJSON);
238 	}
239 	else if (request["cmd"].str == "string-import-paths")
240 	{
241 		if ("cwd" !in request || request["cwd"].type != JSON_TYPE.STRING)
242 			sendException(id,
243 					new Exception(
244 						`Expected new message to be in format {"cmd":"string-import-paths", "cwd":string}`));
245 		else
246 			sendResponse(id, engine.getInstance(request["cwd"].str).stringImportPaths.toJSON);
247 	}
248 	else
249 	{
250 	printUsage:
251 		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]"));
252 	}
253 }
254 
255 void processException(int id, Throwable e)
256 {
257 	stderr.writeln(e);
258 	// dfmt off
259 	sendResponse(id, JSONValue([
260 		"error": JSONValue(true),
261 		"msg": JSONValue(e.msg),
262 		"exception": JSONValue(e.toString())
263 	]));
264 	// dfmt on
265 }
266 
267 void processException(int id, JSONValue request, Throwable e)
268 {
269 	stderr.writeln(e);
270 	// dfmt off
271 	sendResponse(id, JSONValue([
272 		"error": JSONValue(true),
273 		"msg": JSONValue(e.msg),
274 		"exception": JSONValue(e.toString()),
275 		"request": request
276 	]));
277 	// dfmt on
278 }
279 
280 int main(string[] args)
281 {
282 	import std.file;
283 	import etc.linux.memoryerror;
284 
285 	version (unittest)
286 	{
287 	}
288 	else
289 	{
290 		version (DigitalMars)
291 			static if (is(typeof(registerMemoryErrorHandler)))
292 				registerMemoryErrorHandler();
293 
294 		if (args.length > 1 && (args[1] == "-v" || args[1] == "--version" || args[1] == "-version"))
295 		{
296 			stdout.writeln(getVersionInfoString);
297 			return 0;
298 		}
299 
300 		engine = new WorkspaceD();
301 		engine.onBroadcast = (&broadcast).toDelegate;
302 		engine.onBindFail = (&bindFail).toDelegate;
303 		scope (exit)
304 			engine.shutdown();
305 
306 		writeMutex = new Mutex;
307 		commandMutex = new Mutex;
308 
309 		int length = 0;
310 		int id = 0;
311 		ubyte[4] intBuffer;
312 		ubyte[] dataBuffer;
313 		JSONValue data;
314 
315 		scope (exit)
316 			handleRequest(int.min, JSONValue(["cmd" : "unload", "components" : "*"]));
317 
318 		stderr.writeln("Config files stored in ", standardPaths(StandardPath.config, "workspace-d"));
319 
320 		while (stdin.isOpen && stdout.isOpen && !stdin.eof)
321 		{
322 			dataBuffer = stdin.rawRead(intBuffer);
323 			assert(dataBuffer.length == 4, "Unexpected buffer data");
324 			length = bigEndianToNative!int(dataBuffer[0 .. 4]);
325 
326 			assert(length >= 4, "Invalid request");
327 
328 			dataBuffer = stdin.rawRead(intBuffer);
329 			assert(dataBuffer.length == 4, "Unexpected buffer data");
330 			id = bigEndianToNative!int(dataBuffer[0 .. 4]);
331 
332 			dataBuffer.length = length - 4;
333 			dataBuffer = stdin.rawRead(dataBuffer);
334 
335 			try
336 			{
337 				data = parseJSON(cast(string) dataBuffer);
338 			}
339 			catch (Exception e)
340 			{
341 				processException(id, e);
342 			}
343 			catch (AssertError e)
344 			{
345 				processException(id, e);
346 			}
347 
348 			try
349 			{
350 				handleRequest(id, data);
351 			}
352 			catch (Exception e)
353 			{
354 				processException(id, data, e);
355 			}
356 			catch (AssertError e)
357 			{
358 				processException(id, data, e);
359 			}
360 			stdout.flush();
361 		}
362 	}
363 	return 0;
364 }