├── .gitignore ├── Dockerfile ├── README.md ├── conf.json ├── docker_timeout.sh ├── nim.cfg ├── nim_playground.nimble ├── setup.sh ├── src └── nim_playground.nim └── test └── script.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | nimcache 3 | nim_playground -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nimlang/nim 2 | 3 | RUN apt-get update && apt-get install build-essential -y -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nim-playground 2 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "log_fname": "/var/log/nim_playground.log", 3 | "tmp_dir": "/tmp/nim_playground" 4 | } 5 | -------------------------------------------------------------------------------- /docker_timeout.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | to=$1 5 | shift 6 | 7 | cont=$(sudo docker run -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 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install Docker 4 | sudo apt-get update 5 | sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D 6 | sudo apt-add-repository 'deb https://apt.dockerproject.org/repo ubuntu-xenial main' 7 | sudo apt-get update 8 | apt-cache policy docker-engine 9 | sudo apt-get install -y docker-engine 10 | 11 | # Create Docker image 12 | echo "Creating Docker Image" 13 | docker build -t 'virtual_machine' - < Dockerfile 14 | echo "Retrieving Installed Docker Images" 15 | docker images -------------------------------------------------------------------------------- /src/nim_playground.nim: -------------------------------------------------------------------------------- 1 | import jester, asyncdispatch, os, osproc, strutils, json, threadpool, asyncfile, asyncnet, posix, logging, nuuid, tables, httpclient 2 | 3 | type 4 | Config = object 5 | tmpDir: string 6 | logFile: string 7 | 8 | APIToken = object 9 | gist: string 10 | 11 | ParsedRequest = object 12 | code: string 13 | compilationTarget: string 14 | 15 | RequestConfig = object 16 | tmpDir: string 17 | 18 | const configFileName = "conf.json" 19 | # const apiTokenFileName = "token.json" 20 | 21 | onSignal(SIGABRT): 22 | ## Handle SIGABRT from systemd 23 | # Lines printed to stdout will be received by systemd and logged 24 | # Start with "" from 0 to 7 25 | echo "<2>Received SIGABRT" 26 | quit(1) 27 | 28 | var conf = createShared(Config) 29 | let parsedConfig = parseFile(configFileName) 30 | conf.tmpDir = parsedConfig["tmp_dir"].str 31 | conf.logFile = parsedConfig["log_fname"].str 32 | 33 | # var apiToken = createShared(APIToken) 34 | # let parsedAPIToken = parseFile(apiTokenFileName) 35 | # apiToken.gist = parsedAPIToken["gist"].str 36 | 37 | let fl = newFileLogger(conf.logFile, fmtStr = "$datetime $levelname ") 38 | fl.addHandler 39 | 40 | proc respondOnReady(fv: FlowVar[TaintedString], requestConfig: ptr RequestConfig): Future[string] {.async.} = 41 | while true: 42 | if fv.isReady: 43 | echo ^fv 44 | 45 | var errorsFile = openAsync("$1/errors.txt" % requestConfig.tmpDir, fmReadWrite) 46 | var logFile = openAsync("$1/logfile.txt" % requestConfig.tmpDir, fmReadWrite) 47 | var errors = await errorsFile.readAll() 48 | var log = await logFile.readAll() 49 | 50 | var ret = %* {"compileLog": errors, "log": log} 51 | 52 | errorsFile.close() 53 | logFile.close() 54 | freeShared(requestConfig) 55 | return $ret 56 | 57 | 58 | await sleepAsync(500) 59 | 60 | proc prepareAndCompile(code, compilationTarget: string, requestConfig: ptr RequestConfig): TaintedString = 61 | discard existsOrCreateDir(requestConfig.tmpDir) 62 | copyFileWithPermissions("./test/script.sh", "$1/script.sh" % requestConfig.tmpDir) 63 | writeFile("$1/in.nim" % requestConfig.tmpDir, code) 64 | 65 | execProcess(""" 66 | ./docker_timeout.sh 20s -i -t --net=none -v "$1":/usercode virtual_machine /usercode/script.sh in.nim $2 67 | """ % [requestConfig.tmpDir, compilationTarget]) 68 | 69 | # proc createGist(code: string): string = 70 | # let client = newHttpClient() 71 | 72 | # client.headers = newHttpHeaders([("Content-Type", "application/json" )]) 73 | # let body = %*{ 74 | # "description": "Snippet from https://play.nim-lang.org", 75 | # "public": true, 76 | # "files": { 77 | # "playground.nim": { 78 | # "content": code 79 | # } 80 | # } 81 | # } 82 | # let resp = client.request("https://api.github.com/gists?client_id=887dc07b67acec87e489&client_secret=$1" % apiToken.gist, httpMethod = HttpPost, body = $body) 83 | 84 | # let parsedResponse = parseJson(resp.bodyStream, "response.json") 85 | # return parsedResponse.getOrDefault("html_url").str 86 | 87 | # proc loadGist(gistId: string): string = 88 | # let client = newHttpClient() 89 | 90 | # client.headers = newHttpHeaders([("Content-Type", "application/json" )]) 91 | 92 | # let resp = client.request("https://api.github.com/gists/$1?client_id=887dc07b67acec87e489&client_secret=$2" % [gistId, apiToken.gist], httpMethod = HttpGet) 93 | 94 | # let parsedResponse = parseJson(resp.bodyStream, "response.json") 95 | # return parsedResponse.getOrDefault("files").fields["playground.nim"].fields["content"].str 96 | 97 | 98 | proc compile(code, compilationTarget: string, requestConfig: ptr RequestConfig): Future[string] = 99 | let fv = spawn prepareAndCompile(code, compilationTarget, requestConfig) 100 | return respondOnReady(fv, requestConfig) 101 | 102 | routes: 103 | # get "/gist/@gistId": 104 | # resp(Http200, loadGist(@"gistId")) 105 | 106 | # post "/gist": 107 | # var parsedRequest: ParsedRequest 108 | # let parsed = parseJson(request.body) 109 | # if getOrDefault(parsed, "code").isNil: 110 | # resp(Http400) 111 | # parsedRequest = to(parsed, ParsedRequest) 112 | 113 | # resp(Http200, @[("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Methods", "POST")], createGist(parsedRequest.code)) 114 | post "/compile": 115 | var parsedRequest: ParsedRequest 116 | 117 | if request.params.len > 0: 118 | if request.params.hasKey("code"): 119 | parsedRequest.code = request.params["code"] 120 | if request.params.hasKey("compilationTarget"): 121 | parsedRequest.compilationTarget = request.params["compilationTarget"] 122 | else: 123 | let parsed = parseJson(request.body) 124 | if getOrDefault(parsed, "code").isNil: 125 | resp(Http400) 126 | if getOrDefault(parsed, "compilationTarget").isNil: 127 | resp(Http400) 128 | parsedRequest = to(parsed, ParsedRequest) 129 | 130 | let requestConfig = createShared(RequestConfig) 131 | requestConfig.tmpDir = conf.tmpDir & "/" & generateUUID() 132 | let compileResult = await compile(parsedRequest.code, parsedRequest.compilationTarget, requestConfig) 133 | 134 | resp(Http200, [("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Methods", "POST")], compileResult) 135 | 136 | 137 | info "Starting!" 138 | runForever() 139 | freeShared(conf) 140 | # freeShared(apiToken) -------------------------------------------------------------------------------- /test/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 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 /usercode/$file 14 | if [ $? -eq 0 ]; then 15 | ./usercode/${file/.nim/""} 16 | else 17 | echo "Compilation Failed" 18 | fi 19 | --------------------------------------------------------------------------------