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