├── .gitignore ├── DockerImage ├── BUILD.md └── Dockerfile ├── config.toml.example ├── LICENSE ├── README.md ├── request ├── request_test.go └── request.go ├── 0001-Fix-compabability-issue-with-NEUOJ-Product-version.patch ├── judge-controller ├── worker.go ├── judge.go ├── prepare.go ├── worker_test.go ├── run.go ├── controller.go ├── guard.go └── build.go ├── config └── config.go ├── downloader ├── dowloader_test.go └── downloader.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # D-judge .gitignore file 2 | D-judge 3 | judge_root/ 4 | cache_root/ 5 | config.toml 6 | -------------------------------------------------------------------------------- /DockerImage/BUILD.md: -------------------------------------------------------------------------------- 1 | BUILD your own image 2 | ==== 3 | 4 | * RUN `docker build -t your-name/image-name .` 5 | * RUN `docker login` 6 | * RUN `docker push your-name/image-name` 7 | -------------------------------------------------------------------------------- /DockerImage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | MAINTAINER VOID001 4 | 5 | RUN apt-get update 6 | RUN yes | apt-get install python python3 openjdk-7-jre openjdk-7-jdk php5 ruby mysql-server gcc 7 | RUN yes | apt-get install unzip 8 | RUN yes | apt-get install g++ 9 | RUN echo > judgehost-info.txt << EOF Judgehost Image Status \ 10 | Python2 Version: $(python2.7 --version) \ 11 | Python3 Version: $(python3 --version) \ 12 | Java Version: $(java -version) \ 13 | PHP Version :$(php --version) \ 14 | Ruby Version: $(ruby --version) \ 15 | MySQL Version $(mysqld --version) \ 16 | gcc version $(gcc --version) \ 17 | EOF 18 | 19 | RUN service mysql start 20 | 21 | -------------------------------------------------------------------------------- /config.toml.example: -------------------------------------------------------------------------------- 1 | # D-judge configuration 2 | 3 | run_mode = "prod" # Currently not use 4 | host_name = "D-judge-helloworld" # Specify the hostname of the judge 5 | 6 | docker_image = "void001/neuoj-judge-image:latest" # Image use to run in docker 7 | docker_server = "unix:///var/run/docker.sock" # path to your docker socket/port, if you do not know how to set it, leave it as default setting 8 | docker_version = "v1.24" # docker daemon version, use `docker version` and set version to Server API version 9 | 10 | cache_root = "cache_root" # Path need to be abosolute path 11 | max_cache_size = 4096000 # in Bytes 12 | root_mem = 40960000000 # in Bytes 13 | 14 | judge_root = "judge_root" # Path need to be absolute path 15 | 16 | endpoint_name = "neuoj-test" 17 | endpoint_url = "http://127.0.0.1:8080/api" # Set it to your NEUOJ server API endpoint 18 | endpoint_user = "neuoj" # set it to your JUDGE_USER set in NEUOJ .env 19 | endpoint_password = "neuoj" # set it to your JUDGE_PW in NEUOJ .env 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jianqiu Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker Judge 2 | ==== 3 | 4 | Online Judgehost powered by Docker for NEUOJ 5 | 6 | #### Installation 7 | 8 | * You need to install go compiler first https://golang.org/ please use go1.7 9 | * You need to have docker installed 10 | * Run `go get -u -v github.com/VOID001/D-judge` 11 | * cd to `$GOPATH/src/github.com/VOID001/D-judge/` 12 | * Run `go build` then 13 | * Create config.toml, you can copy one from config.toml.example 14 | 15 | 16 | #### Run 17 | 18 | * Configure the Judgehost specified configuration, more info can found in config.toml.example 19 | * Run NEUOJ Server and start docker service 20 | * Run `sudo ./D-judge` to start the judgehost 21 | 22 | #### Contribution 23 | 24 | * Please use pull request and github issue to contribute :) 25 | 26 | 27 | #### FAQ 28 | 29 | * Q: I got `json decode error: json: cannot unmarshal string into Go value of type int64` when running D-judge 30 | * A: Please run `patch -p1 < 0001-Fix-compabability-issue-with-NEUOJ-Product-version.patch` in D-judge source root 31 | 32 | * Q: I got `worker error: downloading testcase error: error processing download: request error status code 500 data` 33 | * A: Please make sure you upload testcase to Server(NEUOJ) 34 | -------------------------------------------------------------------------------- /request/request_test.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/VOID001/D-judge/config" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | var GlobalConfig = config.SystemConfig{ 16 | HostName: "judge-01", 17 | EndpointName: "neuoj", 18 | EndpointUser: "neuoj", 19 | EndpointURL: "http://localhost:8080/api", 20 | EndpointPassword: "neuoj", 21 | JudgeRoot: "/tmp/judge_root", 22 | CacheRoot: "/tmp/cache_root", 23 | DockerImage: "void001/neuoj-judge-image:latest", 24 | DockerServer: "unix:///var/run/docker.sock", 25 | } 26 | 27 | func init() { 28 | config.GlobalConfig = GlobalConfig 29 | // Set Level to debug 30 | log.SetLevel(log.DebugLevel) 31 | } 32 | 33 | func TestDoPostForm(t *testing.T) { 34 | t.Logf("Running TestDo") 35 | body := url.Values{"hostname": {config.GlobalConfig.HostName}} 36 | err := Do(context.Background(), http.MethodPost, "/judgehosts", body, TypeForm, nil) 37 | if err != nil { 38 | err = errors.Wrap(err, "post form error") 39 | t.Error(err) 40 | t.Fail() 41 | } 42 | } 43 | 44 | func TestDoPostJSON(t *testing.T) { 45 | 46 | } 47 | 48 | func TestGet(t *testing.T) { 49 | 50 | } 51 | 52 | func TestDoPostJudgings(t *testing.T) { 53 | jinfo := config.JudgeInfo{} 54 | err := Do(context.Background(), http.MethodPost, fmt.Sprintf("/judgings?judgehost=%s", config.GlobalConfig.HostName), nil, "", &jinfo) 55 | if err != nil { 56 | err = errors.Wrap(err, "post judgings error") 57 | t.Error(err) 58 | t.Fail() 59 | } 60 | t.Logf("Judge Info Get\n %+v", jinfo) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /0001-Fix-compabability-issue-with-NEUOJ-Product-version.patch: -------------------------------------------------------------------------------- 1 | From 03cca51b6ca935f08dbc37a9a36de2ad77cf4d30 Mon Sep 17 00:00:00 2001 2 | From: Jianqiu Zhang 3 | Date: Thu, 10 Nov 2016 00:22:09 +0800 4 | Subject: [PATCH] Fix compabability issue with NEUOJ Product version 5 | 6 | NEUOJ has bug that json_encode will encode a int to string and D-judge cannot decode the json strcture correctly, so D-judge make config adapt with it 7 | --- 8 | config/config.go | 18 +++++++++--------- 9 | 1 file changed, 9 insertions(+), 9 deletions(-) 10 | 11 | diff --git a/config/config.go b/config/config.go 12 | index bf65544..70224f3 100644 13 | --- a/config/config.go 14 | +++ b/config/config.go 15 | @@ -26,15 +26,15 @@ type SystemConfig struct { 16 | } 17 | 18 | type JudgeInfo struct { 19 | - SubmitID int64 `json:"submitid"` 20 | + SubmitID int64 `json:"submitid,string"` 21 | ContestID int64 `json:"cid"` 22 | - TeamID int64 `json:"teamid"` 23 | - JudgingID int64 `json:"judgingid"` 24 | - ProblemID int64 `json:"probid"` 25 | + TeamID int64 `json:"teamid,string"` 26 | + JudgingID int64 `json:"judgingid,string"` 27 | + ProblemID int64 `json:"probid,string"` 28 | Language string `json:"langid"` 29 | - TimeLimit int64 `json:"maxruntime"` 30 | - MemLimit int64 `json:"memlimit"` 31 | - OutputLimit int64 `json:"output_limit"` 32 | + TimeLimit int64 `json:"maxruntime,string"` 33 | + MemLimit int64 `json:"memlimit,string"` 34 | + OutputLimit int64 `json:"output_limit,string"` 35 | BuildZip string `json:"compile_script"` 36 | BuildZipMD5 string `json:"compile_script_md5sum"` 37 | RunZip string `json:"run"` 38 | @@ -45,9 +45,9 @@ type JudgeInfo struct { 39 | } 40 | 41 | type TestcaseInfo struct { 42 | - TestcaseID int64 `json:"testcaseid"` 43 | + TestcaseID int64 `json:"testcaseid,string"` 44 | Rank int64 `json:"rank"` 45 | - ProblemID int64 `json:"probid"` 46 | + ProblemID int64 `json:"probid,string"` 47 | MD5SumInput string `json:"md5sum_input"` 48 | MD5SumOutput string `json:"md5sum_output"` 49 | } 50 | -- 51 | 2.10.0 52 | 53 | -------------------------------------------------------------------------------- /judge-controller/worker.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | "github.com/VOID001/D-judge/config" 15 | "github.com/docker/engine-api/client" 16 | "github.com/docker/engine-api/types" 17 | ) 18 | 19 | type runinfo struct { 20 | usedmem uint64 21 | usedtime int64 22 | outputexceed bool 23 | timeexceed bool 24 | memexceed bool 25 | } 26 | 27 | type Worker struct { 28 | JudgeInfo config.JudgeInfo 29 | WorkDir string 30 | DockerImage string 31 | RunUser string 32 | CPUID int 33 | MaxRetryTime int 34 | containerID string 35 | codeFileName string 36 | } 37 | 38 | const ( 39 | FilePerm = 0644 40 | DirPerm = 0755 41 | SandboxRoot = "/sandbox" 42 | ) 43 | 44 | func (w *Worker) cleanup(ctx context.Context) (err error) { 45 | log.Debugf("doing cleanup for containerID %s", w.containerID) 46 | cli, er := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 47 | if er != nil { 48 | err = errors.Wrap(er, "worker cleanup error") 49 | return err 50 | } 51 | err = cli.ContainerStop(ctx, w.containerID, nil) 52 | if err != nil { 53 | err = errors.Wrap(err, "worker cleanup error") 54 | return err 55 | } 56 | err = cli.ContainerRemove(ctx, w.containerID, types.ContainerRemoveOptions{}) 57 | if err != nil { 58 | err = errors.Wrap(err, "worker cleanup error") 59 | return err 60 | } 61 | return 62 | } 63 | 64 | func (w *Worker) readExitCode(ctx context.Context) (code int, err error) { 65 | // Read the file exitcode and return 66 | 67 | path := filepath.Join(w.WorkDir, "exitcode") 68 | data, er := ioutil.ReadFile(path) 69 | if er != nil { 70 | err = errors.Wrap(er, "read exit code from file error") 71 | return 72 | } 73 | str := fmt.Sprintf("%s", data) 74 | str = strings.TrimSuffix(str, "\n") 75 | str = strings.TrimPrefix(str, "\n") 76 | code, err = strconv.Atoi(str) 77 | if err != nil { 78 | err = errors.Wrap(err, "read exit code from file error") 79 | return 80 | } 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var GlobalConfig SystemConfig 4 | 5 | // Define Run results 6 | const ( 7 | ResTLE = "timelimit" 8 | ResWA = "wrong-answer" 9 | ResAC = "correct" 10 | ResCE = "compiler-error" 11 | ResRE = "run-error" 12 | ) 13 | 14 | type SystemConfig struct { 15 | HostName string `toml:"host_name"` 16 | EndpointUser string `toml:"endpoint_user"` 17 | EndpointName string `toml:"endpoint_name"` 18 | EndpointURL string `toml:"endpoint_url"` 19 | MaxCacheSize int `toml:"max_cache_size"` 20 | EndpointPassword string `toml:"endpoint_password"` 21 | JudgeRoot string `toml:"judge_root"` 22 | DockerImage string `toml:"docker_image"` 23 | DockerServer string `toml:"docker_server"` 24 | DockerVersion string `toml:"docker_version"` 25 | CacheRoot string `toml:"cache_root"` 26 | RootMemory int64 `toml:"root_mem"` 27 | } 28 | 29 | type JudgeInfo struct { 30 | SubmitID int64 `json:"submitid"` 31 | ContestID int64 `json:"cid"` 32 | TeamID int64 `json:"teamid"` 33 | JudgingID int64 `json:"judgingid"` 34 | ProblemID int64 `json:"probid"` 35 | Language string `json:"langid"` 36 | TimeLimit int64 `json:"maxruntime"` 37 | MemLimit int64 `json:"memlimit"` 38 | OutputLimit int64 `json:"output_limit"` 39 | BuildZip string `json:"compile_script"` 40 | BuildZipMD5 string `json:"compile_script_md5sum"` 41 | RunZip string `json:"run"` 42 | RunZipMD5 string `json:"run_md5sum"` 43 | CompareZip string `json:"compare"` 44 | CompareZipMD5 string `json:"compare_md5sum"` 45 | CompareArgs string `json:"compare_args"` 46 | } 47 | 48 | type TestcaseInfo struct { 49 | TestcaseID int64 `json:"testcaseid"` 50 | Rank int64 `json:"rank"` 51 | ProblemID int64 `json:"probid"` 52 | MD5SumInput string `json:"md5sum_input"` 53 | MD5SumOutput string `json:"md5sum_output"` 54 | } 55 | 56 | type SubmissionInfo struct { 57 | info []SubmissionFileInfo `json:""` 58 | } 59 | 60 | type SubmissionFileInfo struct { 61 | FileName string `json:"filename"` 62 | Content string `json:"contetn"` 63 | } 64 | 65 | type RunResult struct { 66 | JudgingID int64 67 | TestcaseID int64 68 | RunResult string 69 | RunTime float64 70 | OutputRun string 71 | OutputError string 72 | OutputSystem string 73 | OutputDiff string 74 | } 75 | -------------------------------------------------------------------------------- /judge-controller/judge.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/VOID001/D-judge/config" 12 | "github.com/VOID001/D-judge/request" 13 | "github.com/docker/engine-api/client" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | const ( 18 | ExitAC = 42 19 | ExitWA = 43 20 | ) 21 | 22 | func (w *Worker) judge(ctx context.Context, rank int64, tid int64) (err error) { 23 | // Create testcase dir, use to store result 24 | execdir := filepath.Join(w.WorkDir, "execdir") 25 | testcasedir := filepath.Join(w.WorkDir, fmt.Sprintf("testcase%03d", rank)) 26 | err = os.Mkdir(testcasedir, DirPerm) 27 | if err != nil { 28 | err = errors.Wrap(err, fmt.Sprintf("Judge error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 29 | return 30 | } 31 | // Build the judge script 32 | cli, er := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 33 | if er != nil { 34 | err = errors.Wrap(er, fmt.Sprintf("Judge error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 35 | return 36 | } 37 | 38 | cmd := fmt.Sprintf("compare/run execdir/testcase.in execdir/testcase.out testcase001 < execdir/program.out 2> compare.err >compare.out") 39 | log.Debugf("executing command %s", cmd) 40 | info, err := w.execcmdAttach(ctx, cli, "root", cmd) 41 | // time.Sleep(time.Second * 10) 42 | code := info.ExitCode 43 | if err != nil { 44 | err = errors.Wrap(err, fmt.Sprintf("Judge error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 45 | return 46 | } 47 | 48 | res := config.RunResult{} 49 | res.RunTime = 0 // This is uneccessary 50 | res.TestcaseID = tid 51 | res.JudgingID = w.JudgeInfo.JudgingID 52 | 53 | // Parse system meta and send to output system 54 | // For Domjudge compability 55 | // Save for Judge use 56 | data, er := ioutil.ReadFile(filepath.Join(execdir, "program.meta")) 57 | if er != nil { 58 | err = errors.Wrap(er, "judge error") 59 | return 60 | } 61 | res.OutputSystem = fmt.Sprintf("%s", data) 62 | 63 | switch code { 64 | case ExitWA: 65 | res.RunResult = config.ResWA 66 | // Report Accepted 67 | case ExitAC: 68 | res.RunResult = config.ResAC 69 | // Report Wrong Answer 70 | default: 71 | err = errors.New(fmt.Sprintf("Judge return unexpected exit code %d", code)) 72 | return 73 | } 74 | 75 | // Remove execdir for next time use 76 | oldexecdir := fmt.Sprintf("%s%03d", execdir, rank) 77 | err = request.PostResult(ctx, res) 78 | if err != nil { 79 | err = errors.Wrap(err, "Judge error") 80 | return 81 | } 82 | err = os.Rename(execdir, oldexecdir) 83 | if err != nil { 84 | err = errors.Wrap(err, fmt.Sprintf("Judge error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 85 | return 86 | } 87 | return 88 | } 89 | -------------------------------------------------------------------------------- /judge-controller/prepare.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | "github.com/VOID001/D-judge/downloader" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func (w *Worker) prepare(ctx context.Context) (err error) { 15 | log.Debugf("preparing for judge, worker info %+v", w) 16 | // Download needed sources, perpare the working dir 17 | 18 | // Ensure the robustness of the judgehost 19 | if _, err = os.Stat(w.WorkDir); os.IsNotExist(err) { 20 | log.Errorf("work dir %s not found, re-create work dir", w.WorkDir) 21 | os.MkdirAll(w.WorkDir, DirPerm) 22 | } 23 | 24 | // Get the code first 25 | d := downloader.Downloader{ 26 | FileType: "code", 27 | Destination: filepath.Join(w.WorkDir, "foo"), // Here just provide a dummy destination, it will correct when call downloader 28 | FileName: filepath.Join(w.WorkDir, "foo"), // Here just provide a dummy filename, it will correct when call downloader 29 | SkipMD5Check: true, 30 | UseCache: false, 31 | Params: []string{fmt.Sprintf("%d", w.JudgeInfo.SubmitID)}, 32 | } 33 | err = d.Do(ctx) 34 | if err != nil { 35 | err = errors.Wrap(err, "error preparing for judge") 36 | return 37 | } 38 | w.codeFileName = d.FileName 39 | 40 | // Get the build & run script then 41 | rundir := filepath.Join(w.WorkDir, "run") 42 | err = os.Mkdir(rundir, DirPerm) 43 | if err != nil { 44 | err = errors.Wrap(err, "error preparing for judge") 45 | return 46 | } 47 | 48 | d = downloader.Downloader{ 49 | FileType: "executable", 50 | FileName: w.JudgeInfo.RunZip, 51 | Destination: filepath.Join(rundir, w.JudgeInfo.RunZip), 52 | SkipMD5Check: false, 53 | MD5: w.JudgeInfo.RunZipMD5, 54 | UseCache: true, 55 | Params: []string{w.JudgeInfo.RunZip}, 56 | } 57 | err = d.Do(ctx) 58 | if err != nil { 59 | err = errors.Wrap(err, "error preparing for judge") 60 | return 61 | } 62 | 63 | builddir := filepath.Join(w.WorkDir, "build") 64 | err = os.Mkdir(builddir, DirPerm) 65 | if err != nil { 66 | err = errors.Wrap(err, "error preparing for judge") 67 | return 68 | } 69 | 70 | d.FileName = w.JudgeInfo.BuildZip 71 | d.Destination = filepath.Join(builddir, w.JudgeInfo.BuildZip) 72 | d.MD5 = w.JudgeInfo.BuildZipMD5 73 | d.Params = []string{w.JudgeInfo.BuildZip} 74 | err = d.Do(ctx) 75 | 76 | if err != nil { 77 | err = errors.Wrap(err, "error preparing for judge") 78 | return 79 | } 80 | 81 | comparedir := filepath.Join(w.WorkDir, "compare") 82 | err = os.Mkdir(comparedir, DirPerm) 83 | if err != nil { 84 | err = errors.Wrap(err, "error preparing for judge") 85 | return 86 | } 87 | d.FileName = w.JudgeInfo.CompareZip 88 | d.Destination = filepath.Join(comparedir, w.JudgeInfo.CompareZip) 89 | d.MD5 = w.JudgeInfo.CompareZipMD5 90 | d.Params = []string{w.JudgeInfo.CompareZip} 91 | 92 | err = d.Do(ctx) 93 | if err != nil { 94 | err = errors.Wrap(err, "error preparing for judge") 95 | return 96 | } 97 | 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /judge-controller/worker_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "testing" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/VOID001/D-judge/config" 10 | "github.com/docker/engine-api/client" 11 | "github.com/docker/engine-api/types" 12 | "github.com/docker/engine-api/types/container" 13 | ) 14 | 15 | var GlobalConfig = config.SystemConfig{ 16 | HostName: "judge-01", 17 | EndpointName: "neuoj", 18 | EndpointUser: "neuoj", 19 | EndpointURL: "http://localhost:8080/api", 20 | EndpointPassword: "neuoj", 21 | JudgeRoot: "/tmp/judge_root", 22 | CacheRoot: "/tmp/cache_root", 23 | DockerImage: "void001/neuoj-judge-image:latest", 24 | DockerServer: "unix:///var/run/docker.sock", 25 | DockerVersion: "v1.24", 26 | } 27 | 28 | func init() { 29 | config.GlobalConfig = GlobalConfig 30 | log.SetLevel(log.DebugLevel) 31 | } 32 | 33 | func TestWorkerPrepare(t *testing.T) { 34 | w := Worker{} 35 | w.JudgeInfo = config.JudgeInfo{ 36 | SubmitID: 1, 37 | ContestID: 0, 38 | TeamID: 2, 39 | JudgingID: 1, 40 | ProblemID: 1, 41 | Language: "c", 42 | TimeLimit: 3, 43 | MemLimit: 104, 44 | OutputLimit: 0, 45 | BuildZip: "c", 46 | BuildZipMD5: "c76e6afa913a9fc827c42c2357f47a53", 47 | RunZip: "run", 48 | RunZipMD5: "c2cb7864f2f7343d1ab5094b8fd40da4", 49 | CompareZip: "compare", 50 | CompareZipMD5: "71306aae6e243f8a030ab1bd7d6b354b", 51 | CompareArgs: "", 52 | } 53 | w.WorkDir = filepath.Join(config.GlobalConfig.JudgeRoot, "judge-test-1") 54 | err := w.prepare(context.Background()) 55 | if err != nil { 56 | t.Logf("Failed, error: %+v", err) 57 | t.Fail() 58 | return 59 | } 60 | } 61 | 62 | func TestWorkerExecCMD(t *testing.T) { 63 | w := Worker{} 64 | //cmd := fmt.Sprintf("compare/run execdir/testcase.in execdir/testcase.out testcase001 < execdir/program.out 2> compare.err >compare.out") 65 | cmd := "sleep 5; exit 233" 66 | cli, err := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 67 | if err != nil { 68 | t.Logf("Failed error: %+v", err) 69 | t.Fail() 70 | return 71 | } 72 | 73 | cfg := container.Config{} 74 | cfg.Image = config.GlobalConfig.DockerImage 75 | cfg.User = "root" // Future will change to judge, a low-privileged user 76 | cfg.Tty = true 77 | cfg.WorkingDir = "/sandbox" 78 | cfg.AttachStdin = false 79 | cfg.AttachStderr = false 80 | cfg.AttachStdout = true 81 | cfg.Cmd = []string{"/bin/bash"} 82 | hcfg := container.HostConfig{} 83 | hcfg.Binds = []string{"/tmp/testdir:/sandbox"} 84 | hcfg.Memory = config.GlobalConfig.RootMemory 85 | hcfg.PidsLimit = 64 // This is enough for almost all case 86 | 87 | resp, err := cli.ContainerCreate(context.TODO(), &cfg, &hcfg, nil, "") 88 | if err != nil { 89 | t.Logf("Failed error: %+v", err) 90 | t.Fail() 91 | return 92 | } 93 | err = cli.ContainerStart(context.TODO(), resp.ID, types.ContainerStartOptions{}) 94 | if err != nil { 95 | t.Logf("Failed error: %+v", err) 96 | t.Fail() 97 | return 98 | } 99 | w.containerID = resp.ID 100 | info, err := w.execcmd(context.TODO(), cli, "root", cmd) 101 | if err != nil { 102 | t.Logf("Failed error: %+v", err) 103 | t.Fail() 104 | return 105 | } 106 | if info.ExitCode != 233 { 107 | t.Logf("Expected exit code 233, got %d", info.ExitCode) 108 | t.Fail() 109 | return 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /downloader/dowloader_test.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | "github.com/VOID001/D-judge/config" 11 | ) 12 | 13 | var GlobalConfig = config.SystemConfig{ 14 | HostName: "judge-01", 15 | EndpointName: "neuoj", 16 | EndpointUser: "neuoj", 17 | EndpointURL: "http://localhost:8080/api", 18 | EndpointPassword: "neuoj", 19 | JudgeRoot: "/tmp/judge_root", 20 | CacheRoot: "/tmp/cache_root", 21 | DockerImage: "void001/neuoj-judge-image:latest", 22 | DockerServer: "unix:///var/run/docker.sock", 23 | } 24 | 25 | func init() { 26 | config.GlobalConfig = GlobalConfig 27 | log.SetLevel(log.DebugLevel) 28 | } 29 | 30 | func TestDoWithoutCache(t *testing.T) { 31 | d := Downloader{} 32 | d.Destination = "/tmp/testdata" 33 | d.FileName = "testdata" 34 | d.SkipMD5Check = true 35 | d.UseCache = false 36 | d.FileType = "code" 37 | d.Params = []string{"1"} 38 | err := d.Do(context.Background()) 39 | if err != nil { 40 | t.Logf("downloader do error: %+v", err) 41 | t.Fail() 42 | return 43 | } 44 | 45 | if _, err := os.Stat(d.Destination); err != nil && os.IsNotExist(err) { 46 | t.Logf("download failed but downloader do not return error") 47 | t.Fail() 48 | return 49 | } 50 | return 51 | } 52 | 53 | func TestDoWithCache(t *testing.T) { 54 | d := Downloader{} 55 | d.Destination = "/tmp/testdata" 56 | d.FileName = "testdata" 57 | d.SkipMD5Check = false 58 | d.UseCache = true 59 | d.FileType = "code" 60 | d.Params = []string{"1"} 61 | d.MD5 = "f7af11c0363fafa66f1705058f1a0058" 62 | err := d.Do(context.Background()) 63 | if err != nil { 64 | t.Logf("downloader do error: %+v", err) 65 | t.Fail() 66 | return 67 | } 68 | if _, err := os.Stat(d.Destination); err != nil && os.IsNotExist(err) { 69 | t.Logf("download failed but downloader do not return error") 70 | t.Fail() 71 | return 72 | } 73 | if _, err := os.Stat(filepath.Join(config.GlobalConfig.CacheRoot, d.FileName)); err != nil && os.IsNotExist(err) { 74 | t.Logf("download failed but downloader do not return error") 75 | t.Fail() 76 | return 77 | } 78 | return 79 | } 80 | 81 | func TestDownExecutableWithCache(t *testing.T) { 82 | d := Downloader{} 83 | d.Destination = "/tmp/c.zip" 84 | d.FileName = "c.zip" 85 | d.SkipMD5Check = false 86 | d.UseCache = true 87 | d.FileType = "executable" 88 | d.Params = []string{"c"} 89 | d.MD5 = "c76e6afa913a9fc827c42c2357f47a53" 90 | err := d.Do(context.Background()) 91 | if err != nil { 92 | t.Logf("downloader do error: %+v", err) 93 | t.Fail() 94 | return 95 | } 96 | 97 | if _, err := os.Stat(d.Destination); err != nil && os.IsNotExist(err) { 98 | t.Logf("download failed but downloader do not return error") 99 | t.Fail() 100 | return 101 | } 102 | if _, err := os.Stat(filepath.Join(config.GlobalConfig.CacheRoot, d.FileName)); err != nil && os.IsNotExist(err) { 103 | t.Logf("download failed but downloader do not return error") 104 | t.Fail() 105 | return 106 | } 107 | return 108 | } 109 | 110 | func TestDownTestcaseWithCache(t *testing.T) { 111 | d := Downloader{} 112 | d.Destination = "/tmp/test.in" 113 | d.FileName = "test.in" 114 | d.SkipMD5Check = false 115 | d.UseCache = true 116 | d.FileType = "testcase" 117 | d.Params = []string{"2", "input"} 118 | d.MD5 = "f303b7d2f2b87f9e16df05e2bca7c409" 119 | err := d.Do(context.Background()) 120 | if err != nil { 121 | t.Logf("downloader do error: %+v", err) 122 | t.Fail() 123 | return 124 | } 125 | 126 | if _, err := os.Stat(d.Destination); err != nil && os.IsNotExist(err) { 127 | t.Logf("download failed but downloader do not return error") 128 | t.Fail() 129 | return 130 | } 131 | if _, err := os.Stat(filepath.Join(config.GlobalConfig.CacheRoot, d.FileName)); err != nil && os.IsNotExist(err) { 132 | t.Logf("download failed but downloader do not return error") 133 | t.Fail() 134 | return 135 | } 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /judge-controller/run.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/VOID001/D-judge/config" 12 | "github.com/VOID001/D-judge/request" 13 | "github.com/docker/engine-api/client" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func (w *Worker) run(ctx context.Context, rank int64, tid int64) (ok bool, err error) { 18 | // Prepare the run script 19 | cli, er := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 20 | if er != nil { 21 | err = errors.Wrap(er, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 22 | return 23 | } 24 | 25 | insp, er := cli.ContainerInspect(ctx, w.containerID) 26 | 27 | // Prepare the execdir 28 | execdir := filepath.Join(w.WorkDir, "execdir") 29 | if _, err = os.Stat(execdir); os.IsNotExist(err) { 30 | os.Mkdir(execdir, DirPerm) 31 | } else { 32 | err = errors.Wrap(err, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 33 | return 34 | } 35 | 36 | // Link the file to execdir 37 | testcase_in := filepath.Join(w.WorkDir, fmt.Sprintf("testcase%03d.in", rank)) 38 | testcase_out := filepath.Join(w.WorkDir, fmt.Sprintf("testcase%03d.out", rank)) 39 | link_in := filepath.Join(execdir, "testcase.in") 40 | link_out := filepath.Join(execdir, "testcase.out") 41 | err = os.Link(testcase_in, link_in) 42 | if err != nil { 43 | err = errors.Wrap(err, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 44 | return 45 | } 46 | err = os.Link(testcase_out, link_out) 47 | if err != nil { 48 | err = errors.Wrap(err, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 49 | return 50 | } 51 | 52 | // Run testcase 53 | pid := insp.State.Pid 54 | //cmd = "/bin/bash -c run/run execdir/testcase.in execdir/program.out ./program 2> run.err; touch ./done.lck" 55 | cmd := "run/run execdir/testcase.in execdir/program.out ./program 2> run.err; touch ./done.lck" 56 | info, err := w.execcmd(ctx, cli, "root", cmd) 57 | if err != nil { 58 | err = errors.Wrap(err, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 59 | return 60 | } 61 | runinfo, er := w.runProtect(ctx, &insp, pid, uint64(w.JudgeInfo.TimeLimit), w.JudgeInfo.OutputLimit, "execdir/program.out") 62 | log.Debugf("run protect protecting %s", cmd) 63 | log.Infof("run protect [run] done, runinfo %+v", runinfo) 64 | if er != nil { 65 | err = errors.Wrap(err, fmt.Sprintf("Run error on Run#%d case %d", w.JudgeInfo.SubmitID, rank)) 66 | return 67 | } 68 | log.Debugf("Testcase run done, info %+v", runinfo) 69 | 70 | // Report the result if run error 71 | res := config.RunResult{} 72 | res.RunTime = float64(runinfo.usedtime) * 1.0 / 1000 / 1000 / 1000 73 | res.TestcaseID = tid 74 | res.JudgingID = w.JudgeInfo.JudgingID 75 | 76 | // Parse system meta and send to output system 77 | // For Domjudge compability 78 | 79 | // Make the systemMeta look like this = = 80 | /* 81 | Timelimit exceeded. 82 | runtime: 1.860s cpu, 2.200s wall 83 | memory used: 131072 bytes 84 | */ 85 | res.OutputSystem = fmt.Sprintf("%s.\nruntime: %fs cpu, %fs wall:\nmemory used: %dbytes\n", res.RunResult, res.RunTime, res.RunTime, runinfo.usedmem) 86 | log.Debugf("system meta %s", res.OutputSystem) 87 | // Save for Judge use 88 | ioutil.WriteFile(filepath.Join(execdir, "program.meta"), []byte(res.OutputSystem), FilePerm) 89 | 90 | res.RunResult = "" 91 | if runinfo.timeexceed { 92 | res.RunResult = config.ResTLE 93 | } 94 | if runinfo.memexceed { 95 | res.RunResult = config.ResRE 96 | } 97 | if runinfo.outputexceed { 98 | res.RunResult = config.ResRE 99 | } 100 | // If not these error, then it is runtime error 101 | if info.ExitCode != 0 { 102 | res.RunResult = config.ResRE 103 | reinfo, er := ioutil.ReadFile(filepath.Join(w.WorkDir, "run.err")) 104 | if er != nil { 105 | err = errors.Wrap(er, "run error") 106 | return 107 | } 108 | res.OutputError = fmt.Sprintf("%s", reinfo) 109 | } 110 | 111 | // Run error, post to Server 112 | if res.RunResult != "" { 113 | err = request.PostResult(ctx, res) 114 | if err != nil { 115 | err = errors.Wrap(err, "run error") 116 | return 117 | } 118 | ok = false 119 | return 120 | } 121 | ok = true 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /downloader/downloader.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "encoding/base64" 7 | "fmt" 8 | log "github.com/Sirupsen/logrus" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/VOID001/D-judge/config" 15 | "github.com/VOID001/D-judge/request" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | var apiMap = map[string]string{ 20 | "testcase": "/testcase_files?testcaseid=%s&%%s", // Seems strange but works! 21 | "executable": "/executable?execid=%s", 22 | "code": "/submission_files?id=%s", 23 | } 24 | 25 | type Downloader struct { 26 | FileType string 27 | FileName string 28 | MD5 string 29 | Destination string 30 | UseCache bool 31 | Params []string 32 | SkipMD5Check bool 33 | } 34 | 35 | const ( 36 | DirPerm = 0755 37 | FilePerm = 0644 38 | CacheContent = "content" 39 | CacheChecksum = "checksum" 40 | ) 41 | 42 | func cleanupcache() (err error) { 43 | err = os.RemoveAll(config.GlobalConfig.CacheRoot) 44 | if err != nil { 45 | err = errors.Wrap(err, "error clean up cache") 46 | return 47 | } 48 | err = os.Mkdir(config.GlobalConfig.CacheRoot, DirPerm) 49 | if err != nil { 50 | err = errors.Wrap(err, "error clean up cache") 51 | return 52 | } 53 | return 54 | } 55 | 56 | func (d *Downloader) Do(ctx context.Context) (err error) { 57 | var content string 58 | url := apiMap[d.FileType] 59 | for i := 0; i < len(d.Params); i++ { 60 | url = fmt.Sprintf(url, d.Params[i]) 61 | log.Debugf("url = %s", url) 62 | } 63 | 64 | hit := d.UseCache 65 | if d.UseCache { 66 | // All errors when lookup cache is not fatal, just fallback to no cache mode 67 | path, er := lookupcache(d.FileName, d.MD5) 68 | if er != nil { 69 | err = errors.Wrap(er, fmt.Sprintf("error processing download, downloader info %+v", d)) 70 | log.Error(err) 71 | log.Infof("Fall back to no cache mode") 72 | hit = false 73 | } 74 | 75 | // Cached data found, return now 76 | if hit && path != "" { 77 | os.Link(path, d.Destination) 78 | return 79 | } 80 | } 81 | 82 | switch d.FileType { 83 | case "code": 84 | // Provide the code name 85 | m := []map[string]string{} 86 | err = request.Do(ctx, http.MethodGet, url, nil, "", &m) 87 | if err != nil { 88 | err = errors.Wrap(err, "error processing download") 89 | return 90 | } 91 | content = m[0]["content"] 92 | d.Destination = filepath.Join(filepath.Dir(d.Destination), m[0]["filename"]) 93 | d.FileName = m[0]["filename"] 94 | break 95 | default: 96 | err = request.Do(ctx, http.MethodGet, url, nil, "", &content) 97 | if err != nil { 98 | err = errors.Wrap(err, "error processing download") 99 | return 100 | } 101 | break 102 | } 103 | 104 | // Decode the base64 data 105 | data, er := base64.StdEncoding.DecodeString(content) 106 | if er != nil { 107 | er = errors.Wrap(er, "error processing download") 108 | } 109 | // Check MD5 110 | if !d.SkipMD5Check { 111 | checksum := md5.Sum(data) 112 | log.Debugf("checksum = %x, d.MD5 = %s", checksum, d.MD5) 113 | if fmt.Sprintf("%x", checksum) != d.MD5 { 114 | err = errors.New("error processing download: checksum error, file corrupted during download") 115 | return 116 | } 117 | } 118 | 119 | if d.SkipMD5Check { 120 | log.Debugf("MD5 checksum skipped") 121 | } 122 | 123 | err = ioutil.WriteFile(d.Destination, data, FilePerm) 124 | if err != nil { 125 | err = errors.Wrap(err, "error processing download") 126 | } 127 | 128 | // Save cache errors is not fatal 129 | if d.UseCache && !hit { 130 | log.Debugf("Cache not hit") 131 | os.Mkdir(filepath.Join(config.GlobalConfig.CacheRoot, d.FileName), DirPerm) 132 | cachedata := filepath.Join(config.GlobalConfig.CacheRoot, d.FileName, CacheContent) 133 | err = os.Link(d.Destination, cachedata) 134 | if err != nil { 135 | log.Errorf("save into cache failed, error %+v", err) 136 | } 137 | cachemd5 := filepath.Join(config.GlobalConfig.CacheRoot, d.FileName, CacheChecksum) 138 | err = ioutil.WriteFile(cachemd5, []byte(d.MD5), FilePerm) 139 | if err != nil { 140 | log.Errorf("save into cache failed, error %+v", err) 141 | } 142 | err = nil 143 | } 144 | return 145 | } 146 | 147 | func lookupcache(name string, md5sum string) (path string, err error) { 148 | log.Debugf("lookupcache(name = %s, md5sum = %s)", name, md5sum) 149 | look := filepath.Join(config.GlobalConfig.CacheRoot, name) 150 | info, er := os.Stat(look) 151 | if er != nil { 152 | err = errors.Wrap(er, "error lookup cache") 153 | return 154 | } 155 | 156 | // Cache Should be a directory 157 | if !info.IsDir() { 158 | err = errors.New("error lookup cache: path is not a dir") 159 | } 160 | look = filepath.Join(look, "content") 161 | file, er := os.Open(look) 162 | if er != nil { 163 | err = errors.Wrap(er, "error lookup cache") 164 | return 165 | } 166 | defer file.Close() 167 | 168 | oldfile, er := ioutil.ReadFile(look) 169 | 170 | if er != nil { 171 | err = errors.Wrap(er, "error lookup cache") 172 | return 173 | } 174 | oldmd5 := md5.Sum(oldfile) 175 | 176 | if fmt.Sprintf("%x", oldmd5) != md5sum { 177 | err = errors.New("error lookup cache, md5sum do not match") 178 | return 179 | } 180 | path = filepath.Join(config.GlobalConfig.CacheRoot, name, CacheContent) 181 | return 182 | } 183 | -------------------------------------------------------------------------------- /request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | "github.com/VOID001/D-judge/config" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | const ( 19 | TypeForm = "application/x-www-form-urlencoded" 20 | TypeJSON = "application/json" 21 | ) 22 | 23 | func Do(ctx context.Context, method string, URL string, data interface{}, ctype string, respdata interface{}) (err error) { 24 | log.Debugf("Do(%v %v %v %v %v %v)", ctx, method, URL, data, ctype, respdata) 25 | req := new(http.Request) 26 | URL = config.GlobalConfig.EndpointURL + URL 27 | log.Debugf("stared request method=%s URL=%s", method, URL) 28 | cli := &http.Client{} 29 | buf := bytes.Buffer{} 30 | enc := json.NewEncoder(&buf) 31 | 32 | // Get should not have body 33 | if method != http.MethodGet && data != nil { 34 | if ctype == TypeForm { 35 | if formdata, ok := data.(url.Values); ok { 36 | req, err = http.NewRequest(method, URL, bytes.NewBufferString(formdata.Encode())) 37 | if err != nil { 38 | err = errors.Wrap(err, "do request error") 39 | return 40 | } 41 | } else { 42 | err = errors.New(fmt.Sprintf("do request error: data invaid type %T", data)) 43 | return 44 | } 45 | } else if ctype == TypeJSON { 46 | enc.Encode(data) 47 | req, err = http.NewRequest(method, URL, &buf) 48 | } else { 49 | err = errors.New(fmt.Sprintf("do request error unsupported content-type %s", ctype)) 50 | return err 51 | } 52 | req.Header.Add("Content-Type", ctype) 53 | } else { 54 | req, err = http.NewRequest(method, URL, nil) 55 | } 56 | 57 | req.Header.Add("X-Djudge-Hostname", config.GlobalConfig.HostName) 58 | req.SetBasicAuth(config.GlobalConfig.EndpointUser, config.GlobalConfig.EndpointPassword) 59 | 60 | resp, err := cli.Do(req) 61 | log.Debugf("request header is %+v", req.Header) 62 | if err != nil { 63 | err = errors.Wrap(err, fmt.Sprintf("request error method=%s URL=%s", method, URL)) 64 | return 65 | } 66 | defer resp.Body.Close() 67 | 68 | tmpbuf := bytes.Buffer{} 69 | dec := json.NewDecoder(resp.Body) 70 | 71 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 72 | tmpbuf.ReadFrom(resp.Body) 73 | err = errors.New(fmt.Sprintf("request error status code %d data\n %s", resp.StatusCode, tmpbuf.String())) 74 | return 75 | } 76 | log.Debugf("Response Header %+v", resp.Header) 77 | log.Debugf("Response Header %s", tmpbuf.String()) 78 | 79 | if respdata != nil { 80 | err = dec.Decode(&respdata) 81 | 82 | if err == io.EOF { 83 | err = nil 84 | respdata = nil 85 | return 86 | } 87 | 88 | if err != nil { 89 | err = errors.Wrap(err, "json decode error") 90 | return 91 | } 92 | log.Debugf("Decoded data %+v", respdata) 93 | } 94 | 95 | log.Debugf("done request method=%s URL=%s", method, URL) 96 | return 97 | } 98 | 99 | func JudgeError(ctx context.Context, errMsg error, jid int64) { 100 | info := make(url.Values) 101 | 102 | // Encode error to base64 string 103 | 104 | // For backward(domjudge) compability, set judge error as compile error 105 | data := base64.StdEncoding.EncodeToString([]byte(errMsg.Error())) 106 | info["compile_success"] = []string{"0"} 107 | info["output_compile"] = []string{data} 108 | info["judgehost"] = []string{config.GlobalConfig.HostName} 109 | 110 | err := Do(ctx, http.MethodPut, fmt.Sprintf("/judgings/%d", jid), info, TypeForm, nil) 111 | if err != nil { 112 | err = errors.Wrap(err, "put Judging Errors error") 113 | log.Error(err) 114 | } 115 | return 116 | } 117 | 118 | func CompileError(ctx context.Context, compileErr error, jid int64) (err error) { 119 | info := make(url.Values) 120 | 121 | // Encode error to base64 string 122 | 123 | // For backward(domjudge) compability, set judge error as compile error 124 | data := base64.StdEncoding.EncodeToString([]byte(compileErr.Error())) 125 | info["compile_success"] = []string{"0"} 126 | info["output_compile"] = []string{data} 127 | info["judgehost"] = []string{config.GlobalConfig.HostName} 128 | 129 | err = Do(ctx, http.MethodPut, fmt.Sprintf("/judgings/%d", jid), info, TypeForm, nil) 130 | if err != nil { 131 | err = errors.Wrap(err, "put Compile Errors error") 132 | return 133 | } 134 | return 135 | 136 | } 137 | 138 | func CompileOK(ctx context.Context, jid int64) (err error) { 139 | info := make(url.Values) 140 | 141 | info["compile_success"] = []string{"1"} 142 | info["output_compile"] = []string{""} 143 | info["judgehost"] = []string{config.GlobalConfig.HostName} 144 | 145 | err = Do(ctx, http.MethodPut, fmt.Sprintf("/judgings/%d", jid), info, TypeForm, nil) 146 | if err != nil { 147 | err = errors.Wrap(err, "put Compile OK error") 148 | return 149 | } 150 | return 151 | 152 | } 153 | 154 | func PostResult(ctx context.Context, result config.RunResult) (err error) { 155 | info := make(url.Values) 156 | 157 | info["judgingid"] = []string{fmt.Sprintf("%d", result.JudgingID)} 158 | info["testcaseid"] = []string{fmt.Sprintf("%d", result.TestcaseID)} 159 | info["runresult"] = []string{result.RunResult} 160 | info["runtime"] = []string{fmt.Sprintf("%lf", result.RunTime)} 161 | info["judgehost"] = []string{config.GlobalConfig.HostName} 162 | info["output_run"] = []string{base64.StdEncoding.EncodeToString([]byte(result.OutputRun))} 163 | info["output_error"] = []string{base64.StdEncoding.EncodeToString([]byte(result.OutputError))} 164 | info["output_system"] = []string{base64.StdEncoding.EncodeToString([]byte(result.OutputSystem))} 165 | info["output_diff"] = []string{base64.StdEncoding.EncodeToString([]byte(result.OutputDiff))} 166 | 167 | err = Do(ctx, http.MethodPost, "/judging_runs", info, TypeForm, nil) 168 | if err != nil { 169 | err = errors.Wrap(err, "Post result error") 170 | return 171 | } 172 | return 173 | } 174 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "math/rand" 8 | "runtime" 9 | "time" 10 | 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "path/filepath" 16 | 17 | "github.com/BurntSushi/toml" 18 | log "github.com/Sirupsen/logrus" 19 | "github.com/VOID001/D-judge/config" 20 | "github.com/VOID001/D-judge/judge-controller" 21 | "github.com/VOID001/D-judge/request" 22 | 23 | "github.com/pkg/errors" 24 | ) 25 | 26 | // Log level constant 27 | const ( 28 | INFO = 0 // INFO Level 29 | WARN = 1 // WARN Level 30 | DEBUG = 2 // DEBUG Level 31 | DirPerm = 0744 // Dir Permission 32 | ) 33 | 34 | // Error constants 35 | const ( 36 | ErrNoDir = "no such file or directory" 37 | ErrNoFile = "no such file or directory" 38 | ) 39 | 40 | var path string 41 | var debuglv int64 42 | 43 | // GlobalConfig Config Object contain the global system config 44 | var GlobalConfig config.SystemConfig 45 | 46 | func init() { 47 | var logfile string 48 | flag.StringVar(&path, "c", "config.toml", "select configuration file") 49 | flag.Int64Var(&debuglv, "d", 0, "debug mode enabled") 50 | flag.StringVar(&logfile, "log", "/dev/stdout", "log file") 51 | flag.Parse() 52 | _, err := toml.DecodeFile(path, &GlobalConfig) 53 | if err != nil { 54 | err = errors.Wrap(err, "Processing config file error") 55 | log.Fatal(err) 56 | } 57 | cwd, err := os.Getwd() 58 | if err != nil { 59 | err = errors.Wrap(err, "Get current directory error") 60 | } 61 | if !filepath.IsAbs(GlobalConfig.JudgeRoot) { 62 | GlobalConfig.JudgeRoot = filepath.Join(cwd, GlobalConfig.JudgeRoot) 63 | } 64 | if !filepath.IsAbs(GlobalConfig.CacheRoot) { 65 | GlobalConfig.CacheRoot = filepath.Join(cwd, GlobalConfig.CacheRoot) 66 | } 67 | if debuglv == INFO { 68 | log.SetLevel(log.InfoLevel) 69 | } 70 | if debuglv == WARN { 71 | log.SetLevel(log.WarnLevel) 72 | } 73 | if debuglv == DEBUG { 74 | log.SetLevel(log.DebugLevel) 75 | } 76 | f, _ := os.Create(logfile) 77 | log.SetOutput(f) 78 | log.SetFormatter(&log.JSONFormatter{}) 79 | config.GlobalConfig = GlobalConfig 80 | } 81 | 82 | func main() { 83 | log.Debugf("Settings %+v", GlobalConfig) 84 | // Perform Sanity Check 85 | log.Infof("sanity check start") 86 | err := sanityCheckDir(GlobalConfig.JudgeRoot) 87 | if err != nil { 88 | err = errors.Wrap(err, "sanity check dir judgeroot error") 89 | log.Fatal(err) 90 | } 91 | err = sanityCheckDir(GlobalConfig.CacheRoot) 92 | if err != nil { 93 | err = errors.Wrap(err, "sanity check dir cacheroot error") 94 | log.Fatal(err) 95 | } 96 | err = sanityCheckConnection(GlobalConfig.EndpointURL) 97 | if err != nil { 98 | err = errors.Wrap(err, "sanity check connection error") 99 | log.Fatal(err) 100 | } 101 | err = sanityCheckDocker() 102 | if err != nil { 103 | err = errors.Wrap(err, "sanity check docker error") 104 | log.Fatal(err) 105 | } 106 | 107 | // Error When Requesting Judgehost 108 | err = request.Do(context.Background(), http.MethodPost, "/judgehosts", url.Values{"hostname": {config.GlobalConfig.HostName}}, request.TypeForm, nil) 109 | if err != nil { 110 | err = errors.Wrap(err, "main loop error") 111 | log.Fatal(err) 112 | } 113 | log.Infof("sanity check success") 114 | 115 | // PerformRequest Lifcycle 116 | daemon := controller.Daemon{} 117 | daemon.MaxWorker = runtime.NumCPU() 118 | daemon.Run(context.Background()) 119 | for { 120 | jinfo := config.JudgeInfo{} 121 | // Request For Judge 122 | err = request.Do(context.Background(), http.MethodPost, fmt.Sprintf("/judgings?judgehost=%s", config.GlobalConfig.HostName), nil, "", &jinfo) 123 | if err != nil { 124 | log.Warn(err) 125 | } 126 | log.Debugf("Judge Info %+v", jinfo) 127 | if jinfo.SubmitID != 0 { 128 | log.Infof("Fetched Submission ID #%d", jinfo.SubmitID) 129 | workDir := fmt.Sprintf("%s/c%d-s%d-j%d", config.GlobalConfig.JudgeRoot, jinfo.ContestID, jinfo.SubmitID, jinfo.JudgingID) 130 | if _, err := os.Stat(workDir); err == nil { 131 | oldWorkDir := fmt.Sprintf("%s-old-%d", workDir, time.Now().Unix()) 132 | log.Infof("Found stale working directory, rename to %s", oldWorkDir) 133 | err := os.Rename(workDir, oldWorkDir) 134 | if err != nil { 135 | err = errors.Wrap(err, "main loop error") 136 | log.Fatal(err) 137 | } 138 | } 139 | os.Mkdir(workDir, DirPerm) 140 | daemon.AddTask(context.Background(), jinfo, workDir, config.GlobalConfig.DockerImage) 141 | } 142 | time.Sleep(time.Duration(rand.Intn(2500)) * time.Millisecond) 143 | } 144 | } 145 | 146 | func sanityCheckDir(dir string) (err error) { 147 | _, err = ioutil.ReadDir(dir) 148 | if err != nil && os.IsNotExist(err) { 149 | err = os.Mkdir(dir, DirPerm) 150 | if err != nil { 151 | err = errors.Wrap(err, fmt.Sprintf("cannot make %s", dir)) 152 | return 153 | } 154 | log.Infof("created dir %s with mode %04o", dir, DirPerm) 155 | } 156 | info, err := os.Stat(dir) 157 | log.Infof("dir %s mode bits %s", dir, info.Mode()) 158 | return 159 | } 160 | 161 | func sanityCheckConnection(endpoint string) (err error) { 162 | req, err := http.NewRequest(http.MethodPost, endpoint, nil) 163 | req.SetBasicAuth(config.GlobalConfig.EndpointUser, config.GlobalConfig.EndpointPassword) 164 | if err != nil { 165 | err = errors.Wrap(err, fmt.Sprintf("cannot create request %s", endpoint)) 166 | return 167 | } 168 | cli := http.Client{} 169 | resp, err := cli.Do(req) 170 | if err != nil { 171 | err = errors.Wrap(err, fmt.Sprintf("cannot connect to %s", endpoint)) 172 | return 173 | } 174 | defer func() { 175 | err := resp.Body.Close() 176 | if err != nil { 177 | log.Errorf("body close error %s", err.Error()) 178 | } 179 | }() 180 | return 181 | } 182 | 183 | func sanityCheckDocker() (err error) { 184 | err = controller.Ping(context.Background()) 185 | if err != nil { 186 | err = errors.Wrap(err, "docker Ping error") 187 | } 188 | return 189 | } 190 | -------------------------------------------------------------------------------- /judge-controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "runtime" 9 | "sync" 10 | 11 | "github.com/VOID001/D-judge/config" 12 | "github.com/VOID001/D-judge/downloader" 13 | "github.com/VOID001/D-judge/request" 14 | 15 | log "github.com/Sirupsen/logrus" 16 | "github.com/docker/engine-api/client" 17 | "github.com/pkg/errors" 18 | //"github.com/docker/engine-api/types" 19 | //"github.com/docker/engine-api/types/container" 20 | "net/http" 21 | ) 22 | 23 | var mu sync.Mutex 24 | var cpuMap []bool 25 | var cpuCount int 26 | 27 | const ( 28 | ErrMaxWorkerExceed = "max worker exceed" 29 | ) 30 | 31 | type Daemon struct { 32 | MaxWorker int 33 | CurrentWorker int 34 | WorkerState []string 35 | workerChan chan Worker 36 | resultChan chan RunResult 37 | } 38 | 39 | type RunResult struct { 40 | Stage string 41 | RunID int 42 | err error 43 | } 44 | 45 | var httpcli http.Client 46 | 47 | func init() { 48 | cpuCount = runtime.NumCPU() 49 | cpuMap = make([]bool, cpuCount) 50 | httpcli = http.Client{} 51 | } 52 | 53 | func Ping(ctx context.Context) (err error) { 54 | cli, err := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 55 | if err != nil { 56 | err = errors.Wrap(err, "create docker client error") 57 | return err 58 | } 59 | _, err = cli.Info(ctx) 60 | if err != nil { 61 | err = errors.Wrap(err, "ping docker server error") 62 | return err 63 | } 64 | return 65 | } 66 | 67 | func (d *Daemon) AddTask(ctx context.Context, jinfo config.JudgeInfo, dir string, img string) (err error) { 68 | log.Debugf("call AddTask(context, jinfo = %+v, dir = %+v, img = %+v)", jinfo, dir, img) 69 | w := Worker{} 70 | w.JudgeInfo = jinfo 71 | w.WorkDir = dir 72 | w.RunUser = "root" 73 | w.DockerImage = img 74 | d.workerChan <- w 75 | return 76 | } 77 | 78 | func (d *Daemon) Run(ctx context.Context) { 79 | d.workerChan = make(chan Worker, 100) 80 | for i := 0; i < d.MaxWorker; i++ { 81 | go d.run(ctx, i) 82 | } 83 | return 84 | } 85 | 86 | func (d *Daemon) run(ctx context.Context, cpuid int) { 87 | for { 88 | // Only Judge Error Will Processed here, other error will process 89 | // in the worker function 90 | 91 | if w, ok := <-d.workerChan; ok { 92 | log.Infof("Started Judging RunID #%d, running on CPU %d", w.JudgeInfo.SubmitID, cpuid) 93 | w.CPUID = cpuid 94 | err := w.prepare(ctx) 95 | if err != nil { 96 | log.Error(err) 97 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 98 | continue // Future will change to continue 99 | } 100 | log.Infof("RunID #%d prepare OK", w.JudgeInfo.SubmitID) 101 | ok, err := w.build(ctx) 102 | if err != nil { 103 | w.cleanup(ctx) 104 | log.Error(err) 105 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 106 | continue 107 | } 108 | // Compile Error, stop the current test 109 | if !ok { 110 | w.cleanup(ctx) 111 | continue 112 | } 113 | log.Infof("RunID #%d compile OK", w.JudgeInfo.SubmitID) 114 | for { 115 | // Request for testcase 116 | tinfo := config.TestcaseInfo{} 117 | 118 | err = request.Do(ctx, http.MethodGet, fmt.Sprintf("/testcases?judgingid=%d", w.JudgeInfo.SubmitID), nil, "", &tinfo) 119 | if err != nil { 120 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 121 | break 122 | // Return Judge Error 123 | } 124 | if tinfo.TestcaseID == 0 { 125 | break 126 | } 127 | log.Debugf("Testcase info %+v", tinfo) 128 | 129 | dl := downloader.Downloader{} 130 | dl.FileType = "testcase" 131 | dl.Destination = filepath.Join(w.WorkDir, fmt.Sprintf("testcase%03d.in", tinfo.Rank)) 132 | dl.FileName = fmt.Sprintf("%d-%s.in", tinfo.TestcaseID, tinfo.MD5SumInput) 133 | dl.SkipMD5Check = false 134 | dl.MD5 = tinfo.MD5SumInput 135 | dl.UseCache = true 136 | dl.Params = []string{fmt.Sprintf("%d", tinfo.TestcaseID), "input"} 137 | err = dl.Do(ctx) 138 | if err != nil { 139 | err = errors.Wrap(err, "worker error: downloading testcase error") 140 | log.Error(err) 141 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 142 | break 143 | // Return Judge Error 144 | } 145 | 146 | dl.Destination = filepath.Join(w.WorkDir, fmt.Sprintf("testcase%03d.out", tinfo.Rank)) 147 | dl.FileName = fmt.Sprintf("%d-%s.out", tinfo.TestcaseID, tinfo.MD5SumInput) 148 | dl.MD5 = tinfo.MD5SumOutput 149 | dl.Params = []string{fmt.Sprintf("%d", tinfo.TestcaseID), "output"} 150 | err = dl.Do(ctx) 151 | if err != nil { 152 | err = errors.Wrap(err, "worker error: downloading testcase error") 153 | log.Error(err) 154 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 155 | break 156 | } 157 | 158 | // Run testcase 159 | ok, err = w.run(ctx, tinfo.Rank, tinfo.TestcaseID) 160 | if err != nil { 161 | w.cleanup(ctx) 162 | err = errors.Wrap(err, "worker error") 163 | log.Error(err) 164 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 165 | break 166 | } 167 | if !ok { 168 | break 169 | } 170 | log.Infof("Run Testcase %d OK", tinfo.Rank) 171 | 172 | // Judge testcase 173 | err = w.judge(ctx, tinfo.Rank, tinfo.TestcaseID) 174 | if err != nil { 175 | w.cleanup(ctx) 176 | err = errors.Wrap(err, "worker error") 177 | log.Error(err) 178 | request.JudgeError(ctx, err, w.JudgeInfo.JudgingID) 179 | break 180 | } 181 | log.Infof("Judge Testcase %d OK", tinfo.Rank) 182 | 183 | } 184 | err = w.cleanup(ctx) 185 | if err != nil { 186 | log.Error(err) 187 | continue 188 | } 189 | } else { 190 | break 191 | } 192 | } 193 | return 194 | } 195 | 196 | func GetAvailableCPU(ctx context.Context) (cpuid int, err error) { 197 | for i := 0; i < cpuCount; i++ { 198 | if cpuMap[i] != true { 199 | mu.Lock() 200 | cpuMap[i] = true 201 | cpuid = i 202 | defer mu.Unlock() 203 | return 204 | } 205 | } 206 | cpuid = -1 207 | return 208 | } 209 | -------------------------------------------------------------------------------- /judge-controller/guard.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | // Internal Run Guard Module 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | "github.com/docker/engine-api/client" 15 | "github.com/docker/engine-api/types" 16 | "github.com/fsnotify/fsnotify" 17 | "github.com/pkg/errors" 18 | "github.com/shirou/gopsutil/process" 19 | ) 20 | 21 | func (w *Worker) runProtect(ctx context.Context, insp *types.ContainerJSON, pid int, timelim uint64, outputlim int64, outputfile string) (info runinfo, err error) { 22 | starttime := time.Now().UnixNano() 23 | curtime := time.Now().UnixNano() 24 | 25 | // Use run protect to protect the running instance 26 | // It will start right after the execmd =w= 27 | info.outputexceed = false 28 | info.usedmem = 0 29 | info.usedtime = 0 30 | info.timeexceed = false 31 | p, err := process.NewProcess(int32(pid)) 32 | if err != nil { 33 | err = errors.Wrap(err, "run protect error: cannot attach process") 34 | return 35 | } 36 | m, err := p.MemoryInfoEx() 37 | if err != nil { 38 | err = errors.Wrap(err, "run protect error: cannot get memory info") 39 | } 40 | var f os.FileInfo 41 | if outputfile != "" { 42 | f, err = os.Stat(outputfile) 43 | if err != nil && !os.IsNotExist(err) { 44 | err = errors.Wrap(err, "run protect error: cannot get output file") 45 | return 46 | } 47 | } 48 | wt, er := fsnotify.NewWatcher() 49 | if er != nil { 50 | err = errors.Wrap(er, "run protect error: cannot create watcher") 51 | return 52 | } 53 | defer wt.Close() 54 | err = wt.Add(w.WorkDir) 55 | if err != nil { 56 | err = errors.Wrap(err, "run protect error: cannot add watchpoint") 57 | return 58 | } 59 | log.Debugf("Add watch to %s", w.WorkDir) 60 | Loop: 61 | for { 62 | select { 63 | case ev := <-wt.Events: 64 | log.Debugf("%s", ev.String()) 65 | if ev.Op == fsnotify.Create && strings.HasSuffix(ev.Name, "done.lck") { 66 | curtime = time.Now().UnixNano() 67 | break Loop 68 | } 69 | default: 70 | curtime = time.Now().UnixNano() 71 | // Only when output file is not empty, check the file size 72 | if outputfile != "" { 73 | f, err = os.Stat(outputfile) 74 | if err != nil && !os.IsNotExist(err) { 75 | err = errors.Wrap(err, "run protect error: cannot stat outputfile") 76 | break Loop 77 | } 78 | // Output Limit exceed 79 | if err == nil && f.Size() > outputlim { 80 | info.outputexceed = true 81 | break Loop 82 | } 83 | } 84 | // Collect memory used 85 | if info.usedmem < m.Dirty { 86 | info.usedmem = m.Dirty 87 | } 88 | // Time limit exceed 89 | if curtime-starttime > int64(timelim*1000000000) { 90 | info.timeexceed = true 91 | log.Debugf("Program exceed hard time limit(used %d, hardlim %d), terminated now", curtime-starttime, timelim*1000000000) 92 | // Killed the program 93 | err = p.Terminate() 94 | if err != nil { 95 | p.Kill() 96 | } 97 | break Loop 98 | } 99 | // done.lck create too quick, then just get it and exit 100 | _, err = os.Stat(filepath.Join(w.WorkDir, "done.lck")) 101 | if err == nil { 102 | break Loop 103 | } 104 | 105 | } 106 | } 107 | info.usedtime = curtime - starttime 108 | info.memexceed = insp.State.OOMKilled 109 | err = os.RemoveAll(filepath.Join(w.WorkDir, "done.lck")) 110 | if err != nil { 111 | err = errors.Wrap(err, "cannot remove done.lck [ABORT!]") 112 | return 113 | } 114 | return 115 | } 116 | 117 | func (w *Worker) execcmd(ctx context.Context, cli *client.Client, user string, cmd string) (info types.ContainerExecInspect, err error) { 118 | ec := types.ExecConfig{} 119 | ec.Detach = true 120 | ec.Tty = false 121 | ec.AttachStdout = true // Set this to true will make the command block until finish 122 | ec.Cmd = make([]string, 3) 123 | ec.Cmd[0] = "/bin/bash" 124 | ec.Cmd[1] = "-c" 125 | ec.Cmd[2] = cmd 126 | log.Infof("%+v", ec) 127 | ec.User = user 128 | eresp, er := cli.ContainerExecCreate(ctx, w.containerID, ec) 129 | if er != nil { 130 | err = errors.Wrap(er, "exec command in container error") 131 | return 132 | } 133 | sc := types.ExecStartCheck{} 134 | sc.Tty = ec.Tty 135 | sc.Detach = ec.Detach 136 | log.Infof("ExecStartCheck %+v", sc) 137 | err = cli.ContainerExecStart(ctx, eresp.ID, sc) 138 | if err != nil { 139 | err = errors.Wrap(err, "exec command in container error") 140 | return 141 | } 142 | log.Debugf("Executing exec ID = %s", eresp.ID) 143 | info, err = cli.ContainerExecInspect(ctx, eresp.ID) 144 | if err != nil { 145 | err = errors.Wrap(err, "exec command in container error") 146 | return 147 | } 148 | log.Infof("ONLY FOR CURRENT DEBUG info.ExitCode = %d", info.ExitCode) 149 | return 150 | } 151 | 152 | func (w *Worker) execcmdAttach(ctx context.Context, cli *client.Client, user string, cmd string) (info types.ContainerExecInspect, err error) { 153 | ec := types.ExecConfig{} 154 | ec.Detach = false 155 | ec.Tty = false 156 | ec.AttachStdout = true // Set this to true will make the command block until finish 157 | ec.Cmd = make([]string, 3) 158 | ec.Cmd[0] = "/bin/bash" 159 | ec.Cmd[1] = "-c" 160 | ec.Cmd[2] = cmd 161 | log.Infof("%+v", ec) 162 | ec.User = user 163 | eresp, er := cli.ContainerExecCreate(ctx, w.containerID, ec) 164 | if er != nil { 165 | err = errors.Wrap(er, "exec command in container error") 166 | return 167 | } 168 | sc := types.ExecStartCheck{} 169 | sc.Tty = ec.Tty 170 | sc.Detach = ec.Detach 171 | log.Infof("ExecStartCheck %+v", sc) 172 | err = cli.ContainerExecStart(ctx, eresp.ID, sc) 173 | if err != nil { 174 | err = errors.Wrap(err, "exec command in container error") 175 | return 176 | } 177 | insp, err := cli.ContainerExecAttach(ctx, eresp.ID, ec) 178 | if err != nil { 179 | err = errors.Wrap(err, "exec command in container error") 180 | } 181 | c := insp.Conn 182 | defer insp.Close() 183 | 184 | log.Debugf("Executing exec ID = %s", eresp.ID) 185 | one := make([]byte, 1) 186 | _, err = c.Read(one) 187 | if err == io.EOF { 188 | log.Infof("Connection Closed") 189 | } 190 | info, err = cli.ContainerExecInspect(ctx, eresp.ID) 191 | if err != nil { 192 | err = errors.Wrap(err, "exec command in container error") 193 | return 194 | } 195 | return 196 | } 197 | -------------------------------------------------------------------------------- /judge-controller/build.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/VOID001/D-judge/config" 12 | "github.com/VOID001/D-judge/request" 13 | "github.com/docker/engine-api/client" 14 | "github.com/docker/engine-api/types" 15 | "github.com/docker/engine-api/types/container" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | func (w *Worker) build(ctx context.Context) (ok bool, err error) { 20 | // Start the container and Build the target 21 | cli, er := client.NewClient(config.GlobalConfig.DockerServer, config.GlobalConfig.DockerVersion, nil, nil) 22 | if er != nil { 23 | err = errors.Wrap(er, fmt.Sprintf("Build error on Run#%d", w.JudgeInfo.SubmitID)) 24 | return 25 | } 26 | 27 | // log.Infof("MARK") 28 | cfg := container.Config{} 29 | cfg.Image = config.GlobalConfig.DockerImage 30 | cfg.WorkingDir = filepath.Join("/sandbox") 31 | cfg.User = "root" // Future will change to judge, a low-privileged user 32 | cfg.Tty = true 33 | cfg.AttachStdin = false 34 | cfg.AttachStderr = false 35 | cfg.AttachStdout = false 36 | cfg.Cmd = []string{"/bin/bash"} 37 | hcfg := container.HostConfig{} 38 | hcfg.Binds = []string{fmt.Sprintf("%s:%s", w.WorkDir, SandboxRoot)} 39 | log.Infof("Binds %s", fmt.Sprintf("%s:%s", w.WorkDir, SandboxRoot)) 40 | hcfg.CpusetCpus = fmt.Sprintf("%d", w.CPUID) 41 | hcfg.Memory = config.GlobalConfig.RootMemory 42 | hcfg.PidsLimit = 64 // This is enough for almost all case 43 | 44 | resp, er := cli.ContainerCreate(ctx, &cfg, &hcfg, nil, "") 45 | if er != nil { 46 | err = errors.Wrap(er, fmt.Sprintf("Build error on Run#%d", w.JudgeInfo.SubmitID)) 47 | return 48 | } 49 | //defer cli.ContainerRemove(ctx, w.containerID, types.ContainerRemoveOptions{}) 50 | w.containerID = resp.ID 51 | log.Debugf("RunID #%d container create ID %s", w.JudgeInfo.SubmitID, w.containerID) 52 | err = cli.ContainerStart(ctx, w.containerID, types.ContainerStartOptions{}) 53 | if err != nil { 54 | err = errors.Wrap(err, fmt.Sprintf("Build error on Run#%d", w.JudgeInfo.SubmitID)) 55 | return 56 | } 57 | //cmd := fmt.Sprintf("bash -c unzip -o build/%s -d build", w.JudgeInfo.BuildZip) 58 | cmd := fmt.Sprintf("unzip -o build/%s -d build", w.JudgeInfo.BuildZip) 59 | log.Infof("container %s executing %s", w.containerID, cmd) 60 | info, err := w.execcmdAttach(ctx, cli, "root", cmd) 61 | if err != nil { 62 | err = errors.Wrap(err, "Build error") 63 | } 64 | if info.ExitCode != 0 { 65 | err = errors.New(fmt.Sprintf("Build error: RunID#%d exec command %+v return non-zero value %d", w.JudgeInfo.SubmitID, cmd, info.ExitCode)) 66 | return 67 | } 68 | 69 | //cmd = "bash -c build/build 2> build/build.err" 70 | cmd = "cd build; ./build 2> ./build.err" 71 | log.Infof("container %s executing %s", w.containerID, cmd) 72 | info, err = w.execcmd(ctx, cli, "root", cmd) 73 | if err != nil { 74 | err = errors.Wrap(err, "Build error") 75 | } 76 | if info.ExitCode != 0 { 77 | err = errors.New(fmt.Sprintf("Build error: exec command %+v return non-zero value %d", cmd, info.ExitCode)) 78 | return 79 | } 80 | 81 | // Build the run executable 82 | cmd = fmt.Sprintf("unzip -o run/%s -d run", w.JudgeInfo.RunZip) 83 | info, er = w.execcmdAttach(ctx, cli, "root", cmd) 84 | if er != nil { 85 | err = errors.Wrap(err, "Build error") 86 | return 87 | } 88 | if info.ExitCode != 0 { 89 | err = errors.New(fmt.Sprintf("Build error: exec command %+v return non-zero value %d", cmd, info.ExitCode)) 90 | return 91 | } 92 | 93 | //cmd = fmt.Sprintf("/bin/bash -c run/build 2> run/build.err") 94 | cmd = fmt.Sprintf("cd run; ./build 2> ./build.err") 95 | info, err = w.execcmdAttach(ctx, cli, "root", cmd) 96 | if err != nil { 97 | err = errors.Wrap(er, "Build error") 98 | return 99 | } 100 | if info.ExitCode != 0 { 101 | err = errors.New(fmt.Sprintf("Build error: exec command %+v return non-zero value %d", cmd, info.ExitCode)) 102 | return 103 | } 104 | 105 | // Build the compare executable 106 | // Build the judge script 107 | //cmd := fmt.Sprintf("/bin/bash -c unzip -o compare/%s -d compare", w.JudgeInfo.CompareZip) 108 | cmd = fmt.Sprintf("unzip -o compare/%s -d compare", w.JudgeInfo.CompareZip) 109 | log.Debugf("executing command %s", cmd) 110 | info, er = w.execcmdAttach(ctx, cli, "root", cmd) 111 | if er != nil { 112 | err = errors.Wrap(er, "Build error") 113 | return 114 | } 115 | if info.ExitCode != 0 { 116 | err = errors.New(fmt.Sprintf("Build error: exec command %+v return non-zero value %d", cmd, info.ExitCode)) 117 | } 118 | 119 | //cmd = fmt.Sprintf("/bin/bash -c cd compare; ./build 2> ./build.err") 120 | cmd = fmt.Sprintf("cd compare; ./build 2> ./build.err") 121 | log.Debugf("executing command %s", cmd) 122 | info, err = w.execcmdAttach(ctx, cli, "root", cmd) 123 | if err != nil { 124 | err = errors.Wrap(err, "Build error") 125 | return 126 | } 127 | if info.ExitCode != 0 { 128 | err = errors.New(fmt.Sprintf("Build error: exec command %+v return non-zero value %d", cmd, info.ExitCode)) 129 | } 130 | 131 | // Do the real compile 132 | insp, err := cli.ContainerInspect(ctx, w.containerID) 133 | if err != nil { 134 | err = errors.Wrap(err, "Build error: inspect container") 135 | return 136 | } 137 | pid := insp.State.Pid 138 | //cmd = fmt.Sprintf("bash -c build/run ./program DUMMY ./%s 2> ./compile.err > ./compile.out; touch ./done.lck", w.codeFileName) 139 | cmd = fmt.Sprintf("build/run ./program DUMMY ./%s 2> ./compile.err > ./compile.out; echo $? > exitcode; touch ./done.lck", w.codeFileName) 140 | log.Debugf("container %s executing %s", w.containerID, cmd) 141 | _, err = w.execcmd(ctx, cli, "root", cmd) 142 | if err != nil { 143 | err = errors.Wrap(err, "build error") 144 | return 145 | } 146 | log.Debugf("Protecting run %s", cmd) 147 | runinfo, er := w.runProtect(ctx, &insp, pid, uint64(30), w.JudgeInfo.OutputLimit, "") 148 | if er != nil { 149 | err = errors.Wrap(er, fmt.Sprintf("Build error on Run#%d", w.JudgeInfo.SubmitID)) 150 | return 151 | } 152 | log.Infof("run protect [build] exited, runinfo %+v", runinfo) 153 | if runinfo.timeexceed || runinfo.memexceed || runinfo.outputexceed { 154 | err = errors.New(fmt.Sprintf("Build Error, Quota exceed %+v", runinfo)) 155 | return 156 | } 157 | _, err = os.Stat(filepath.Join(w.WorkDir, "exitcode")) 158 | if err != nil { 159 | err = errors.Wrap(err, "build error") 160 | return 161 | } 162 | // Read exit code from file 163 | code, er := w.readExitCode(ctx) 164 | if er != nil { 165 | err = errors.Wrap(er, "build error") 166 | return 167 | } 168 | 169 | if code != 0 { 170 | data, er := ioutil.ReadFile(filepath.Join(w.WorkDir, "compile.err")) 171 | if er != nil { 172 | err = errors.Wrap(er, "build error") 173 | return 174 | } 175 | errMsg := fmt.Sprintf("build error: exec command %+v return non-zero value %d\nCompile Error Message\n-------------------------\n%s", cmd, info.ExitCode, data) 176 | log.Debugf("Run#%d Compile Error", w.JudgeInfo.SubmitID) 177 | log.Debugf("erorMsg %s", errMsg) 178 | // This means compile error 179 | err = request.CompileError(ctx, errors.New(errMsg), w.JudgeInfo.SubmitID) 180 | if err != nil { 181 | err = errors.Wrap(err, "build error") 182 | return 183 | } 184 | // Set error to nil 185 | err = nil 186 | ok = false 187 | return 188 | } 189 | err = request.CompileOK(ctx, w.JudgeInfo.SubmitID) 190 | if err != nil { 191 | err = errors.Wrap(err, "build error") 192 | return 193 | } 194 | ok = true 195 | return 196 | } 197 | --------------------------------------------------------------------------------