├── nim.cfg ├── .gitignore ├── docker ├── ignored ├── updateimages.sh ├── Dockerfile_nopackages ├── Dockerfile └── packages.nimble ├── conf.json ├── setup.sh ├── nim_playground.nimble ├── docker_timeout.sh ├── test └── script.sh ├── README.md └── src └── nim_playground.nim /nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | -d:ssl 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | nimcache 3 | nim_playground -------------------------------------------------------------------------------- /docker/ignored: -------------------------------------------------------------------------------- 1 | v0.10.2 2 | v0.11.0 3 | v0.11.2 4 | v0.12.0 5 | v0.15.2 6 | v0.9.0 7 | v0.9.4 8 | v0.9.6 9 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "log_fname": "/var/log/nim_playground.log", 3 | "tmp_dir": "/tmp/nim_playground" 4 | } 5 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install Docker 4 | sudo apt update 5 | sudo apt install docker.io 6 | sudo systemctl start docker 7 | 8 | # Create Docker image 9 | echo "Creating Docker Image" 10 | docker build -t 'virtual_machine' - < Dockerfile 11 | echo "Retrieving Installed Docker Images" 12 | docker images 13 | -------------------------------------------------------------------------------- /nim_playground.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Zachary Carter" 5 | description = "API for play.nim-lang.org" 6 | license = "MIT" 7 | 8 | srcdir = "src" 9 | bin = @["nim_playground"] 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 0.16.1" 14 | requires "jester >= 0.1.1" 15 | requires "nuuid >= 0.1.0" 16 | requires "ansitohtml >= 0.1.0" 17 | -------------------------------------------------------------------------------- /docker_timeout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | to=$1 5 | shift 6 | 7 | cont=$(docker run --memory=200m --cpus=".8" -d "$@") 8 | code=$(timeout "$to" docker wait "$cont" || true) 9 | docker kill $cont &> /dev/null 10 | echo -n 'status: ' 11 | if [ -z "$code" ]; then 12 | echo timeout 13 | else 14 | echo exited: $code 15 | fi 16 | 17 | echo output: 18 | # pipe to sed simply for pretty nice indentation 19 | docker logs $cont | sed 's/^/\t/' 20 | 21 | docker rm $cont &> /dev/null 22 | -------------------------------------------------------------------------------- /test/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | file=$1 4 | compilationTarget=$2 5 | 6 | #exec 1> $"/usercode/logfile.txt" 7 | #exec 2> $"/usercode/errors.txt" 8 | exec < /dev/null 9 | 10 | chmod 777 /usercode/logfile.txt 11 | chmod 777 /usercode/errors.txt 12 | 13 | nim $compilationTarget --colors:on --NimblePath:/playground/nimble --nimcache:/usercode/nimcache /usercode/$file &> /usercode/errors.txt 14 | if [ $? -eq 0 ]; then 15 | /usercode/${file/.nim/""} &> /usercode/logfile.txt 16 | else 17 | echo "" &> /usercode/logfile.txt 18 | fi 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nim-playground 2 | 3 | This is the back-end for the [Nim playground](https://play.nim-lang.org). The front-end can be found [here](https://github.com/PMunch/nim-playground-frontend). All code that is executed by the playground is run within a container. This container always run the latest release of Nim, and comes with a series of Nimble packages installed. The list of packages can be found in [this list](https://github.com/PMunch/nim-playground/blob/master/docker/packages.nimble), and if you want to add or remove one simply make a PR to this repo that changes this file. 4 | -------------------------------------------------------------------------------- /docker/updateimages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | missing=$(uniq -u <(sort <(cat ignored <(git ls-remote --tags git://github.com/nim-lang/nim.git | sed -n 's/.*refs\/tags\/\(.*\)^{}/\1/p') <(docker images | sed -n 's/virtual_machine *\(v[^ ]*\).*/\1/p')))) 4 | latest=$(git ls-remote --tags git://github.com/nim-lang/nim.git | sed -n 's/.*refs\/tags\/\(.*\)^{}/\1/p' | sed 's/v/0./' | sort -t. -n -k1,1 -k2,2 -k3,3 -k4,4 | sed 's/0./v/' | tail -n1) 5 | 6 | while read -r line; do 7 | if [ ! -z "$line" ]; then 8 | echo $line > curtag 9 | cat curtag 10 | if [ "$line" == "$latest" ]; then 11 | docker build --no-cache -t "virtual_machine:$line" . 12 | else 13 | docker build --no-cache -t "virtual_machine:$line" -f Dockerfile_nopackages . 14 | fi 15 | fi 16 | done <<< $missing 17 | 18 | rm curtag 19 | 20 | docker tag virtual_machine:$(docker images | sed -n 's/virtual_machine *\(v[^ ]*\).*/\1/p' | sort --version-sort | tail -n1) virtual_machine:latest 21 | -------------------------------------------------------------------------------- /docker/Dockerfile_nopackages: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 as builder 2 | 3 | RUN apk add git gcc musl-dev 4 | 5 | WORKDIR /build 6 | 7 | RUN git clone https://github.com/nim-lang/Nim.git 8 | 9 | COPY curtag Nim 10 | 11 | RUN cd Nim && git checkout $(cat curtag) 12 | 13 | WORKDIR /build/Nim 14 | 15 | RUN git clone https://github.com/nim-lang/csources.git 16 | 17 | WORKDIR /build/Nim/csources 18 | 19 | COPY curtag . 20 | 21 | RUN git checkout $(cat curtag) || git checkout $(echo $(git tag) $(cat curtag) | sed 's/ /\n/g' | sed 's/v/0./' | sort -t. -n -k1,1 -k2,2 -k3,3 -k4,4 | sed 's/0./v/' | sed -n "/$(cat curtag)/q;p" | tail -n1) 22 | 23 | RUN sh build.sh 24 | 25 | WORKDIR /build/Nim 26 | 27 | RUN bin/nim c --skipUserCfg --skipParentCfg koch 28 | 29 | RUN ./koch boot -d:release 30 | 31 | RUN mkdir /build/result 32 | 33 | RUN ./koch install /build/result 34 | 35 | ################################## 36 | 37 | FROM alpine:3.7 as playground 38 | 39 | RUN apk add gcc g++ musl-dev 40 | 41 | RUN apk add pcre # Requirement for regex 42 | 43 | WORKDIR /playground 44 | 45 | COPY --from=builder /build/result/* ./nim/ 46 | 47 | ENV PATH=$PATH:/playground/nim/bin 48 | 49 | RUN mkdir /usercode && chown nobody:nobody /usercode 50 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 as builder 2 | 3 | RUN apk add git gcc musl-dev 4 | 5 | WORKDIR /build 6 | 7 | RUN git clone https://github.com/nim-lang/Nim.git 8 | 9 | COPY curtag Nim 10 | 11 | RUN cd Nim && git checkout $(cat curtag) 12 | 13 | WORKDIR /build/Nim 14 | 15 | RUN git clone https://github.com/nim-lang/csources.git 16 | 17 | WORKDIR /build/Nim/csources 18 | 19 | COPY curtag . 20 | 21 | RUN git checkout $(cat curtag) || git checkout $(echo $(git tag) $(cat curtag) | sed 's/ /\n/g' | sed 's/v/0./' | sort -t. -n -k1,1 -k2,2 -k3,3 -k4,4 | sed 's/0./v/' | sed -n "/$(cat curtag)/q;p" | tail -n1) 22 | 23 | RUN sh build.sh 24 | 25 | WORKDIR /build/Nim 26 | 27 | RUN bin/nim c --skipUserCfg --skipParentCfg koch 28 | 29 | RUN ./koch boot -d:release 30 | 31 | RUN mkdir /build/result 32 | 33 | RUN ./koch install /build/result 34 | 35 | RUN ./koch nimble 36 | 37 | ################################## 38 | 39 | FROM alpine:3.7 as installer 40 | 41 | RUN apk add git gcc musl-dev libressl-dev 42 | 43 | WORKDIR /installer 44 | 45 | COPY --from=builder /build/result/* ./nim/ 46 | 47 | COPY --from=builder /build/Nim/bin/nimble ./nim/bin 48 | 49 | ENV PATH=$PATH:/installer/nim/bin 50 | 51 | COPY packages.nimble ./ 52 | 53 | RUN nimble install --nimbleDir:/installer/nimble -y --depsOnly 54 | 55 | ################################## 56 | 57 | FROM alpine:3.7 as playground 58 | 59 | RUN apk add gcc g++ musl-dev 60 | 61 | RUN apk add lapack # Requirement for arraymancer 62 | 63 | RUN apk add pcre # Requirement for regex 64 | 65 | WORKDIR /playground 66 | 67 | COPY --from=builder /build/result/* ./nim/ 68 | 69 | COPY --from=installer /installer/nimble/pkgs ./nimble 70 | 71 | ENV PATH=$PATH:/playground/nim/bin 72 | 73 | RUN mkdir /usercode && chown nobody:nobody /usercode 74 | 75 | RUN chown -R nobody:nobody /playground/nimble 76 | -------------------------------------------------------------------------------- /docker/packages.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.0.0" 4 | author = "Peter Munch-Ellingsen" 5 | description = "Pseduo-package to list dependencies to install" 6 | license = "MIT" 7 | 8 | # Dependencies 9 | 10 | requires "termstyle" 11 | requires "NimData" 12 | requires "argparse" 13 | requires "arraymancer" 14 | requires "ast_pattern_matching" 15 | requires "bigints" 16 | requires "binaryheap" 17 | requires "blscurve" 18 | requires "bncurve" 19 | requires "c2nim" 20 | requires "cascade" 21 | requires "chroma" 22 | requires "chronicles" 23 | requires "chronos" 24 | requires "cligen" 25 | requires "combparser" 26 | requires "compactdict" 27 | requires "criterion" 28 | requires "dashing" 29 | requires "docopt" 30 | requires "elvis" 31 | requires "fragments" 32 | requires "gara" 33 | requires "glob" 34 | requires "gnuplot" 35 | requires "hts" 36 | requires "illwill" 37 | requires "inim" 38 | requires "itertools" 39 | requires "iterutils" 40 | requires "jstin" 41 | requires "karax" 42 | requires "loopfusion" 43 | requires "macroutils" 44 | requires "msgpack4nim" 45 | requires "neo" 46 | requires "nesm" 47 | requires "nicy" 48 | requires "nimcrypto" 49 | requires "nimes" 50 | requires "nimfp" 51 | requires "nimgen" 52 | requires "nimly" 53 | requires "nimpy" 54 | requires "nimquery" 55 | requires "nimsl" 56 | requires "nimsvg" 57 | requires "norm" 58 | requires "npeg" 59 | requires "optionsutils" 60 | requires "ormin" 61 | requires "parsetoml" 62 | requires "patty" 63 | requires "plotly" 64 | requires "pnm" 65 | requires "polypbren" 66 | requires "protobuf" 67 | requires "rbtree" 68 | requires "react" 69 | requires "regex" 70 | requires "result" 71 | requires "rosencrantz" 72 | requires "snip" 73 | requires "stint" 74 | requires "strunicode" 75 | requires "telebot" 76 | requires "tempdir" 77 | requires "tiny_sqlite" 78 | requires "timeit" 79 | requires "unicodedb" 80 | requires "unicodeplus" 81 | requires "unpack" 82 | requires "with" 83 | requires "ws" 84 | requires "yaml" 85 | requires "zero_functional" 86 | requires "https://github.com/nim-lang/fusion" 87 | -------------------------------------------------------------------------------- /src/nim_playground.nim: -------------------------------------------------------------------------------- 1 | import jester, asyncdispatch, os, osproc, strutils, json, threadpool, asyncfile, asyncnet, posix, logging, nuuid, tables, httpclient, streams, uri 2 | import ansitohtml, ansiparse, sequtils 3 | 4 | type 5 | Config = object 6 | tmpDir: ptr string 7 | logFile: ptr string 8 | 9 | APIToken = object 10 | gist: string 11 | 12 | ParsedRequest = object 13 | code: string 14 | compilationTarget: string 15 | 16 | RequestConfig = object 17 | tmpDir: string 18 | 19 | OutputFormat = enum 20 | Raw, HTML, Ansi, AnsiParsed 21 | 22 | const configFileName = "conf.json" 23 | 24 | onSignal(SIGABRT): 25 | ## Handle SIGABRT from systemd 26 | # Lines printed to stdout will be received by systemd and logged 27 | # Start with "" from 0 to 7 28 | echo "<2>Received SIGABRT" 29 | quit(1) 30 | 31 | var conf = createShared(Config) 32 | let parsedConfig = parseFile(configFileName) 33 | var 34 | tmpDir = parsedConfig["tmp_dir"].str 35 | logFile = parsedConfig["log_fname"].str 36 | 37 | discard existsOrCreateDir(tmpDir) 38 | 39 | conf.tmpDir = tmpDir.addr 40 | conf.logFile = logFile.addr 41 | 42 | let fl = newFileLogger(conf.logFile[], fmtStr = "$datetime $levelname ") 43 | fl.addHandler 44 | 45 | proc `%`(c: char): JsonNode = 46 | %($c) 47 | 48 | proc respondOnReady(fv: FlowVar[TaintedString], requestConfig: ptr RequestConfig, output: OutputFormat): Future[string] {.async.} = 49 | while true: 50 | if fv.isReady: 51 | echo ^fv 52 | 53 | var errorsFile = openAsync("$1/errors.txt" % requestConfig.tmpDir, fmRead) 54 | var logFile = openAsync("$1/logfile.txt" % requestConfig.tmpDir, fmRead) 55 | var errors = await errorsFile.readAll() 56 | var log = await logFile.readAll() 57 | 58 | template cleanAndColourize(x: string): string = 59 | x 60 | .multiReplace([("<", "<"), (">", ">"), ("\n", "
")]) 61 | .ansiToHtml({"31": "color: red", "32": "color: #66d9ef", "36": "color: #50fa7b"}.toTable) 62 | 63 | template clearAnsi(y: string): string = 64 | y.parseAnsi 65 | .filter(proc (x: AnsiData): bool = x.kind == String) 66 | .map(proc (x: AnsiData): string = x.str) 67 | .join() 68 | 69 | var ret: JsonNode 70 | 71 | case output: 72 | of HTML: 73 | ret = %* {"compileLog": cleanAndColourize(errors), 74 | "log": cleanAndColourize(log)} 75 | of Ansi: 76 | ret = %* {"compileLog": errors, "log": log} 77 | of AnsiParsed: 78 | ret = %* {"compileLog": errors.parseAnsi, "log": log.parseAnsi} 79 | of Raw: 80 | ret = %* {"compileLog": errors.clearAnsi, "log": log.clearAnsi} 81 | 82 | errorsFile.close() 83 | logFile.close() 84 | discard execProcess("sudo -u nobody /bin/chmod a+w $1/*" % [requestConfig.tmpDir]) 85 | removeDir(requestConfig.tmpDir) 86 | freeShared(requestConfig) 87 | return $ret 88 | 89 | 90 | await sleepAsync(500) 91 | 92 | proc prepareAndCompile(code, compilationTarget: string, requestConfig: ptr RequestConfig, version: string): TaintedString = 93 | discard existsOrCreateDir(requestConfig.tmpDir) 94 | copyFileWithPermissions("./test/script.sh", "$1/script.sh" % requestConfig.tmpDir) 95 | writeFile("$1/in.nim" % requestConfig.tmpDir, code) 96 | echo execProcess("chmod a+w $1" % [requestConfig.tmpDir]) 97 | 98 | let cmd = """ 99 | ./docker_timeout.sh 20s -i -t --net=none -v "$1":/usercode --user nobody virtual_machine:$2 /usercode/script.sh in.nim $3 100 | """ % [requestConfig.tmpDir, version, compilationTarget] 101 | 102 | execProcess(cmd) 103 | 104 | proc loadUrl(url: string): Future[string] {.async.} = 105 | let client = newAsyncHttpClient() 106 | client.onProgressChanged = proc (total, progress, speed: BiggestInt) {.async.} = 107 | if total > 1048576 or progress > 1048576 or (progress > 4000 and speed < 100): 108 | client.close() 109 | return await client.getContent(url) 110 | 111 | proc createIx(code: string): string = 112 | let client = newHttpClient() 113 | var data = newMultipartData() 114 | data["f:1"] = code 115 | client.postContent("http://ix.io", multipart = data)[0..^2] & "/nim" 116 | 117 | proc loadIx(ixid: string): Future[string] {.async.} = 118 | try: 119 | return await loadUrl("http://ix.io/$1" % ixid) 120 | except: 121 | return "Unable to load ix paste, file too large, or download is too slow" 122 | 123 | proc compile(code, compilationTarget: string, output: OutputFormat, requestConfig: ptr RequestConfig, version: string): Future[string] = 124 | let fv = spawn prepareAndCompile(code, compilationTarget, requestConfig, version) 125 | return respondOnReady(fv, requestConfig, output) 126 | 127 | proc isVersion(ver: string): bool = 128 | let parts = ver.split('.') 129 | if parts.len != 3: 130 | return false 131 | if parts[0][0] != 'v': 132 | return false 133 | else: 134 | if not parts[0][1..^1].isDigit or not parts[1].isDigit or not parts[2].isDigit: 135 | return false 136 | return ver in execProcess("docker images | sed -n 's/virtual_machine *\\(v[^ ]*\\).*/\\1/p' | sort --version-sort").split("\n")[0..^2] 137 | 138 | routes: 139 | get "/index.html#@extra": 140 | redirect "/#" & @"extra" 141 | 142 | get "/index.html": 143 | redirect "/" 144 | 145 | get "/": 146 | resp readFile("public/index.html") 147 | 148 | get "/versions": 149 | resp $(%*{"versions": execProcess("docker images | sed -n 's/virtual_machine *\\(v[^ ]*\\).*/\\1/p' | sort --version-sort").split("\n")[0..^2]}) 150 | 151 | get "/tour/@url": 152 | resp(Http200, [("Content-Type","text/plain")], await loadUrl(decodeUrl(@"url"))) 153 | 154 | get "/ix/@ixid": 155 | resp(Http200, await loadIx(@"ixid")) 156 | 157 | post "/ix": 158 | var parsedRequest: ParsedRequest 159 | let parsed = parseJson(request.body) 160 | if getOrDefault(parsed, "code").isNil: 161 | resp(Http400) 162 | parsedRequest = to(parsed, ParsedRequest) 163 | 164 | resp(Http200, @[("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Methods", "POST")], createix(parsedRequest.code)) 165 | 166 | post "/compile": 167 | var parsedRequest: ParsedRequest 168 | 169 | var 170 | outputFormat = Raw 171 | version = "latest" 172 | if request.params.len > 0: 173 | if request.params.hasKey("code"): 174 | parsedRequest.code = request.params["code"] 175 | if request.params.hasKey("compilationTarget"): 176 | parsedRequest.compilationTarget = request.params["compilationTarget"] 177 | if request.params.hasKey("outputFormat"): 178 | try: 179 | outputFormat = parseEnum[OutputFormat](request.params["outputFormat"]) 180 | except: 181 | resp(Http400) 182 | if request.params.hasKey("version"): 183 | version = request.params["version"] 184 | else: 185 | let parsed = parseJson(request.body) 186 | if getOrDefault(parsed, "code").isNil: 187 | resp(Http400, "{\"error\":\"No code\"") 188 | if getOrDefault(parsed, "compilationTarget").isNil: 189 | resp(Http400, "{\"error\":\"No compilation target\"}") 190 | parsedRequest = to(parsed, ParsedRequest) 191 | if parsed.hasKey("outputFormat"): 192 | try: 193 | outputFormat = parseEnum[OutputFormat](parsed["outputFormat"].str) 194 | except: 195 | resp(Http400, "{\"error\":\"Invalid output format\"") 196 | if parsed.hasKey("version"): 197 | version = parsed["version"].str 198 | 199 | if version != "latest" and not version.isVersion: 200 | resp(Http400, "{\"error\":\"Unknown version\"}") 201 | 202 | let requestConfig = createShared(RequestConfig) 203 | requestConfig.tmpDir = conf.tmpDir[] & "/" & generateUUID() 204 | let compileResult = await compile(parsedRequest.code, parsedRequest.compilationTarget, outputFormat, requestConfig, version) 205 | 206 | resp(Http200, [("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Methods", "POST")], compileResult) 207 | 208 | 209 | info "Starting!" 210 | runForever() 211 | freeShared(conf) 212 | --------------------------------------------------------------------------------