├── .github └── workflows │ └── build.yaml ├── .gitignore ├── Dockerfile ├── Dockerfile.release ├── LICENSE ├── README.md ├── Setup.hs ├── app └── Main.hs ├── cabal.project ├── config.example.yaml ├── docker-compose.example.yaml ├── hie.yaml.cbl ├── hie.yaml.stack ├── languages ├── apl │ ├── Dockerfile │ └── run.sh ├── bash │ ├── Dockerfile │ └── run.sh ├── brainfuck │ ├── Dockerfile │ ├── bf.cpp │ └── run.sh ├── c │ ├── Dockerfile │ └── run.sh ├── cpp │ ├── Dockerfile │ └── run.sh ├── csharp │ ├── Dockerfile │ └── run.sh ├── elixir │ ├── Dockerfile │ └── run.sh ├── erlang │ ├── Dockerfile │ └── run.sh ├── fsharp │ ├── Dockerfile │ └── run.sh ├── go │ ├── Dockerfile │ └── run.sh ├── haskell │ ├── Dockerfile │ └── run.sh ├── idris │ ├── Dockerfile │ └── run.sh ├── java │ ├── Dockerfile │ └── run.sh ├── javascript │ ├── Dockerfile │ └── run.sh ├── julia │ ├── Dockerfile │ └── run.sh ├── lua │ ├── Dockerfile │ └── run.sh ├── nim │ ├── Dockerfile │ └── run.sh ├── ocaml │ ├── Dockerfile │ └── run.sh ├── pascal │ ├── Dockerfile │ └── run.sh ├── perl │ ├── Dockerfile │ └── run.sh ├── php │ ├── Dockerfile │ └── run.sh ├── prolog │ ├── Dockerfile │ └── run.sh ├── python │ ├── Dockerfile │ └── run.sh ├── r │ ├── Dockerfile │ └── run.sh ├── racket │ ├── Dockerfile │ └── run.sh ├── ruby │ ├── Dockerfile │ └── run.sh ├── rust │ ├── Dockerfile │ └── run.sh └── typescript │ ├── Dockerfile │ └── run.sh ├── myriad.cabal ├── src ├── Myriad.hs └── Myriad │ ├── Config.hs │ ├── Core.hs │ ├── Docker.hs │ └── Server.hs └── stack.yaml /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Builds 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ghc: ['8.8.3'] 13 | os: [ubuntu-latest, macos-latest] 14 | include: # GHC 8.8.3 fails to install on Windows 15 | - ghc: '8.6.5' 16 | os: windows-latest 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - uses: actions/setup-haskell@v1.1.1 24 | with: 25 | ghc-version: ${{ matrix.ghc }} 26 | cabal-version: '3.2' 27 | 28 | # - name: Freeze 29 | # run: cabal freeze 30 | 31 | # - name: Cache Cabal 32 | # uses: actions/cache@v1.2.0 33 | # with: 34 | # path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} 35 | # key: ${{ runner.OS }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 36 | 37 | - name: Build Myriad 38 | run: cabal build -O2 myriad:exe:myriad 39 | 40 | - name: Find Binary 41 | id: find_binary 42 | shell: bash 43 | run: | 44 | FOUND=$(find dist-newstyle \( -name 'myriad' -o -name 'myriad.exe' \) -type f) 45 | cp $FOUND myriad 46 | cp config.example.yaml config.yaml 47 | strip myriad 48 | tar -cvzf myriad-${{ github.event.release.name }}-${{ runner.OS }}-${{ matrix.ghc }}.tar.gz config.yaml languages myriad 49 | 50 | - name: Upload Binary 51 | uses: actions/upload-release-asset@v1.0.2 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ github.event.release.upload_url }} 56 | asset_path: myriad-${{ github.event.release.name }}-${{ runner.OS }}-${{ matrix.ghc }}.tar.gz 57 | asset_name: myriad-${{ github.event.release.name }}-${{ runner.OS }}-${{ matrix.ghc }}.tar.gz 58 | asset_content_type: application/gzip 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # cabal 2 | dist-newstyle 3 | cabal.project.local 4 | 5 | # stack 6 | .stack-work 7 | stack.yaml.lock 8 | 9 | # hie 10 | hie.yaml 11 | 12 | # other 13 | config.yaml 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as build 2 | 3 | ENV LANG en_US.UTF-8 4 | ENV LANGUAGE en_US:en 5 | ENV LC_ALL en_US.UTF-8 6 | 7 | WORKDIR /tmp/haskell 8 | RUN apk update && \ 9 | apk upgrade --available && \ 10 | apk add \ 11 | build-base make cmake gcc gmp curl xz perl cpio coreutils \ 12 | binutils-gold tar gzip unzip \ 13 | libc-dev musl-dev ncurses-dev gmp-dev zlib-dev expat-dev libffi-dev \ 14 | gd-dev postgresql-dev linux-headers 15 | 16 | RUN curl https://gitlab.haskell.org/haskell/ghcup-hs/raw/master/bootstrap-haskell -sSf | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh && \ 17 | /root/.ghcup/bin/ghcup set 18 | ENV PATH "$PATH:/root/.cabal/bin:/root/.ghcup/bin" 19 | 20 | WORKDIR /tmp/myriad 21 | COPY . . 22 | RUN cabal new-install 23 | 24 | RUN mkdir -p /opt/myriad && \ 25 | cp -L /root/.cabal/bin/myriad /opt/myriad && \ 26 | mv languages /opt/myriad && \ 27 | mv config.example.yaml /opt/myriad/config.yaml 28 | 29 | 30 | FROM alpine:latest 31 | RUN apk add --no-cache docker-cli gmp 32 | WORKDIR /opt/myriad 33 | COPY --from=build /opt/myriad . 34 | 35 | EXPOSE 8081 36 | 37 | ENTRYPOINT ["./myriad"] -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-glibc:latest as build 2 | 3 | ARG MYRIAD_VERSION=0.5.0.3 4 | ARG GHC_VERSION=8.8.3 5 | 6 | RUN apk add --no-cache curl tar gzip 7 | WORKDIR /tmp/myriad 8 | RUN curl -OL https://github.com/1Computer1/myriad/releases/download/${MYRIAD_VERSION}/myriad-${MYRIAD_VERSION}-Linux-${GHC_VERSION}.tar.gz && \ 9 | tar -xzf myriad-${MYRIAD_VERSION}-Linux-${GHC_VERSION}.tar.gz && \ 10 | rm -f myriad-${MYRIAD_VERSION}-Linux-${GHC_VERSION}.tar.gz 11 | 12 | FROM frolvlad/alpine-glibc:latest 13 | RUN apk add --no-cache docker-cli gmp 14 | WORKDIR /opt/myriad 15 | COPY --from=build /tmp/myriad . 16 | 17 | EXPOSE 8081 18 | 19 | ENTRYPOINT ["./myriad"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 1Computer1 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Myriad 2 | 3 | Arbitrary code execution server using Docker. 4 | Each language has its own Docker image and so each evaluation will run in the respective language's locked-down container. 5 | 6 | Features include: 7 | 8 | - Building images on startup. 9 | - Preparing containers on startup or on demand. 10 | - Periodically cleanup running containers. 11 | - Customizable settings for each image: 12 | - Maximum memory usage. 13 | - Maximum CPU usage. 14 | - Maximum evaluation time. 15 | - Maximum concurrent evaluations. 16 | - Maximum number of retries. 17 | - Maximum output size. 18 | 19 | Requires Docker 18+ to operate. 20 | 21 | ## Download Pre-Built Binary 22 | 23 | Check the `Releases` tab for pre-built binaries. 24 | The languages folder and an example configuration are also included. 25 | 26 | ## Installation from Source 27 | 28 | You can use either `stack` or `cabal`. 29 | - `stack` should be >= 2.1.1, `cabal` should be >= 2.4.0.0. 30 | - GHC 8.8.3 is required if not already installed by `stack` or if using `cabal`. 31 | 32 | Make sure the place where `stack` or `cabal` places binaries is in your PATH. 33 | - For `stack`, you can get it with `stack path --local-bin`. 34 | - For `cabal`, you should find it in `$HOME/.cabal/bin` (Linux) or `%APPDATA%\cabal\bin` (Windows). 35 | 36 | Run `stack install` or `cabal new-install` inside the project folder. 37 | Or, to build within the project, run `stack build` or `cabal new-build`. 38 | 39 | ## Configure and Run 40 | 41 | Make sure the configuration is filled out, see `config.example.yaml` for an example. 42 | Run `myriad` (or `stack run` or `cabal new-run` if you built within the project) to start the server. 43 | The config and languages folder will default to `./config.yaml` and `./languages`. 44 | You can configure this with `--config` and `--languages`. 45 | 46 | ## Endpoints 47 | 48 | ### **GET** `/languages` 49 | 50 | List of enabled languages. 51 | Example response: 52 | 53 | ```json 54 | ["haskell", "javascript"] 55 | ``` 56 | 57 | ### **POST** `/eval` 58 | 59 | Evaluate code. 60 | JSON payload with `language` and `code` keys. 61 | The `language` is as in the name of a subfolder in the `languages` directory. 62 | Example payload: 63 | 64 | ```json 65 | { "language": "haskell", "code": "main = print (1 + 1)" } 66 | ``` 67 | 68 | Example response: 69 | 70 | ```json 71 | { "result": "2\n" } 72 | ``` 73 | 74 | Errors with 404 if `language` is not found, `504` if evaluation timed out, or `500` if evaluation failed for other reasons. 75 | 76 | ### **GET** `/containers` 77 | 78 | List of containers being handled by Myriad. 79 | 80 | ### **POST** `/cleanup` 81 | 82 | Kill all containers, giving back the names of the containers killed. 83 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Options.Applicative 4 | import Myriad 5 | 6 | data Args = Args 7 | { configPath :: FilePath 8 | , languagesDir :: FilePath 9 | } 10 | 11 | parseArgs :: IO Args 12 | parseArgs = execParser $ info (helper <*> args) (fullDesc <> progDesc "Run the Myriad server") 13 | where 14 | args = Args 15 | <$> option str (mconcat 16 | [ long "config" 17 | , short 'c' 18 | , help "Set the myriad configuration" 19 | , metavar "PATH" 20 | , value "./config.yaml" 21 | , showDefault 22 | ]) 23 | <*> option str (mconcat 24 | [ long "languages" 25 | , short 'l' 26 | , help "Set the languages directory" 27 | , metavar "DIR" 28 | , value "./languages/" 29 | , showDefault 30 | ]) 31 | 32 | main :: IO () 33 | main = do 34 | Args { configPath, languagesDir } <- parseArgs 35 | runMyriadServer configPath languagesDir 36 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | ./ 3 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | # Whether to build images concurrently. 2 | # This will take up more resources when building all the images for the first time. 3 | buildConcurrently: true 4 | 5 | # Whether to start containers on startup of myriad. 6 | prepareContainers: false 7 | 8 | # Interval in minutes to kill all running languages containers. 9 | cleanupInterval: 30 10 | 11 | # Port to run myriad on. 12 | port: 8081 13 | 14 | # The default language configuration. 15 | defaultLanguage: 16 | # The OCI runtime to use when running the container. 17 | runtime: runc 18 | 19 | # The maximum memory and swap usage (separately) of a container. 20 | memory: 256m 21 | 22 | # The number of CPUs to use. 23 | cpus: 0.25 24 | 25 | # Time in seconds for an evaluation before the container kills itself. 26 | timeout: 20 27 | 28 | # The maximum number of concurrent evaluations in the container. 29 | concurrent: 5 30 | 31 | # The maximum number of retries when the evaluation fails due to a non-timeout related reason. 32 | retries: 10 33 | 34 | # The maximum number of bytes that can be outputted. 35 | outputLimit: 4k 36 | 37 | # The languages to enable. 38 | # The fields available are the same as in 'defaultLanguage', plus the name of the language. 39 | # The names are as in your 'languages' folder. 40 | languages: 41 | - name: apl 42 | - name: bash 43 | - name: brainfuck 44 | - name: c 45 | - name: cpp 46 | - name: csharp 47 | - name: elixir 48 | - name: erlang 49 | - name: fsharp 50 | - name: go 51 | - name: haskell 52 | - name: idris 53 | - name: java 54 | - name: javascript 55 | - name: julia 56 | - name: lua 57 | - name: nim 58 | - name: ocaml 59 | - name: pascal 60 | - name: perl 61 | - name: php 62 | - name: prolog 63 | - name: python 64 | - name: r 65 | - name: racket 66 | - name: ruby 67 | - name: rust 68 | - name: typescript 69 | -------------------------------------------------------------------------------- /docker-compose.example.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | myriad: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.release 8 | image: myriad:latest 9 | container_name: myriad 10 | network_mode: bridge 11 | volumes: 12 | - /var/run/docker.sock:/var/run/docker.sock 13 | - ./config.yaml:/opt/myriad/config.yaml:ro 14 | ports: 15 | - 127.0.0.1:8081:8081/tcp 16 | - ::1:8081:8081/tcp 17 | restart: unless-stopped -------------------------------------------------------------------------------- /hie.yaml.cbl: -------------------------------------------------------------------------------- 1 | # For use with Haskell IDE Engine or Haskell Language Server using cabal. 2 | # Copy this file over to 'hie.yaml' to use. 3 | 4 | cradle: 5 | cabal: 6 | - path: "./src" 7 | component: "lib:myriad" 8 | - path: "./app" 9 | component: "myriad:exe:myriad" 10 | -------------------------------------------------------------------------------- /hie.yaml.stack: -------------------------------------------------------------------------------- 1 | # For use with Haskell IDE Engine or Haskell Language Server using stack. 2 | # Copy this file over to 'hie.yaml' to use. 3 | 4 | cradle: 5 | stack: 6 | - path: "./src" 7 | component: "myriad:lib" 8 | - path: "./app" 9 | component: "myriad:exe:myriad" 10 | -------------------------------------------------------------------------------- /languages/apl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM juergensauermann/gnu-apl 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/apl/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.apl 2 | apl --OFF -s -f program.apl 3 | -------------------------------------------------------------------------------- /languages/bash/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bash 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/bash/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.sh 2 | bash program.sh 3 | -------------------------------------------------------------------------------- /languages/brainfuck/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS build 2 | 3 | COPY bf.cpp . 4 | RUN apk add --no-cache g++ && \ 5 | g++ bf.cpp -o bf 6 | 7 | FROM alpine 8 | LABEL author="1Computer1" 9 | 10 | RUN apk add --no-cache libstdc++ 11 | COPY --from=build bf /usr/local/bin/ 12 | COPY run.sh /var/run/ 13 | -------------------------------------------------------------------------------- /languages/brainfuck/bf.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char **argv) { 7 | std::string ops; 8 | if (argc == 1) { 9 | std::string line; 10 | while (std::getline(std::cin, line)) { 11 | ops.append(line); 12 | } 13 | 14 | if (ops.empty()) { 15 | std::cerr << "No input given"; 16 | return 1; 17 | } 18 | } else { 19 | ops.assign(argv[1], strlen(argv[1])); 20 | } 21 | 22 | int len = ops.length(); 23 | std::vector tape = { 0 }; 24 | int oix = 0; 25 | int tix = 0; 26 | while (oix < len) { 27 | switch (ops[oix]) { 28 | case '>': 29 | tix++; 30 | if (tix >= tape.size()) { 31 | tape.push_back(0); 32 | } 33 | 34 | oix++; 35 | break; 36 | case '<': 37 | tix--; 38 | if (tix < 0) { 39 | std::cerr << "Out of bounds"; 40 | return 1; 41 | } 42 | 43 | oix++; 44 | break; 45 | case '+': 46 | tape[tix]++; 47 | oix++; 48 | break; 49 | case '-': 50 | tape[tix]--; 51 | oix++; 52 | break; 53 | case '.': 54 | std::cout << tape[tix]; 55 | oix++; 56 | break; 57 | case ',': 58 | std::cin >> tape[tix]; 59 | oix++; 60 | break; 61 | case '[': 62 | if (tape[tix] == 0) { 63 | int ls = 0; 64 | int rs = 0; 65 | for (int i = oix; i < len; i++) { 66 | switch (ops[i]) { 67 | case '[': 68 | ls++; 69 | break; 70 | case ']': 71 | rs++; 72 | break; 73 | default: 74 | break; 75 | } 76 | 77 | if (ls == rs) { 78 | oix = i + 1; 79 | break; 80 | } 81 | } 82 | } else { 83 | oix++; 84 | } 85 | 86 | break; 87 | case ']': 88 | if (tape[tix] != 0) { 89 | int ls = 0; 90 | int rs = 0; 91 | for (int i = oix; i >= 0; i--) { 92 | switch (ops[i]) { 93 | case '[': 94 | ls++; 95 | break; 96 | case ']': 97 | rs++; 98 | break; 99 | default: 100 | break; 101 | } 102 | 103 | if (ls == rs) { 104 | oix = i + 1; 105 | break; 106 | } 107 | } 108 | } else { 109 | oix++; 110 | } 111 | 112 | break; 113 | default: 114 | oix++; 115 | } 116 | } 117 | 118 | return 0; 119 | } 120 | -------------------------------------------------------------------------------- /languages/brainfuck/run.sh: -------------------------------------------------------------------------------- 1 | cat | bf 2 | -------------------------------------------------------------------------------- /languages/c/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | LABEL author="1Computer1" 3 | 4 | RUN apk add --no-cache gcc libc-dev 5 | 6 | COPY run.sh /var/run/ 7 | -------------------------------------------------------------------------------- /languages/c/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.c 2 | gcc program.c -o program && ./program 3 | -------------------------------------------------------------------------------- /languages/cpp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | LABEL author="1Computer1" 3 | 4 | RUN apk add --no-cache g++ 5 | 6 | COPY run.sh /var/run/ 7 | -------------------------------------------------------------------------------- /languages/cpp/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.cpp 2 | g++ program.cpp -o program && ./program 3 | -------------------------------------------------------------------------------- /languages/csharp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mono 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/csharp/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.cs 2 | csc -nologo program.cs 2>/dev/null && mono program.exe 3 | -------------------------------------------------------------------------------- /languages/elixir/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/elixir/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.exs 2 | elixir program.exs 3 | -------------------------------------------------------------------------------- /languages/erlang/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM erlang:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/erlang/run.sh: -------------------------------------------------------------------------------- 1 | echo "%% -*- erlang -*-" > program.erl 2 | cat >> program.erl 3 | escript program.erl 4 | -------------------------------------------------------------------------------- /languages/fsharp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fsharp 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/fsharp/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.fs 2 | fsharpc --optimize- program.fs >/dev/null && mono program.exe 3 | -------------------------------------------------------------------------------- /languages/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/go/run.sh: -------------------------------------------------------------------------------- 1 | export GOCACHE=/tmp/"$CODEDIR"/cache 2 | cat > program.go 3 | go run program.go 4 | -------------------------------------------------------------------------------- /languages/haskell/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | LABEL author="1Computer1" 3 | ENV LANG C.UTF-8 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends gnupg dirmngr ca-certificates && \ 7 | echo 'deb https://downloads.haskell.org/debian stretch main' > /etc/apt/sources.list.d/ghc.list && \ 8 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA3CBA3FFE22B574 && \ 9 | apt-get update && \ 10 | apt-get install -y --no-install-recommends ghc-9.0.1 && \ 11 | apt-get purge -y gnupg dirmngr ca-certificates && \ 12 | apt-get autoremove -y && \ 13 | apt-get autoclean -y 14 | 15 | ENV PATH /opt/ghc/9.0.1/bin:$PATH 16 | 17 | COPY run.sh /var/run/ 18 | -------------------------------------------------------------------------------- /languages/haskell/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.hs 2 | ghc -e main program.hs 3 | -------------------------------------------------------------------------------- /languages/idris/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ 4 | apk update && \ 5 | apk add idris@testing && \ 6 | rm -rf /var/cache/apk/* 7 | 8 | COPY run.sh /var/run/ 9 | -------------------------------------------------------------------------------- /languages/idris/run.sh: -------------------------------------------------------------------------------- 1 | cat > Main.idr 2 | idris --execute ./Main.idr 3 | -------------------------------------------------------------------------------- /languages/java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/java/run.sh: -------------------------------------------------------------------------------- 1 | cat > Main.java 2 | javac Main.java && java Main 3 | -------------------------------------------------------------------------------- /languages/javascript/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/javascript/run.sh: -------------------------------------------------------------------------------- 1 | cat | node 2 | -------------------------------------------------------------------------------- /languages/julia/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM julia 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/julia/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.jl 2 | julia program.jl 3 | -------------------------------------------------------------------------------- /languages/lua/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --no-cache lua5.3 4 | 5 | COPY run.sh /var/run/ 6 | -------------------------------------------------------------------------------- /languages/lua/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.lua 2 | lua5.3 program.lua 3 | -------------------------------------------------------------------------------- /languages/nim/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nimlang/nim:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/nim/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.nim 2 | nim compile --run --colors=off --memTracker=off --verbosity=0 --hints=off --warnings=off --nimcache:/tmp/"$CODEDIR"/cache ./program.nim 3 | -------------------------------------------------------------------------------- /languages/ocaml/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-ocaml 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/ocaml/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.ml 2 | ocamlopt -cclib --static -o program program.ml && ./program 3 | -------------------------------------------------------------------------------- /languages/pascal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-fpc 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/pascal/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.pas 2 | 3 | # fpc does not use stderr, ld however does, capture both 4 | res="$(fpc program.pas 2>&1)" 5 | 6 | if [ $? -eq 0 ]; then 7 | ./program 8 | else 9 | printf %s "$res" 10 | fi 11 | -------------------------------------------------------------------------------- /languages/perl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM perl:slim 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/perl/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.pl 2 | perl program.pl 3 | -------------------------------------------------------------------------------- /languages/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/php/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.php 2 | php program.php 3 | -------------------------------------------------------------------------------- /languages/prolog/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swipl 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/prolog/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.pl 2 | swipl --quiet program.pl 3 | -------------------------------------------------------------------------------- /languages/python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/python/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.py 2 | python program.py 3 | -------------------------------------------------------------------------------- /languages/r/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM r-base 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/r/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.R 2 | Rscript program.R 3 | -------------------------------------------------------------------------------- /languages/racket/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jackfirth/racket 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/racket/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.rkt 2 | racket program.rkt 3 | -------------------------------------------------------------------------------- /languages/ruby/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:alpine 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/ruby/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.rb 2 | ruby program.rb 3 | -------------------------------------------------------------------------------- /languages/rust/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim 2 | LABEL author="1Computer1" 3 | 4 | COPY run.sh /var/run/ 5 | -------------------------------------------------------------------------------- /languages/rust/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.rs 2 | rustc -C opt-level=0 --color never program.rs && ./program 3 | -------------------------------------------------------------------------------- /languages/typescript/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | LABEL author="1Computer1" 3 | 4 | RUN yarn global add typescript @types/node 5 | 6 | COPY run.sh /var/run/ 7 | -------------------------------------------------------------------------------- /languages/typescript/run.sh: -------------------------------------------------------------------------------- 1 | cat > program.ts 2 | tsc --lib DOM,ESNext --target ES2019 --strict \ 3 | --skipLibCheck --types /usr/local/share/.config/yarn/global/node_modules/@types/node program.ts \ 4 | && cat program.js | node 5 | -------------------------------------------------------------------------------- /myriad.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | 3 | name: myriad 4 | version: 0.5.0.3 5 | synopsis: Arbitrary code execution in Docker. 6 | description: Please see the README on GitHub at 7 | category: Server 8 | homepage: https://github.com/1Computer1/myriad#readme 9 | bug-reports: https://github.com/1Computer1/myriad/issues 10 | author: 1Computer 11 | maintainer: onecomputer00@gmail.com 12 | copyright: 2020 1Computer 13 | license: MIT 14 | license-file: LICENSE 15 | build-type: Simple 16 | extra-source-files: 17 | README.md 18 | 19 | source-repository head 20 | type: git 21 | location: https://github.com/1Computer1/myriad 22 | 23 | common shared 24 | default-language: Haskell2010 25 | default-extensions: 26 | ConstraintKinds 27 | DataKinds 28 | DeriveAnyClass 29 | DeriveGeneric 30 | DerivingStrategies 31 | FlexibleContexts 32 | FlexibleInstances 33 | FunctionalDependencies 34 | GeneralizedNewtypeDeriving 35 | LambdaCase 36 | MultiParamTypeClasses 37 | MultiWayIf 38 | NamedFieldPuns 39 | OverloadedLabels 40 | OverloadedStrings 41 | PatternSynonyms 42 | TupleSections 43 | TypeApplications 44 | TypeFamilies 45 | TypeOperators 46 | build-depends: 47 | aeson 48 | , async 49 | , base >= 4.12 && < 5 50 | , bytestring 51 | , containers 52 | , filepath 53 | , lifted-async 54 | , lifted-base 55 | , monad-control 56 | , monad-logger 57 | , mtl 58 | , optics 59 | , servant >= 0.17 60 | , servant-server >= 0.17 61 | , snowflake 62 | , string-conversions 63 | , text 64 | , time 65 | , transformers 66 | , transformers-base 67 | , typed-process 68 | , wai 69 | , warp 70 | , yaml 71 | 72 | library 73 | import: shared 74 | exposed-modules: 75 | Myriad 76 | Myriad.Config 77 | Myriad.Core 78 | Myriad.Docker 79 | Myriad.Server 80 | other-modules: 81 | Paths_myriad 82 | autogen-modules: 83 | Paths_myriad 84 | hs-source-dirs: 85 | src 86 | ghc-options: -Wall 87 | 88 | executable myriad 89 | import: shared 90 | main-is: Main.hs 91 | other-modules: 92 | Paths_myriad 93 | autogen-modules: 94 | Paths_myriad 95 | hs-source-dirs: 96 | app 97 | ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall 98 | build-depends: 99 | myriad 100 | , optparse-applicative 101 | -------------------------------------------------------------------------------- /src/Myriad.hs: -------------------------------------------------------------------------------- 1 | module Myriad 2 | ( runMyriadServer 3 | ) where 4 | 5 | import Control.Monad.Logger (runStdoutLoggingT) 6 | 7 | import Data.String.Conversions 8 | 9 | import Network.Wai.Handler.Warp 10 | 11 | import Optics 12 | 13 | import Myriad.Core 14 | import Myriad.Docker 15 | import Myriad.Server 16 | 17 | runMyriadServer :: FilePath -> FilePath -> IO () 18 | runMyriadServer configPath languagesDir = do 19 | env <- initEnv configPath languagesDir 20 | runMyriadT env $ do 21 | buildAllImages 22 | startCleanup 23 | logInfo ["Finished Docker-related setup"] 24 | let myriadPort = fromIntegral $ env ^. #config % #port 25 | onReady = runStdoutLoggingT $ logInfo ["Server started on http://localhost:", cs $ show myriadPort] 26 | settings = setPort myriadPort . setBeforeMainLoop onReady $ defaultSettings 27 | runSettings settings $ app env 28 | -------------------------------------------------------------------------------- /src/Myriad/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DuplicateRecordFields #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE UndecidableInstances #-} 4 | 5 | module Myriad.Config 6 | ( LanguageName 7 | , Config(..) 8 | , Language(..) 9 | , readConfig 10 | ) where 11 | 12 | import Data.Aeson 13 | import qualified Data.ByteString as B 14 | import Data.Maybe 15 | import qualified Data.Text as T 16 | import Data.Yaml 17 | 18 | import Optics 19 | 20 | type LanguageName = T.Text 21 | 22 | data Language = Language 23 | { _name :: LanguageName 24 | , _runtime :: T.Text 25 | , _memory :: T.Text 26 | , _cpus :: Double 27 | , _timeout :: Int 28 | , _concurrent :: Int 29 | , _retries :: Int 30 | , _outputLimit :: T.Text 31 | } deriving (Show) 32 | 33 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''Language 34 | 35 | data Config = Config 36 | { _languages :: [Language] 37 | , _buildConcurrently :: Bool 38 | , _prepareContainers :: Bool 39 | , _cleanupInterval :: Int 40 | , _port :: Int 41 | } deriving (Show) 42 | 43 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''Config 44 | 45 | data DefaultLanguage = DefaultLanguage 46 | { _runtime :: T.Text 47 | , _memory :: T.Text 48 | , _cpus :: Double 49 | , _timeout :: Int 50 | , _concurrent :: Int 51 | , _retries :: Int 52 | , _outputLimit :: T.Text 53 | } deriving (Show) 54 | 55 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''DefaultLanguage 56 | 57 | instance FromJSON DefaultLanguage where 58 | parseJSON = withObject "default language" $ \m -> DefaultLanguage 59 | <$> m .: "runtime" 60 | <*> m .: "memory" 61 | <*> m .: "cpus" 62 | <*> m .: "timeout" 63 | <*> m .: "concurrent" 64 | <*> m .: "retries" 65 | <*> m .: "outputLimit" 66 | 67 | data RawLanguage = RawLanguage 68 | { _name :: LanguageName 69 | , _runtime :: Maybe T.Text 70 | , _memory :: Maybe T.Text 71 | , _cpus :: Maybe Double 72 | , _timeout :: Maybe Int 73 | , _concurrent :: Maybe Int 74 | , _retries :: Maybe Int 75 | , _outputLimit :: Maybe T.Text 76 | } deriving (Show) 77 | 78 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''RawLanguage 79 | 80 | instance FromJSON RawLanguage where 81 | parseJSON = withObject "language" $ \m -> RawLanguage 82 | <$> m .: "name" 83 | <*> m .:? "runtime" 84 | <*> m .:? "memory" 85 | <*> m .:? "cpus" 86 | <*> m .:? "timeout" 87 | <*> m .:? "concurrent" 88 | <*> m .:? "retries" 89 | <*> m .:? "outputLimit" 90 | 91 | data RawConfig = RawConfig 92 | { _languages :: [RawLanguage] 93 | , _defaultLanguage :: DefaultLanguage 94 | , _buildConcurrently :: Bool 95 | , _prepareContainers :: Bool 96 | , _cleanupInterval :: Int 97 | , _port :: Int 98 | } deriving (Show) 99 | 100 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''RawConfig 101 | 102 | instance FromJSON RawConfig where 103 | parseJSON = withObject "config" $ \m -> RawConfig 104 | <$> m .: "languages" 105 | <*> m .: "defaultLanguage" 106 | <*> m .: "buildConcurrently" 107 | <*> m .: "prepareContainers" 108 | <*> m .: "cleanupInterval" 109 | <*> m .: "port" 110 | 111 | readConfig :: FilePath -> IO Config 112 | readConfig = fmap fromRawConfig . readRawConfig 113 | 114 | readRawConfig :: FilePath -> IO RawConfig 115 | readRawConfig f = do 116 | x <- B.readFile f 117 | case Data.Yaml.decodeEither' x of 118 | Left e -> error $ prettyPrintParseException e 119 | Right y -> pure y 120 | 121 | fromRawConfig :: RawConfig -> Config 122 | fromRawConfig r = 123 | Config 124 | { _languages = map (fromRawLanguage (r ^. #defaultLanguage)) $ r ^. #languages 125 | , _buildConcurrently = r ^. #buildConcurrently 126 | , _prepareContainers = r ^. #prepareContainers 127 | , _cleanupInterval = r ^. #cleanupInterval 128 | , _port = r ^. #port 129 | } 130 | 131 | fromRawLanguage :: DefaultLanguage -> RawLanguage -> Language 132 | fromRawLanguage d r = 133 | Language 134 | { _name = r ^. #name 135 | , _runtime = fromMaybe (d ^. #runtime) (r ^. #runtime) 136 | , _memory = fromMaybe (d ^. #memory) (r ^. #memory) 137 | , _cpus = fromMaybe (d ^. #cpus) (r ^. #cpus) 138 | , _timeout = fromMaybe (d ^. #timeout) (r ^. #timeout) 139 | , _concurrent = fromMaybe (d ^. #concurrent) (r ^. #concurrent) 140 | , _retries = fromMaybe (d ^. #retries) (r ^. #retries) 141 | , _outputLimit = fromMaybe (d ^. #outputLimit) (r ^. #outputLimit) 142 | } 143 | -------------------------------------------------------------------------------- /src/Myriad/Core.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | {-# LANGUAGE UndecidableInstances #-} 3 | 4 | module Myriad.Core 5 | ( Language 6 | , ContainerName 7 | , ImageName 8 | , Env(..) 9 | , MyriadT 10 | , runMyriadT 11 | , initEnv 12 | , exec 13 | , exec_ 14 | , logInfo 15 | , logDebug 16 | , logWarn 17 | , logError 18 | , mapMVar 19 | , writeMVar 20 | ) where 21 | 22 | import Control.Monad.Base 23 | import Control.Monad.Except 24 | import Control.Monad.Logger hiding (logError, logDebug, logWarn, logInfo) 25 | import Control.Monad.Reader 26 | import Control.Monad.State 27 | import Control.Monad.Trans.Control 28 | import Control.Monad.Writer 29 | 30 | import qualified Data.ByteString.Lazy as BL 31 | import qualified Data.Map.Strict as M 32 | import Data.Snowflake 33 | import Data.String.Conversions 34 | import qualified Data.Text as T 35 | 36 | import Control.Concurrent.MVar.Lifted 37 | import Control.Concurrent.QSem.Lifted 38 | import System.Process.Typed 39 | 40 | import Optics 41 | 42 | import Myriad.Config 43 | 44 | type ContainerName = String 45 | 46 | type ImageName = String 47 | 48 | data Env = Env 49 | { _config :: Config 50 | , _languagesDir :: FilePath 51 | , _containers :: MVar (M.Map LanguageName ContainerName) 52 | , _containerSems :: MVar (M.Map LanguageName QSem) 53 | , _evalSems :: MVar (M.Map LanguageName QSem) 54 | , _snowflakeGen :: SnowflakeGen 55 | } 56 | 57 | makeFieldLabelsWith classUnderscoreNoPrefixFields ''Env 58 | 59 | newtype MyriadT m a = MyriadT { unMyriadT :: ReaderT Env (LoggingT m) a } 60 | deriving newtype 61 | ( Functor 62 | , Applicative 63 | , Monad 64 | , MonadReader Env 65 | , MonadLogger 66 | , MonadLoggerIO 67 | , MonadIO 68 | , MonadError e 69 | , MonadState s 70 | , MonadWriter w 71 | , MonadBase b 72 | ) 73 | 74 | instance MonadTrans MyriadT where 75 | lift = MyriadT . lift . lift 76 | 77 | instance MonadTransControl MyriadT where 78 | type StT MyriadT a = a 79 | liftWith = defaultLiftWith2 MyriadT unMyriadT 80 | restoreT = defaultRestoreT2 MyriadT 81 | 82 | instance MonadBaseControl b m => MonadBaseControl b (MyriadT m) where 83 | type StM (MyriadT m) a = ComposeSt MyriadT m a 84 | liftBaseWith = defaultLiftBaseWith 85 | restoreM = defaultRestoreM 86 | 87 | initEnv :: FilePath -> FilePath -> IO Env 88 | initEnv configPath languagesDir = 89 | Env 90 | <$> readConfig configPath 91 | <*> pure languagesDir 92 | <*> newMVar M.empty 93 | <*> newMVar M.empty 94 | <*> newMVar M.empty 95 | <*> newSnowflakeGen defaultConfig 0 96 | 97 | runMyriadT :: MonadIO m => Env -> MyriadT m a -> m a 98 | runMyriadT env = runStdoutLoggingT . flip runReaderT env . unMyriadT 99 | 100 | exec :: (MonadIO m, MonadLogger m) => [String] -> m BL.ByteString 101 | exec args = do 102 | logDebug ["Executing `", cs $ mconcat args, "`"] 103 | readProcessInterleaved_ . shell $ mconcat args 104 | 105 | exec_ :: (MonadIO m, MonadLogger m) => [String] -> m () 106 | exec_ = void . exec 107 | 108 | logInfo :: MonadLogger m => [T.Text] -> m () 109 | logInfo = logInfoN . mconcat 110 | 111 | logDebug :: MonadLogger m => [T.Text] -> m () 112 | logDebug = logDebugN . mconcat 113 | 114 | logWarn :: MonadLogger m => [T.Text] -> m () 115 | logWarn = logWarnN . mconcat 116 | 117 | logError :: MonadLogger m => [T.Text] -> m () 118 | logError = logErrorN . mconcat 119 | 120 | mapMVar :: (MonadBase IO m, MonadBaseControl IO m) => MVar a -> (a -> a) -> m () 121 | mapMVar var f = modifyMVar_ var (pure . f) 122 | 123 | writeMVar :: (MonadBase IO m, MonadBaseControl IO m) => MVar a -> a -> m () 124 | writeMVar var x = mapMVar var $ const x 125 | -------------------------------------------------------------------------------- /src/Myriad/Docker.hs: -------------------------------------------------------------------------------- 1 | module Myriad.Docker 2 | ( EvalResult(..) 3 | , buildImage 4 | , buildAllImages 5 | , startCleanup 6 | , setupContainer 7 | , killContainer 8 | , killContainers 9 | , evalCode 10 | ) where 11 | 12 | import Control.Monad.Reader 13 | 14 | import qualified Data.ByteString.Lazy as BL 15 | import qualified Data.Map.Strict as M 16 | import Data.Snowflake 17 | import Data.String.Conversions 18 | 19 | import Control.Concurrent.Async.Lifted 20 | import Control.Concurrent.Lifted (fork, threadDelay) 21 | import Control.Concurrent.MVar.Lifted 22 | import Control.Concurrent.QSem.Lifted 23 | import Control.Exception.Lifted 24 | import System.FilePath (()) 25 | import System.Process.Typed 26 | 27 | import Optics 28 | 29 | import Myriad.Config 30 | import Myriad.Core 31 | 32 | type Myriad = MyriadT IO 33 | 34 | data EvalResult 35 | = EvalOk BL.ByteString 36 | | EvalTimedOut 37 | | EvalErrored 38 | deriving (Show) 39 | 40 | buildImage :: Language -> Myriad () 41 | buildImage lang = do 42 | env <- ask 43 | logInfo ["Checking for image ", cs $ imageName lang] 44 | res <- try $ exec ["docker images -q ", imageName lang] 45 | case res of 46 | Left (SomeException err) -> logError ["An exception occured when checking for image ", cs $ imageName lang, ":\n", cs $ show err] 47 | Right s -> do 48 | when (BL.null s) . void $ do -- If string is empty that means the image does not yet exist 49 | logInfo ["Building image ", cs $ imageName lang] 50 | exec_ ["docker build -t ", imageName lang, " ", cs (env ^. #languagesDir) cs (lang ^. #name)] 51 | logInfo ["Built image ", cs $ imageName lang] 52 | setupQSems 53 | when (env ^. #config % #prepareContainers) . void $ setupContainer lang 54 | where 55 | setupQSems :: Myriad () 56 | setupQSems = do 57 | env <- ask 58 | csem <- newQSem 1 -- We only want one container to be set up at a time 59 | esem <- newQSem $ fromIntegral (lang ^. #concurrent) 60 | mapMVar (env ^. #containerSems) $ M.insert (lang ^. #name) csem 61 | mapMVar (env ^. #evalSems) $ M.insert (lang ^. #name) esem 62 | 63 | buildAllImages :: Myriad () 64 | buildAllImages = do 65 | config <- gview #config 66 | if config ^. #buildConcurrently 67 | then do 68 | logInfo ["Building all images concurrently"] 69 | forConcurrently_ (config ^. #languages) buildImage 70 | else do 71 | logInfo ["Building all images sequentially"] 72 | forM_ (config ^. #languages) buildImage 73 | 74 | startCleanup :: Myriad () 75 | startCleanup = do 76 | config <- gview #config 77 | when (config ^. #cleanupInterval > 0) . void $ do 78 | let t = fromIntegral (config ^. #cleanupInterval) 79 | fork $ timer t 80 | where 81 | -- Given time in minutes 82 | timer :: Int -> Myriad () 83 | timer t = forever $ do 84 | -- Takes time in microseconds 85 | threadDelay $ t * 60000000 86 | logInfo ["Starting cleanup of containers"] 87 | n <- killContainers 88 | logInfo ["Cleaned up ", cs $ show n, " containers, next in ", cs $ show t, " minutes"] 89 | timer t 90 | 91 | setupContainer :: Language -> Myriad ContainerName 92 | setupContainer lang = do 93 | cnts <- gview #containers >>= readMVar 94 | case cnts M.!? (lang ^. #name) of 95 | Nothing -> setup 96 | Just cnt -> pure cnt 97 | where 98 | setup :: Myriad ContainerName 99 | setup = do 100 | ref <- gview #containers 101 | cnt <- newContainerName lang 102 | logInfo ["Setting up new container ", cs cnt] 103 | exec_ 104 | [ "docker run --runtime=" 105 | , cs $ lang ^. #runtime 106 | , " --rm --name=" 107 | , cs cnt 108 | -- User 1000 will be for setting up the environment 109 | , " -u1000:1000 -w/tmp/ -dt --net=none --cpus=" 110 | , show $ lang ^. #cpus 111 | , " -m=" 112 | , cs $ lang ^. #memory 113 | , " --memory-swap=" 114 | , cs $ lang ^. #memory 115 | , " " 116 | , imageName lang 117 | , " /bin/sh" 118 | ] 119 | -- The `eval` directory is where all the eval work is done 120 | -- 711 so that users can't traverse into other people's code 121 | exec_ ["docker exec ", cnt, " mkdir eval"] 122 | exec_ ["docker exec ", cnt, " chmod 711 eval"] 123 | mapMVar ref $ M.insert (lang ^. #name) cnt 124 | logInfo ["Started new container ", cs cnt] 125 | pure cnt 126 | 127 | killContainer :: LanguageName -> Myriad Bool 128 | killContainer lang = do 129 | containers <- gview #containers >>= readMVar 130 | case containers M.!? lang of 131 | Nothing -> pure False 132 | Just cnt -> do 133 | logInfo ["Killing container ", cs cnt] 134 | res <- kill cnt 135 | case res of 136 | Nothing -> do 137 | logInfo ["Killed container ", cs cnt] 138 | pure True 139 | Just err -> do 140 | logError ["An exception occured when killing ", cs cnt, ":\n", cs $ show err] 141 | pure False 142 | where 143 | kill :: ContainerName -> Myriad (Maybe SomeException) 144 | kill cnt = do 145 | ref <- gview #containers 146 | mapMVar ref $ M.delete lang 147 | res <- try $ exec_ ["docker kill ", cnt] 148 | case res of 149 | Left err -> pure $ Just err 150 | Right _ -> pure Nothing 151 | 152 | killContainers :: Myriad [ContainerName] 153 | killContainers = do 154 | containers <- gview #containers >>= readMVar 155 | logInfo ["Starting killing of containers"] 156 | xs <- forConcurrently (M.toList containers) $ \(k, v) -> (v,) <$> killContainer k 157 | logInfo ["Finished killing of containers"] 158 | pure . map fst $ filter snd xs 159 | 160 | evalCode :: Language -> Int -> String -> Myriad EvalResult 161 | evalCode lang numRetries code = withContainer $ \cnt -> do 162 | doneRef <- newMVar False -- For keeping track of if the evaluation is done, i.e. succeeded or timed out. 163 | void . fork $ timer cnt doneRef -- `race` could not have been used here since some evals can't be cancelled. 164 | snowflakeGen <- gview #snowflakeGen 165 | snowflake <- liftIO $ nextSnowflake snowflakeGen 166 | res <- try $ eval cnt snowflake 167 | case res of 168 | Left (SomeException err) -> do 169 | void . killContainer $ lang ^. #name 170 | done <- readMVar doneRef 171 | if done 172 | -- If we find the eval is done from an exception, then it was timed out. 173 | then do 174 | logInfo ["Code timed out in container ", cs cnt, ", evaluation ", cs $ show snowflake] 175 | pure EvalTimedOut 176 | -- Otherwise, the container was killed from another eval, so we should retry. 177 | else do 178 | writeMVar doneRef True 179 | if numRetries < fromIntegral (lang ^. #retries) 180 | then do 181 | logInfo 182 | [ "An exception occured in " 183 | , cs cnt 184 | , ", evaluation " 185 | , cs $ show snowflake 186 | , "retrying:\n" 187 | , cs $ show err 188 | ] 189 | evalCode lang (numRetries + 1) code 190 | else do 191 | logInfo 192 | [ "An exception occured in " 193 | , cs cnt 194 | , ", evaluation " 195 | , cs $ show snowflake 196 | , ":\n" 197 | , cs $ show err 198 | ] 199 | pure EvalErrored 200 | Right x -> do 201 | writeMVar doneRef True 202 | pure x 203 | where 204 | withContainer :: (ContainerName -> Myriad a) -> Myriad a 205 | withContainer f = do 206 | env <- ask 207 | csem <- (M.! (lang ^. #name)) <$> readMVar (env ^. #containerSems) 208 | esem <- (M.! (lang ^. #name)) <$> readMVar (env ^. #evalSems) 209 | bracket_ (waitQSem esem) (signalQSem esem) $ do 210 | cnt <- bracket_ (waitQSem csem) (signalQSem csem) $ setupContainer lang 211 | f cnt 212 | 213 | timer :: ContainerName -> MVar Bool -> Myriad () 214 | timer cnt doneRef = do 215 | -- Given time in seconds 216 | let t = fromIntegral $ lang ^. #timeout 217 | logDebug ["Starting timeout of ", cs . show $ t, " seconds for container ", cs cnt] 218 | -- Takes time in microseconds 219 | threadDelay $ t * 1000000 220 | done <- readMVar doneRef 221 | if done 222 | then do 223 | logDebug ["Finished timeout for container ", cs cnt, ", but container already done"] 224 | else do 225 | logDebug ["Finished timeout for container ", cs cnt, " and killing it"] 226 | writeMVar doneRef True 227 | void . killContainer $ lang ^. #name 228 | 229 | eval :: ContainerName -> Snowflake -> Myriad EvalResult 230 | eval cnt snowflake = do 231 | logInfo ["Running code in container ", cs cnt, ", evaluation ", cs $ show snowflake, ":\n", cs code] 232 | exec_ ["docker exec ", cs cnt, " mkdir eval/", show snowflake] 233 | exec_ ["docker exec ", cs cnt, " chmod 777 eval/", show snowflake] 234 | -- User 1001 will be used for the actual execution so that they can't access `eval` itself 235 | let limit = lang ^. #outputLimit 236 | cmd = mconcat 237 | [ "docker exec -i -u1001:1001 -w/tmp/eval/" 238 | , show snowflake 239 | , " " 240 | , cnt 241 | , " /bin/sh /var/run/run.sh 2>&1 | head -c " 242 | , cs limit 243 | ] 244 | pr = setStdin (byteStringInput $ cs code) $ shell cmd 245 | logDebug ["Executing with stdin `", cs cmd, "`"] 246 | output <- readProcessInterleaved_ pr 247 | exec_ ["docker exec ", cnt, " rm -rf eval/", show snowflake] 248 | logInfo ["Ran code in container ", cs cnt, ", evaluation ", cs $ show snowflake] 249 | pure $ EvalOk output 250 | 251 | newContainerName :: Language -> Myriad ContainerName 252 | newContainerName lang = do 253 | snowflakeGen <- gview #snowflakeGen 254 | snowflake <- liftIO $ nextSnowflake snowflakeGen 255 | pure $ "myriad-" <> cs (lang ^. #name) <> "-" <> show snowflake 256 | 257 | imageName :: Language -> ImageName 258 | imageName lang = "1computer1/myriad:" <> cs (lang ^. #name) 259 | -------------------------------------------------------------------------------- /src/Myriad/Server.hs: -------------------------------------------------------------------------------- 1 | module Myriad.Server 2 | ( app 3 | ) where 4 | 5 | import Control.Monad.Except 6 | import Control.Monad.Reader 7 | 8 | import Data.Aeson 9 | import Data.List (find) 10 | import qualified Data.Map as M 11 | import Data.String.Conversions 12 | import qualified Data.Text as T 13 | import GHC.Generics 14 | 15 | import Control.Concurrent.Async.Lifted 16 | import Control.Concurrent.MVar.Lifted 17 | import Servant 18 | 19 | import Optics 20 | 21 | import Myriad.Core 22 | import Myriad.Docker 23 | 24 | type Myriad = MyriadT Handler 25 | 26 | data EvalRequest = EvalRequest 27 | { language :: T.Text 28 | , code :: String 29 | } deriving (Generic, FromJSON) 30 | 31 | data EvalResponse = EvalResponse 32 | { result :: T.Text 33 | } deriving (Generic, ToJSON) 34 | 35 | type API 36 | = "languages" :> Get '[JSON] [T.Text] 37 | :<|> "eval" :> ReqBody '[JSON] EvalRequest :> Post '[JSON] EvalResponse 38 | :<|> "containers" :> Get '[JSON] [T.Text] 39 | :<|> "cleanup" :> Post '[JSON] [T.Text] 40 | 41 | app :: Env -> Application 42 | app = serve (Proxy @API) . server 43 | 44 | server :: Env -> Server API 45 | server env = hoistServer (Proxy @API) (runMyriadT env) serverT 46 | 47 | serverT :: ServerT API Myriad 48 | serverT = handleLanguages :<|> handleEval :<|> handleContainers :<|> handleCleanup 49 | where 50 | handleLanguages :: Myriad [T.Text] 51 | handleLanguages = do 52 | logInfo ["GET /languages"] 53 | languages <- gview $ #config % #languages 54 | pure $ map (^. #name) languages 55 | 56 | handleEval :: EvalRequest -> Myriad EvalResponse 57 | handleEval EvalRequest { language, code } = do 58 | logInfo ["POST /eval"] 59 | languages <- gview $ #config % #languages 60 | case find (\x -> x ^. #name == language) languages of 61 | Nothing -> do 62 | logDebug ["Language ", cs language , " was not found (404)"] 63 | throwError $ err404 { errBody = "Language " <> cs language <> " was not found" } 64 | Just cfg -> do 65 | env <- ask 66 | res <- withAsync (liftIO . runMyriadT env . evalCode cfg 0 $ cs code) wait 67 | case res of 68 | EvalErrored -> do 69 | logDebug ["Evaluation failed (500)"] 70 | throwError $ err500 { errBody = "Evaluation failed" } 71 | EvalTimedOut -> do 72 | logDebug ["Evaluation timed out (504)"] 73 | throwError $ err504 { errBody = "Evaluation timed out" } 74 | EvalOk xs -> pure . EvalResponse $ cs xs 75 | 76 | handleContainers :: Myriad [T.Text] 77 | handleContainers = do 78 | logInfo ["GET /containers"] 79 | containers <- gview #containers >>= readMVar 80 | pure . map cs $ M.elems containers 81 | 82 | handleCleanup :: Myriad [T.Text] 83 | handleCleanup = do 84 | logInfo ["POST /cleanup"] 85 | env <- ask 86 | liftIO $ map cs <$> runMyriadT env killContainers 87 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-16.2 2 | 3 | packages: 4 | - . 5 | 6 | extra-deps: 7 | - optics-0.3 8 | - optics-core-0.3 9 | - optics-extra-0.3 10 | - optics-th-0.3 11 | - servant-0.17 12 | - servant-server-0.17 13 | --------------------------------------------------------------------------------