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 }