1 /**
2 Copyright: Copyright (c) 2015 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 import std.typecons : Flag, Yes, No;
17 
18 import voxelman.utils.messagewindow;
19 import voxelman.utils.linebuffer;
20 import gui;
21 
22 struct PluginInfo
23 {
24 	string id;
25 	string semver;
26 	string downloadUrl;
27 	bool isEnabled = true;
28 	PluginInfo*[] dependencies;
29 	PluginInfo*[] dependants;
30 }
31 
32 struct PluginPack
33 {
34 	string id;
35 	string semver;
36 	string filename;
37 	PluginInfo*[] plugins;
38 }
39 
40 enum AppType
41 {
42 	client,
43 	server
44 }
45 
46 enum JobType : int
47 {
48 	run,
49 	compile,
50 	compileAndRun
51 }
52 
53 enum JobState
54 {
55 	compile,
56 	run
57 }
58 
59 struct JobParams
60 {
61 	string pluginPack = "default";
62 	AppType appType = AppType.client;
63 	Flag!"start" start = Yes.start;
64 	Flag!"build" build = Yes.build;
65 	Flag!"arch64" arch64 = Yes.arch64;
66 	Flag!"nodeps" nodeps = Yes.nodeps;
67 	Flag!"force" force = No.force;
68 	Flag!"release" release = No.release;
69 	JobType jobType;
70 }
71 
72 struct Job
73 {
74 	JobParams params;
75 	string command;
76 	MessageWindow messageWindow;
77 	ProcessPipes pipes;
78 
79 	JobState jobState = JobState.compile;
80 	bool isRunning;
81 	bool needsClose;
82 	bool needsRestart;
83 	int status;
84 }
85 
86 immutable buildFolder = "builds/default";
87 struct Launcher
88 {
89 	string pluginFolderPath;
90 	string pluginPackFolderPath;
91 	PluginInfo*[] plugins;
92 	PluginInfo*[string] pluginsById;
93 	PluginPack*[] pluginPacks;
94 	PluginPack*[string] pluginsPacksById;
95 
96 	Job*[] jobs;
97 	size_t numRunningJobs;
98 	LineBuffer appLog;
99 
100 	void startJob(JobParams params = JobParams.init)
101 	{
102 		auto job = new Job(params);
103 		job.messageWindow.init();
104 		job.messageWindow.messageHandler = (string com)=>sendCommand(job,com);
105 		updateJobType(job);
106 		restartJobState(job);
107 		restartJob(job);
108 		jobs ~= job;
109 	}
110 
111 	void updateJobType(Job* job)
112 	{
113 		final switch(job.params.jobType) with(JobType) {
114 			case run:
115 				job.params.build = No.build;
116 				job.params.start = Yes.start;
117 				break;
118 			case compile:
119 				job.params.build = Yes.build;
120 				job.params.start = No.start;
121 				break;
122 			case compileAndRun:
123 				job.params.build = Yes.build;
124 				job.params.start = Yes.start;
125 				break;
126 		}
127 	}
128 
129 	void restartJobState(Job* job)
130 	{
131 		final switch(job.params.jobType) with(JobType) {
132 			case run: job.jobState = JobState.run; break;
133 			case compile: job.jobState = JobState.compile; break;
134 			case compileAndRun: job.jobState = JobState.compile; break;
135 		}
136 	}
137 
138 	void restartJob(Job* job)
139 	{
140 		assert(!job.isRunning);
141 		++numRunningJobs;
142 
143 		string command;
144 		string workDir;
145 		if (job.jobState == JobState.compile) {
146 			command = makeCompileCommand(job.params);
147 			workDir = "";
148 		}
149 		else if (job.jobState == JobState.run) {
150 			command = makeRunCommand(job.params);
151 			workDir = buildFolder;
152 		}
153 
154 		ProcessPipes pipes = pipeShell(command, Redirect.all, null, Config.none, workDir);
155 
156 		(*job) = Job(job.params, command, job.messageWindow, pipes, job.jobState);
157 		job.isRunning = true;
158 	}
159 
160 	size_t stopProcesses()
161 	{
162 		foreach(job; jobs)
163 			job.pipes.pid.kill;
164 		return jobs.length;
165 	}
166 
167 	bool anyProcessesRunning() @property
168 	{
169 		return numRunningJobs > 0;
170 	}
171 
172 	void update()
173 	{
174 		foreach(job; jobs) logPipes(job);
175 
176 		foreach(job; jobs)
177 		{
178 			if (job.isRunning)
179 			{
180 				auto res = job.pipes.pid.tryWait();
181 				if (res.terminated)
182 				{
183 					--numRunningJobs;
184 					job.isRunning = false;
185 					job.status = res.status;
186 
187 					bool success = job.status == 0;
188 					bool doneCompilation = job.jobState == JobState.compile;
189 					bool needsStart = job.params.start;
190 					if (doneCompilation) job.messageWindow.putln("Compilation successful");
191 					if (success && doneCompilation && needsStart)
192 					{
193 						job.jobState = JobState.run;
194 						job.messageWindow.lineBuffer.clear();
195 						restartJob(job);
196 					}
197 				}
198 			}
199 
200 			if (!job.isRunning && job.needsRestart)
201 			{
202 				job.messageWindow.lineBuffer.clear();
203 				restartJobState(job);
204 				restartJob(job);
205 			}
206 
207 			job.needsRestart = false;
208 		}
209 
210 		jobs = remove!(a => a.needsClose && !a.isRunning)(jobs);
211 		jobs.each!(j => j.needsClose = false);
212 	}
213 
214 	void setRootPath(string pluginFolder, string pluginPackFolder)
215 	{
216 		pluginFolderPath = pluginFolder;
217 		pluginPackFolderPath = pluginPackFolder;
218 	}
219 
220 	void clear()
221 	{
222 		plugins = null;
223 		pluginsById = null;
224 		pluginPacks = null;
225 		pluginsPacksById = null;
226 	}
227 
228 	void readPlugins()
229 	{
230 		import std.file : exists, read, dirEntries, SpanMode;
231 		import std.path : baseName;
232 
233 		if (!exists(pluginFolderPath)) return;
234 		foreach (entry; dirEntries(pluginFolderPath, SpanMode.depth))
235 		{
236 			if (entry.isFile && baseName(entry.name) == "plugininfo.d")
237 			{
238 				string fileData = cast(string)read(entry.name);
239 				auto p = readPluginInfo(fileData);
240 				plugins ~= p;
241 				pluginsById[p.id] = p;
242 			}
243 		}
244 	}
245 
246 	void printPlugins()
247 	{
248 		foreach(p; plugins)
249 		{
250 			infof("%s %s", p.id, p.semver);
251 		}
252 	}
253 
254 	void readPluginPacks()
255 	{
256 		import std.file : read, dirEntries, SpanMode;
257 		import std.path : extension, absolutePath, buildNormalizedPath;
258 
259 		foreach (entry; dirEntries(pluginPackFolderPath, SpanMode.depth))
260 		{
261 			if (entry.isFile && entry.name.extension == ".txt")
262 			{
263 				string fileData = cast(string)read(entry.name);
264 				auto pack = readPluginPack(fileData);
265 				pack.filename = entry.name.absolutePath.buildNormalizedPath;
266 				pluginPacks ~= pack;
267 				pluginsPacksById[pack.id] = pack;
268 			}
269 		}
270 	}
271 }
272 
273 
274 string makeCompileCommand(JobParams params)
275 {
276 	immutable arch = params.arch64 ? `--arch=x86_64` : `--arch=x86`;
277 	immutable conf = params.appType == AppType.client ? `--config=client` : `--config=server`;
278 	immutable deps = params.nodeps ? ` --nodeps` : ``;
279 	immutable doForce = params.force ? ` --force` : ``;
280 	immutable release = params.release ? `--build=release` : `--build=debug`;
281 	return format("dub build -q %s %s%s%s %s\0", arch, conf, deps, doForce, release);
282 }
283 
284 string makeRunCommand(JobParams params)
285 {
286 	string conf = params.appType == AppType.client ? `client.exe` : `server.exe`;
287 	return format("%s --pack=%s\0", conf, params.pluginPack);
288 }
289 
290 void sendCommand(Job* job, string command)
291 {
292 	if (job.jobState != JobState.run) return;
293 	job.pipes.stdin.rawWrite(command);
294 	job.pipes.stdin.rawWrite("\n");
295 }
296 
297 void logPipes(Job* job)
298 {
299 	import std.exception : ErrnoException;
300 	try
301 	{
302 		foreach(pipe; only(job.pipes.stdout, job.pipes.stderr))
303 		{
304 			auto size = pipe.size;
305 			if (size > 0)
306 			{
307 				char[1024] buf;
308 				size_t charsToRead = min(pipe.size, buf.length);
309 				char[] data = pipe.rawRead(buf[0..charsToRead]);
310 				job.messageWindow.lineBuffer.put(data);
311 			}
312 		}
313 	}
314 	catch(ErrnoException e)
315 	{	// Ignore e
316 		// It happens only when both launcher and child process is 32bit
317 		// and child crashes with access violation (in opengl call for example).
318 		// exception std.exception.ErrnoException@std\stdio.d(920):
319 		// Could not seek in file `HANDLE(32C)' (Invalid argument)
320 	}
321 }
322 
323 PluginInfo* readPluginInfo(string fileData)
324 {
325 	import std.regex : matchFirst, ctRegex;
326 
327 	auto pinfo = new PluginInfo;
328 
329 	auto idCapture = matchFirst(fileData, ctRegex!(`id\s*=\s*"(?P<id>[^"]*)"`, "s"));
330 	pinfo.id = idCapture["id"].toCString;
331 
332 	auto semverCapture = matchFirst(fileData, ctRegex!(`semver\s*=\s*"(?P<semver>[^"]*)"`, "s"));
333 	pinfo.semver = semverCapture["semver"].toCString;
334 
335 	return pinfo;
336 }
337 
338 PluginPack* readPluginPack(string fileData)
339 {
340 	import std.array : empty;
341 	import std.regex : matchFirst, ctRegex;
342 	import std.string : lineSplitter;
343 
344 	auto pack = new PluginPack;
345 
346 	auto input = fileData.lineSplitter;
347 
348 	if (!input.empty) {
349 		auto packInfo = matchFirst(input.front, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m"));
350 		pack.id = packInfo["id"].toCString;
351 		pack.semver = packInfo["semver"].toCString;
352 		input.popFront;
353 	}
354 
355 	foreach(line; input)
356 	{
357 		if (line.empty)
358 			continue;
359 
360 		auto pluginInfo = matchFirst(line, ctRegex!(`(?P<id>.*) (?P<semver>.*)`, "m"));
361 		auto pinfo = new PluginInfo;
362 		pinfo.id = pluginInfo["id"].toCString;
363 		pinfo.semver = pluginInfo["semver"].toCString;
364 		pack.plugins ~= pinfo;
365 	}
366 
367 	return pack;
368 }
369 
370 string toCString(in const(char)[] s)
371 {
372 	import std.exception : assumeUnique;
373 	auto copy = new char[s.length + 1];
374 	copy[0..s.length] = s[];
375 	copy[s.length] = 0;
376 	return assumeUnique(copy[0..s.length]);
377 }