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 	dlangtour/core-exec to compile and run
17 	the resulting binary.
18 +/
19 class Docker: IExecProvider
20 {
21 	private immutable BaseDockerImage = "dlangtour/core-exec";
22 	private immutable DockerImages = [
23 		BaseDockerImage ~ ":dmd",
24 		BaseDockerImage ~ ":dmd-beta",
25 		BaseDockerImage ~ ":dmd-nightly",
26 		BaseDockerImage ~ ":ldc",
27 		BaseDockerImage ~ ":ldc-beta",
28 		//BaseDockerImage ~ ":gdc"
29 		"dlangtour/core-dreg:latest",
30 	];
31 
32 	private int timeLimitInSeconds_;
33 	private int maximumOutputSize_;
34 	private int maximumQueueSize_;
35 	private shared int queueSize_;
36 	private int memoryLimitMB_;
37 	private string dockerBinaryPath_;
38 
39 
40 	this(int timeLimitInSeconds, int maximumOutputSize,
41 			int maximumQueueSize, int memoryLimitMB,
42 			string dockerBinaryPath, bool waitUntilPulled)
43 	{
44 		this.timeLimitInSeconds_ = timeLimitInSeconds;
45 		this.maximumOutputSize_ = maximumOutputSize;
46 		this.queueSize_ = 0;
47 		this.maximumQueueSize_ = maximumQueueSize;
48 		this.memoryLimitMB_ = memoryLimitMB;
49 		this.dockerBinaryPath_ = dockerBinaryPath;
50 
51 		logInfo("Initializing Docker driver");
52 		logInfo("Docker binary path: %s", dockerBinaryPath_);
53 		logInfo("Time Limit: %d s", timeLimitInSeconds_);
54 		logInfo("Output size limit: %d B", maximumOutputSize_);
55 		logInfo("Maximum Queue Size: %d", maximumQueueSize_);
56 		logInfo("Memory Limit: %d MB", memoryLimitMB_);
57 
58 		import std.algorithm.iteration : filter;
59 		import std.concurrency : ownerTid, receiveOnly, send, spawn;
60 		import std.parallelism : parallel;
61 
62 		// Temporarily share the DockerExecProvider across all threads
63 		__gshared typeof(this) inst;
64 		inst = this;
65 		// updating the docker images should happen in the background
66 		spawn((string dockerBinaryPath, in string[] dockerImages) {
67 			// core-dreg is a very large Docker image (> 3 GB)
68 			// Thus constantly pulling it on every CI build is problematic
69 			foreach (dockerImage; dockerImages.filter!(a => a != "dlangtour/core-dreg:latest").parallel)
70 			{
71 
72 				logInfo("Checking whether Docker is functional and updating Docker image '%s'", dockerImage);
73 				logInfo("Using docker binary at '%s'", dockerBinaryPath);
74 
75 				auto docker = execute([dockerBinaryPath, "ps"]);
76 				if (docker.status != 0) {
77 					throw new Exception("Docker doesn't seem to be functional. Error: '"
78 							~ docker.output ~ "'. RC: " ~ to!string(docker.status));
79 				}
80 
81 				auto dockerPull = execute([dockerBinaryPath, "pull", dockerImage]);
82 				if (docker.status != 0) {
83 					throw new Exception("Failed pulling RDMD Docker image. Error: '" ~ docker.output
84 							~ "'. RC: " ~ to!string(docker.status));
85 				}
86 				logInfo("Pulled Docker image '%s'.", dockerImage);
87 
88 				logInfo("Verifying functionality with 'Hello World' program...");
89 				RunInput input = {
90 					source: q{void main() { import std.stdio; write("Hello World"); }}
91 				};
92 				auto result = inst.compileAndExecute(input);
93 				enforce(result.success && result.output == "Hello World",
94 						new Exception("Compiling 'Hello World' wasn't successful: " ~ result.output));
95 			}
96 			// Remove previous, untagged images
97 			//executeShell("docker images --no-trunc | grep '<none>' | awk '{ print $3 }' | xargs -r docker rmi");
98 			ownerTid.send(true);
99 		}, this.dockerBinaryPath_, DockerImages);
100 		if (waitUntilPulled)
101 			assert(receiveOnly!bool, "Docker pull failed");
102 	}
103 
104 	Tuple!(string, "output", bool, "success") compileAndExecute(RunInput input)
105 	{
106 		import std.string: format;
107 		import std.algorithm.searching : canFind, find;
108 
109 		if (queueSize_ > maximumQueueSize_) {
110 			return typeof(return)("Maximum number of parallel compiles has been exceeded. Try again later.", false);
111 		}
112 
113 		import core.atomic : atomicOp;
114 		atomicOp!"+="(queueSize_, 1);
115 		scope(exit) atomicOp!"-="(queueSize_, 1);
116 
117 
118 		auto encoded = Base64.encode(cast(ubyte[]) input.source);
119 		// try to find the compiler in the available images
120 		auto r = DockerImages.find!(d => d.canFind(input.compiler));
121 		// use dmd as fallback
122 		const dockerImage = (r.length > 0) ? r[0] : DockerImages[0];
123 
124         auto env = [
125             "DOCKER_FLAGS": input.args,
126             "DOCKER_RUNTIME_ARGS": input.runtimeArgs,
127             "DOCKER_COLOR": input.color ? "on" : "off",
128         ];
129 
130         auto args = [this.dockerBinaryPath_, "run", "--rm",
131 		    "-e", "DOCKER_COLOR",
132 		    "-e", "DOCKER_FLAGS",
133 		    "-e", "DOCKER_RUNTIME_ARGS",
134 			"--net=none", "--memory-swap=-1",
135 			"-m", to!string(memoryLimitMB_ * 1024 * 1024),
136 			dockerImage, encoded];
137 		if (input.stdin) {
138 			args ~= Base64.encode(cast(ubyte[]) input.stdin);
139 		}
140 
141 		auto docker = pipeProcess(args,
142 				Redirect.stdout | Redirect.stderrToStdout | Redirect.stdin, env);
143 		docker.stdin.write(encoded);
144 		docker.stdin.flush();
145 		docker.stdin.close();
146 
147 		bool success;
148 		auto startTime = MonoTime.currTime();
149 
150 		logInfo("Executing Docker image %s with env='%s'", dockerImage, env);
151 
152 		string output;
153 		enum bufReadLength = 4096;
154 		// returns true if the maximum output limit has been exceeded
155 		bool readFromPipe() {
156             while (true) {
157                 auto buf = docker.stdout.rawRead(new char[bufReadLength]);
158                 output ~= buf;
159 				if (output.length > maximumOutputSize_) {
160 					output ~= "\n\n---Program's output exceeds limit of %d bytes.---".format(maximumOutputSize_);
161 					return true;
162 				}
163 				if (buf.length < bufReadLength)
164 				    break;
165 			}
166 			return false;
167 		}
168 
169 		// Don't block and give away current time slice
170 		// by sleeping for a certain time until child process has finished. Kill process if time limit
171 		// has been reached.
172 		while (true) {
173 			auto result = tryWait(docker.pid);
174 			if (MonoTime.currTime() - startTime > timeLimitInSeconds_.seconds) {
175 				// send SIGKILL 9 to process
176 				kill(docker.pid, 9);
177 				return typeof(return)("Compilation or running program took longer than %d seconds. Aborted!".format(timeLimitInSeconds_), false);
178 			}
179 			if (result.terminated) {
180 				success = result.status == 0;
181 				break;
182 			}
183 
184 			sleep(50.msecs);
185 			if (readFromPipe())
186 				return typeof(return)(output, success);
187 		}
188 		readFromPipe();
189 
190 		return typeof(return)(output, success);
191 	}
192 
193 	Package[] installedPackages()
194 	{
195 		import std.array : array;
196 		import std.algorithm.iteration : filter, joiner, map, splitter;
197 		import std.range : empty, dropOne;
198 		import std.functional;
199 
200 		auto res = execute([dockerBinaryPath_, "run", "--rm", "--entrypoint=/bin/cat", DockerImages[0], "/installed_packages"]);
201 		enforce(res.status == 0, "Error:" ~ res.output);
202 		return res.output
203 			.splitter("\n")
204 			.filter!(not!empty)
205 			.map!((l){
206 				auto ps = l.splitter(":");
207 				return Package(ps.front, ps.dropOne.front.filter!(a => a != '"').to!string);
208 			})
209 			.array;
210 	}
211 }