1 module exec.docker;
2 
3 import exec.iexecprovider;
4 import std.process;
5 import vibe.core.core : sleep;
6 import core.time : msecs;
7 import vibe.core.log: logInfo;
8 import std.base64;
9 import std.datetime;
10 import std.typecons: Tuple;
11 import std.exception: enforce;
12 import std.conv: to;
13 
14 /+
15 	Execution provider that uses the Docker iamge
16 	stonemaster/dlang-tour-rdmd to compile and run
17 	the resulting binary.
18 +/
19 class Docker: IExecProvider
20 {
21 	private immutable DockerImage = "stonemaster/dlang-tour-rdmd";
22 
23 	private int timeLimitInSeconds_;
24 	private int maximumOutputSize_;
25 	private int maximumQueueSize_;
26 	private int queueSize_;
27 	private int memoryLimitMB_;
28 
29 	this(int timeLimitInSeconds, int maximumOutputSize,
30 			int maximumQueueSize, int memoryLimitMB)
31 	{
32 		this.timeLimitInSeconds_ = timeLimitInSeconds;
33 		this.maximumOutputSize_ = maximumOutputSize;
34 		this.queueSize_ = 0;
35 		this.maximumQueueSize_ = maximumQueueSize;
36 		this.memoryLimitMB_ = memoryLimitMB;
37 
38 		logInfo("Initializing Docker driver");
39 		logInfo("Time Limit: %d", timeLimitInSeconds_);
40 		logInfo("Maximum Queue Size: %d", maximumQueueSize_);
41 		logInfo("Memory Limit: %d MB", memoryLimitMB_);
42 		logInfo("Output size limit: %d B", maximumQueueSize_);
43 		logInfo("Checking whether Docker is functional and updating Docker image '%s'", DockerImage);
44 
45 		auto docker = execute(["docker", "ps"]);
46 		if (docker.status != 0) {
47 			throw new Exception("Docker doesn't seem to be functional. Error: " ~ docker.output);
48 		}
49 
50 		auto dockerPull = execute(["docker", "pull", DockerImage]);
51 		if (docker.status != 0) {
52 			throw new Exception("Failed pulling RDMD Docker image. Error: " ~ docker.output);
53 		}
54 
55 		logInfo("Pulled Docker image '%s'.", DockerImage);
56 		logInfo("Verifying functionality with 'Hello World' program...");
57 		auto result = compileAndExecute(q{void main() { import std.stdio; write("Hello World"); }});
58 		enforce(result.success && result.output == "Hello World",
59 				new Exception("Compiling 'Hello World' wasn't successful: " ~ result.output));
60 	}
61 
62 	Tuple!(string, "output", bool, "success") compileAndExecute(string source)
63 	{
64 		import std.string: format;
65 
66 		if (queueSize_ > maximumQueueSize_) {
67 			return typeof(return)("Maximum number of parallel compiles has been exceeded. Try again later.", false);
68 		}
69 
70 		++queueSize_;
71 		scope(exit)
72 			--queueSize_;
73 
74 		auto encoded = Base64.encode(cast(ubyte[])source);
75 		auto docker = pipeProcess(["docker", "run", "--rm",
76 				"--net=none", "--memory-swap=-1",
77 				"-m", to!string(memoryLimitMB_ * 1024 * 1024),
78 				DockerImage, encoded],
79 				Redirect.stdout | Redirect.stderrToStdout | Redirect.stdin);
80 		docker.stdin.write(encoded);
81 		docker.stdin.flush();
82 		docker.stdin.close();
83 
84 		bool success;
85 		auto startTime = Clock.currTime();
86 		// Don't block and give away current time slice
87 		// by sleeping for a certain time until child process has finished. Kill process if time limit
88 		// has been reached.
89 		while (true) {
90 			auto result = tryWait(docker.pid);
91 			if (Clock.currTime() - startTime > timeLimitInSeconds_.seconds) {
92 				// send SIGKILL 9 to process
93 				kill(docker.pid, 9);
94 				return typeof(return)("Compilation or running program took longer than %d seconds. Aborted!".format(timeLimitInSeconds_),
95 						false);
96 			}
97 			if (result.terminated) {
98 				success = result.status == 0;
99 				break;
100 			}
101 
102 			sleep(300.msecs);
103 		}
104 
105 		string output;
106 		foreach (chunk; docker.stdout.byChunk(4096)) {
107 			output ~= chunk;
108 			if (output.length > maximumOutputSize_) {
109 				return typeof(return)("Program's output exceeds limit of %d bytes.".format(maximumOutputSize_),
110 						false);
111 			}
112 		}
113 
114 		return typeof(return)(output, success);
115 	}
116 }