Builder Your Own Code Runner

Builder Your Own Code Runner

Nowadays, there are a lot of online coding platform, like LeetCode and CoderPad. Such platforms provide an IDE at the frontend for the user to write code. When they hit ‘Run’, the code will be automatically executed in remote server and have the result back to the user. RunCode is my prototype of such platform. In this article, we will build our own!

Docker

We need docker to separate each user’s code and output file. By creating individual container for each code execution, we will not worry user’s malicious code like rm -rf / that will delete all the server. Besides, we can separate stdout so that users’ output won’t be interleaved.

After installing docker, we will pull some alpine images for C/C++, Java, Python. I choose alpine because of its small size.

For C&C++, we only need to install g++ (it will install gcc as well). Creating the following Dockerfile.

FROM alpine:3.10
RUN apk add --no-cache g++

run docker build -t cpp . to build the image with tag cpp.

For Java and Python, we will use the official alpine image

docker pull python:3.7-alpine
docker pull java:alpine

Build the Command

Memory Restriction

It’s important that each user’s program will consume the memory from the host server indefinitely. We need to set memory restraint by using docker -m

For example, limit the maximum memory usage is 64MB.

docker -m 64M --memory-swap 64M

However, if you see the warning No swap limit support. We need to change /etc/default/grub according to this solution. Especially for Google Cloud, we need to change /etc/default/grub.d/50-cloudimg-settings.cfg instead.

After that, we will not see the warning.

CleanUp

using docker --rm to clean up once done.

Timeout

It’s also important to set timeout so that malicious code (while True: pass) will not get executed indefinitely. However, it seems docker doesn’t support this natively. We may need to rely on command timeout to send SIGKILL when some time period is passed (5s). After that, we still need to clean up all the container resources, therefore we may need a random name for each container. So in case of timeout, we can use this name to kill this container first then combined with --rm, the container will be removed.

Folder mapping

For each code execution, we need to create a new folder that contains the user input file, which will map(mount) it to the docker container by using docker -v

docker -v /home/xinyi/random-folder:/code

Therefore inside the folder /code of the container, it contains our input file.

Compile & Run

Python

Python is the easiest to do, but we need to use -u to get the output

docker run --rm -m 64M --memory-swap 64M --name cb8c6ecf-4afa-4fb0-9f46-a3a9dd982ca8 -v /runcode/input/cb8c6ecf-4afa-4fb0-9f46-a3a9dd982ca8:/code -w /code python:3.7-alpine python3 -u file.py

C/C++

docker run --rm -m 64M --memory-swap 64M --name 00854b7c-f768-406a-80cc-1851d2228920 -v /runcode/input/00854b7c-f768-406a-80cc-1851d2228920:/code -w /code cpp /bin/sh -c "g++ -Wall file.cpp -o a && ./a >&1 | tee"

I find only in this way we can get the multiprocessing output.

Java

Java is the most tricky one, since we need to find the class with function main. Besides, if there is a public class, the filename should be exactly the same as the class name. Therefore, we need to do preprocessing on the input file and rename the file if possible.

Regex /^\s*public class\s+([^\s]+)/m is used to get the public class name, if any, rename the java file to the class name.

Then using runJava.sh to compile and run.

javac $1
if [ $? -ne 0 ]; then # java compile error
exit 1
fi
for classfile in *.class; do
classname=${classfile%.*}

#Execute fgrep with -q option to not display anything on stdout when the match is found
if javap -public $classname | fgrep -q 'public static void main'; then
java $classname "$@"
exit 0;
fi
done

Therefore, an additional read-only mapping for runJava.sh is necessary

docker run --rm -m 64M --memory-swap 64M --name 00854b7c-f768-406a-80cc-1851d2228920 -v /runcode/input/00854b7c-f768-406a-80cc-1851d2228920:/code -w /code -v /runcode/input/runJava.sh:/code/runJava.sh:ro  java:alpine sh -c "./runJava.sh YourClass.java"

Error&Output

We can determine error, timeout or actual output from err, stdout and stderr like the following Node.js snippet.

static async dockerRunAndCleanup(id, cmd) {
return new Promise(((resolve, reject) => {
exec(cmd, {timeout: this.TimeoutMs, killSignal: 'SIGKILL'}, (err, stdout, stderr) => {
if (stderr) {
if (stderr.startsWith("docker: Error response from daemon: OCI runtime"))
reject("Out of Memory 64MB");
else
reject(stderr);
}
if (err) {
if (err.killed) { // timeout
exec(`${this.dockerContainerKill} ${id}`); // kill the container
resolve(stdout + `\nTimeout after ${this.TimeoutMs}ms`);
} else
reject(err.message.split('\n').slice(1).join('\n'));
} else
resolve(stdout);
});
}))
}
# Docker
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×