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 }