1 /**
2 Copyright: Copyright (c) 2015-2016 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 
7 module launcher;
8 
9 import std.experimental.logger;
10 import std.process;
11 import std.string;
12 import std.algorithm;
13 import std.stdio;
14 import std.array;
15 import std.range;
16 public import std.typecons : Flag, Yes, No;
17 import std.file;
18 import std.path;
19 import std.conv : to;
20 
21 import voxelman.utils.messagewindow;
22 import voxelman.utils.linebuffer;
23 import gui;
24 
25 enum DEFAULT_PORT = 1234;
26 
27 struct PluginInfo
28 {
29 	string id;
30 	string semver;
31 	string downloadUrl;
32 	bool isEnabled = true;
33 	PluginInfo*[] dependencies;
34 	PluginInfo*[] dependants;
35 }
36 
37 struct PluginPack
38 {
39 	string id;
40 	string semver;
41 	string filename;
42 	PluginInfo*[] plugins;
43 }
44 
45 enum AppType
46 {
47 	client,
48 	server
49 }
50 
51 enum JobType : int
52 {
53 	run,
54 	compile,
55 	compileAndRun,
56 	test
57 }
58 
59 enum JobState
60 {
61 	build,
62 	run
63 }
64 
65 enum Compiler
66 {
67 	dmd,
68 	ldc,
69 	gdc
70 }
71 
72 string[] compilerExeNames = ["dmd", "ldc2", "gdc"];
73 
74 struct JobParams
75 {
76 	string[string] runParameters;
77 	AppType appType = AppType.client;
78 	Flag!"start" start = Yes.start;
79 	Flag!"build" build = Yes.build;
80 	Flag!"arch64" arch64 = Yes.arch64;
81 	Flag!"nodeps" nodeps = Yes.nodeps;
82 	Flag!"force" force = No.force;
83 	Flag!"release" release = No.release;
84 	Compiler compiler;
85 	JobType jobType;
86 }
87 
88 struct Job
89 {
90 	JobParams params;
91 	string command;
92 	MessageWindow messageWindow;
93 	ProcessPipes pipes;
94 
95 	JobState jobState = JobState.build;
96 	string title;
97 	bool isRunning;
98 	bool needsClose;
99 	bool needsRestart;
100 	int status;
101 }
102 
103 string jobStateString(Job* job)
104 {
105 	if (!job.isRunning) return "[STOPPED]";
106 	final switch(job.jobState) with(JobState)
107 	{
108 		case build: break;
109 		case run: return "[RUNNING]";
110 	}
111 
112 	final switch(job.params.jobType) with(JobType)
113 	{
114 		case run: return "[INVALID]";
115 		case compile: return "[BUILDING]";
116 		case compileAndRun: return "[BUILDING]";
117 		case test: return "[TESTING]";
118 	}
119 	assert(false);
120 }
121 
122 struct ServerInfo
123 {
124 	string name;
125 	string ip;
126 	ushort port;
127 }
128 
129 immutable buildFolder = "builds/default";
130 immutable configFolder = "config";
131 immutable serversFname = "config/servers.txt";
132 struct Launcher
133 {
134 	string pluginFolderPath;
135 	string pluginPackFolderPath;
136 	PluginInfo*[] plugins;
137 	PluginInfo*[string] pluginsById;
138 	PluginPack*[] pluginPacks;
139 	PluginPack*[string] pluginsPacksById;
140 	ServerInfo*[] servers;
141 
142 	Job*[] jobs;
143 	size_t numRunningJobs;
144 	LineBuffer appLog;
145 
146 	void createJob(JobParams params = JobParams.init)
147 	{
148 		auto job = new Job(params);
149 		job.messageWindow.init();
150 		job.messageWindow.messageHandler = (string com)=>sendCommand(job,com);
151 		updateJobType(job);
152 		restartJobState(job);
153 		updateTitle(job);
154 		jobs ~= job;
155 	}
156 
157 	static void updateTitle(Job* job)
158 	{
159 		string title = job.params.appType == AppType.client ? `Client` : `Server`;
160 		job.title = title;
161 	}
162 
163 	static void updateJobType(Job* job)
164 	{
165 		final switch(job.params.jobType) with(JobType) {
166 			case run:
167 				job.params.build = No.build;
168 				job.params.start = Yes.start;
169 				break;
170 			case compile:
171 				job.params.build = Yes.build;
172 				job.params.start = No.start;
173 				break;
174 			case compileAndRun:
175 				job.params.build = Yes.build;
176 				job.params.start = Yes.start;
177 				break;
178 			case test:
179 				job.params.build = Yes.build;
180 				job.params.start = No.start;
181 				break;
182 		}
183 	}
184 
185 	static void restartJobState(Job* job)
186 	{
187 		final switch(job.params.jobType) with(JobType) {
188 			case run: job.jobState = JobState.run; break;
189 			case compile: job.jobState = JobState.build; break;
190 			case compileAndRun: job.jobState = JobState.build; break;
191 			case test: job.jobState = JobState.build; break;
192 		}
193 	}
194 
195 	void startJob(Job* job)
196 	{
197 		assert(!job.isRunning);
198 		++numRunningJobs;
199 
200 		updateJobType(job);
201 		updateTitle(job);
202 
203 		string command;
204 		string workDir;
205 
206 		if (job.jobState == JobState.build)
207 		{
208 			final switch(job.params.jobType) with(JobType) {
209 				case run: return;
210 				case compile: goto case;
211 				case compileAndRun:
212 					command = makeCompileCommand(job.params);
213 					workDir = "";
214 					break;
215 				case test:
216 					command = makeTestCommand(job.params);
217 					workDir = "";
218 					break;
219 			}
220 		}
221 		else if (job.jobState == JobState.run) {
222 			command = makeRunCommand(job.params);
223 			workDir = buildFolder;
224 		}
225 
226 		ProcessPipes pipes = pipeShell(command, Redirect.all, null, Config.none, workDir);
227 
228 		(*job) = Job(job.params, command, job.messageWindow, pipes, job.jobState, job.title);
229 		job.isRunning = true;
230 	}
231 
232 	size_t stopProcesses()
233 	{
234 		foreach(job; jobs)
235 			job.pipes.pid.kill;
236 		return jobs.length;
237 	}
238 
239 	bool anyProcessesRunning() @property
240 	{
241 		return numRunningJobs > 0;
242 	}
243 
244 	void update()
245 	{
246 		foreach(job; jobs) logPipes(job);
247 
248 		foreach(job; jobs)
249 		{
250 			if (job.isRunning)
251 			{
252 				auto res = job.pipes.pid.tryWait();
253 				if (res.terminated)
254 				{
255 					--numRunningJobs;
256 					job.isRunning = false;
257 					job.status = res.status;
258 
259 					bool success = job.status == 0;
260 					bool doneBuild = job.jobState == JobState.build;
261 					bool needsStart = job.params.start;
262 					if (doneBuild)
263 					{
264 						onJobBuildCompletion(job, job.status == 0);
265 					}
266 					if (success && doneBuild && needsStart)
267 					{
268 						job.jobState = JobState.run;
269 						startJob(job);
270 					}
271 				}
272 			}
273 
274 			if (!job.isRunning && job.needsRestart)
275 			{
276 				job.messageWindow.lineBuffer.clear();
277 				restartJobState(job);
278 				startJob(job);
279 			}
280 
281 			job.needsRestart = false;
282 		}
283 
284 		jobs = remove!(a => a.needsClose && !a.isRunning)(jobs);
285 		jobs.each!(j => j.needsClose = false);
286 	}
287 
288 	void setRootPath(string pluginFolder, string pluginPackFolder, string toolFolder)
289 	{
290 		pluginFolderPath = pluginFolder;
291 		pluginPackFolderPath = pluginPackFolder;
292 	}
293 
294 	void clear()
295 	{
296 		plugins = null;
297 		pluginsById = null;
298 		pluginPacks = null;
299 		pluginsPacksById = null;
300 	}
301 
302 	void readPlugins()
303 	{
304 		if (!exists(pluginFolderPath)) return;
305 		foreach (entry; dirEntries(pluginFolderPath, SpanMode.depth))
306 		{
307 			if (entry.isFile && baseName(entry.name) == "plugininfo.d")
308 			{
309 				string fileData = cast(string)read(entry.name);
310 				auto p = readPluginInfo(fileData);
311 				plugins ~= p;
312 				pluginsById[p.id] = p;
313 			}
314 		}
315 	}
316 
317 	void readPluginPacks()
318 	{
319 		foreach (entry; dirEntries(pluginPackFolderPath, SpanMode.depth))
320 		{
321 			if (entry.isFile && entry.name.extension == ".txt")
322 			{
323 				string fileData = cast(string)read(entry.name);
324 				auto pack = readPluginPack(fileData);
325 				pack.filename = entry.name.absolutePath.buildNormalizedPath;
326 				pluginPacks ~= pack;
327 				pluginsPacksById[pack.id] = pack;
328 			}
329 		}
330 	}
331 
332 	void readServers()
333 	{
334 		import std.regex : matchFirst, ctRegex;
335 		if (!exists(serversFname)) return;
336 		string serversData = cast(string)read(serversFname);
337 		foreach(line; serversData.lineSplitter)
338 		{
339 			auto serverInfoStr = matchFirst(line, ctRegex!(`(?P<ip>[^:]*):(?P<port>\d{1,5})\s*(?P<name>.*)`, "s"));
340 			auto sinfo = new ServerInfo;
341 			sinfo.ip = serverInfoStr["ip"].toCString;
342 			sinfo.port = to!ushort(serverInfoStr["port"]);
343 			sinfo.name = serverInfoStr["name"].toCString;
344 			infof("%s", *sinfo);
345 			servers ~= sinfo;
346 		}
347 	}
348 
349 	void addServer(ServerInfo server)
350 	{
351 		auto info = new ServerInfo();
352 		*info = server;
353 		servers ~= info;
354 		saveServers();
355 	}
356 
357 	void removeServer(size_t serverIndex)
358 	{
359 		servers = remove(servers, serverIndex);
360 		saveServers();
361 	}
362 
363 	void saveServers()
364 	{
365 		import std.exception;
366 		try
367 		{
368 			auto f = File(serversFname, "w");
369 			foreach(server; servers)
370 			{
371 				f.writefln("%s:%s %s", server.ip, server.port, server.name);
372 			}
373 		}
374 		catch(ErrnoException e)
375 		{
376 			error(e);
377 		}
378 	}
379 }
380 
381 string makeCompileCommand(JobParams params)
382 {
383 	immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`;
384 	immutable deps = params.nodeps ? ` --nodeps` : ``;
385 	immutable doForce = params.force ? ` --force` : ``;
386 	immutable release = params.release ? `--build=release` : `--build=debug`;
387 	immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]);
388 	return format("dub build -q %s %s --config=exe%s%s %s\0", arch, compiler, deps, doForce, release)[0..$-1];
389 }
390 
391 string makeRunCommand(JobParams params)
392 {
393 	string conf = params.appType == AppType.client ? `voxelman.exe --app=client` : `voxelman.exe --app=server`;
394 	string command = conf;
395 
396 	foreach(paramName, paramValue; params.runParameters)
397 	{
398 		if (paramValue)
399 			command ~= format(" --%s=%s", paramName, paramValue);
400 		else
401 			command ~= format(" --%s", paramName);
402 	}
403 
404 	command ~= '\0';
405 	return command[0..$-1];
406 }
407 
408 string makeTestCommand(JobParams params)
409 {
410 	immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`;
411 	immutable deps = params.nodeps ? ` --nodeps` : ``;
412 	immutable doForce = params.force ? ` --force` : ``;
413 	immutable compiler = format(`--compiler=%s`, compilerExeNames[params.compiler]);
414 	return format("dub test -q %s %s %s %s\0", arch, compiler, deps, doForce)[0..$-1];
415 }
416 
417 void onJobBuildCompletion(Job* job, bool success)
418 {
419 	if (success)
420 	{
421 		if (job.params.jobType != JobType.test)
422 			job.messageWindow.putln("Compilation successful");
423 	}
424 	else
425 	{
426 		if (job.params.jobType != JobType.test)
427 			job.messageWindow.putln("Compilation failed");
428 	}
429 }
430 
431 void sendCommand(Job* job, string command)
432 {
433 	if (!job.isRunning) return;
434 	job.pipes.stdin.rawWrite(command);
435 	job.pipes.stdin.rawWrite("\n");
436 }
437 
438 void logPipes(Job* job)
439 {
440 	import std.exception : ErrnoException;
441 	import std.utf : UTFException;
442 	if (!job.isRunning) return;
443 
444 	try
445 	{
446 		foreach(pipe; only(job.pipes.stdout, job.pipes.stderr))
447 		{
448 			auto size = pipe.size;
449 			if (size > 0)
450 			{
451 				char[1024] buf;
452 				size_t charsToRead = min(pipe.size, buf.length);
453 				char[] data = pipe.rawRead(buf[0..charsToRead]);
454 				job.messageWindow.lineBuffer.put(data);
455 			}
456 		}
457 	}
458 	catch(ErrnoException e)
459 	{	// Ignore e
460 		// It happens only when both launcher and child process is 32bit
461 		// and child crashes with access violation (in opengl call for example).
462 		// exception std.exception.ErrnoException@std\stdio.d(920):
463 		// Could not seek in file `HANDLE(32C)' (Invalid argument)
464 	}
465 	catch(UTFException e)
466 	{	// Ignore e
467 	}
468 }
469 
470 PluginInfo* readPluginInfo(string fileData)
471 {
472 	import std.regex : matchFirst, ctRegex;
473 
474 	auto pinfo = new PluginInfo;
475 
476 	auto idCapture = matchFirst(fileData, ctRegex!(`id\s*=\s*"(?P<id>[^"]*)"`, "s"));
477 	pinfo.id = idCapture["id"].toCString;
478 
479 	auto semverCapture = matchFirst(fileData, ctRegex!(`semver\s*=\s*"(?P<semver>[^"]*)"`, "s"));
480 	pinfo.semver = semverCapture["semver"].toCString;
481 
482 	return pinfo;
483 }
484 
485 PluginPack* readPluginPack(string fileData)
486 {
487 	import std.array : empty;
488 	import std.regex : matchFirst, ctRegex;
489 	import std.string : lineSplitter;
490 
491 	auto pack = new PluginPack;
492 
493 	auto input = fileData.lineSplitter;
494 
495 	if (!input.empty) {
496 		auto packInfo = matchFirst(input.front, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m"));
497 		pack.id = packInfo["id"].toCString;
498 		pack.semver = packInfo["semver"].toCString;
499 		input.popFront;
500 	}
501 
502 	foreach(line; input)
503 	{
504 		if (line.empty)
505 			continue;
506 
507 		auto pluginInfo = matchFirst(line, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m"));
508 		auto pinfo = new PluginInfo;
509 		pinfo.id = pluginInfo["id"].toCString;
510 		pinfo.semver = pluginInfo["semver"].toCString;
511 		pack.plugins ~= pinfo;
512 	}
513 
514 	return pack;
515 }
516 
517 string toCString(in const(char)[] s)
518 {
519 	import std.exception : assumeUnique;
520 	auto copy = new char[s.length + 1];
521 	copy[0..s.length] = s[];
522 	copy[s.length] = '\0';
523 	return assumeUnique(copy[0..s.length]);
524 }
525 
526 string fromCString(char[] str)
527 {
528 	char[] chars = str.ptr.fromStringz();
529 	return chars.ptr[0..chars.length+1].idup[0..$-1];
530 }