client | ||
cmd | ||
data | ||
file | ||
judger | ||
language | ||
pkg | ||
problem | ||
runner | ||
taskqueue | ||
.dockerignore | ||
.gitignore | ||
Dockerfile.exec | ||
go.mod | ||
go.sum | ||
LICENSE | ||
README.md |
go-judge
Under designing & development
The goal to to reimplement syzoj/judge-v3 in GO language using go-sandbox.
Planned Design
Executor Service Draft (under development)
A rest service to run program in restricted environment and it is basically a wrapper for pkg/envexec
to run single / multiple programs.
- /run POST execute program in the restricted environment
- /file GET list all cached file
- /file POST prepare a file in the executor service (in memory), returns fileId (can be referenced in /run parameter)
- /file/:fileId GET downloads file from executor service (in memory), returns file content
- /file/:fileId DELETE delete file specified by fileId
Install & Run Developing Server
Install GO 1.13+ from download
go get github.com/criyle/go-judge/cmd/executorserver
~/go/bin/executorserver # or executorserver if $(GOPATH)/bin is in your $PATH
Or, by docker
docker run -it --rm --privileged -p 5050:5050 criyle/executorserver:demo
Build by your own docker build -t executorserver -f Dockerfile.exec .
The executorserver
need root privilege to create cgroup
. Either creates sub-directory /sys/fs/cgroup/cpuacct/go-judger
, /sys/fs/cgroup/memory/go-judger
, /sys/fs/cgroup/pids/go-judger
and make execution user readable or use sudo
to run it.
The default binding address for the executor server is :5050
. Can be specified with -http
flag.
The default concurrency is 4
, Can be specified with -parallism
flag.
The default file store is in memory, local cache can be specified wieh -dir
flag.
Planed API interface
interface LocalFile {
src: string; // absolute path for the file
}
interface MemoryFile {
content: string | Buffer; // file contents
}
interface PreparedFile {
fileId: string; // fileId defines file uploaded by /file
}
interface Pipe {
name: string; // file name in copyOut
max: number; // maximum bytes to collect from pipe
}
interface Cmd {
args: string[]; // command line argument
env?: string[]; // environment
// specifies file input / pipe collector for program file descriptors
files?: (LocalFile | MemoryFile | PreparedFile | Pipe | null)[];
// limitations
cpuLimit?: number; // ns
realCpuLimit?: number; // ns
memoryLimit?: number; // byte
procLimit?: number;
// copy the correspond file to the container dst path
copyIn?: {[dst:string]:LocalFile | MemoryFile | PreparedFile};
// copy out specifies files need to be copied out from the container after execution
copyOut?: string[];
// similar to copyOut but stores file in executor service and returns fileId, later download through /file/:fileId
copyOutCached?: string[];
// specifies the directory to dump container /w content
copyOutDir: string
}
enum Status {
Accepted, // normal
MemoryLimitExceeded, // mle
TimeLimitExceeded, // tle
OutputLimitExceeded, // ole
FileError, // fe
RuntimeError, // re
DangerousSyscall, // dgs
InternalError, // system error
}
interface PipeIndex {
index: number; // the index of cmd
fd: number; // the fd number of cmd
}
interface PipeMap {
in: PipeIndex; // input end of the pipe
out: PipeIndex; // output end of the pipe
}
interface Request {
cmd: Cmd[];
pipeMapping: PipeMap[];
}
interface Result {
status: Status;
error?: string; // potential system error message
time: number; // ns
memory: number; // byte
// copyFile name -> content
files?: {[name:string]:string};
// copyFileCached name -> fileId
fileIds?: {[name:string]:string};
}
Example Request & Response:
Single (Require apt install g++
inside the container):
{
"cmd": [{
"args": ["/usr/bin/g++", "a.cc", "-o", "a"],
"env": ["PATH=/usr/bin:/bin"],
"files": [{
"content": ""
}, {
"name": "stdout",
"max": 10240
}, {
"name": "stderr",
"max": 10240
}],
"cpuLimit": 10000000000,
"memoryLimit": 104857600,
"procLimit": 50,
"copyIn": {
"a.cc": {
"content": "#include <iostream>\nusing namespace std;\nint main() {\nint a, b;\ncin >> a >> b;\ncout << a + b << endl;\n}"
}
},
"copyOut": ["stdout", "stderr"],
"copyOutCached": ["a.cc", "a"],
"copyOutDir": "1"
}]
}
[
{
"status": "Accepted",
"exitStatus": 0,
"time": 303225231,
"memory": 32243712,
"files": {
"stderr": "",
"stdout": ""
},
"fileIds": {
"a": "5LWIZAA45JHX4Y4Z",
"a.cc": "NOHPGGDTYQUFRSLJ"
}
}
]
Multiple:
{
"cmd": [{
"args": ["/bin/cat", "1"],
"env": ["PATH=/usr/bin:/bin"],
"files": [{
"content": ""
}, null, {
"name": "stderr",
"max": 10240
}],
"cpuLimit": 1000000000,
"memoryLimit": 1048576,
"procLimit": 50,
"copyIn": {
"1": { "content": "TEST 1" }
},
"copyOut": ["stderr"]
},
{
"args": ["/bin/cat"],
"env": ["PATH=/usr/bin:/bin"],
"files": [null, {
"name": "stdout",
"max": 10240
}, {
"name": "stderr",
"max": 10240
}],
"cpuLimit": 1000000000,
"memoryLimit": 1048576,
"procLimit": 50,
"copyOut": ["stdout", "stderr"]
}],
"pipeMapping": [{
"in" : {"index": 0, "fd": 1 },
"out" : {"index": 1, "fd" : 0 }
}]
}
[
{
"status": "Accepted",
"exitStatus": 0,
"time": 1545123,
"memory": 253952,
"files": {
"stderr": ""
},
"fileIds": {}
},
{
"status": "Accepted",
"exitStatus": 0,
"time": 1501463,
"memory": 253952,
"files": {
"stderr": "",
"stdout": "TEST 1"
},
"fileIds": {}
}
]
Workflow
^
| Client (talk to the website)
v
+------+ +----+
| |<-->|Data|
|Judger| +----+--+
| |<-->|Problem|
+------+ +-------+
^
| TaskQueue
v
+------+ +--------+
|Runner|<->|Language|
+------+ +--------+
^
| EnvExec
v
+--------------------+
|ContainerEnvironment|
+--------------------+
Container Root Filesystem
- necessary lib / exec / compiler / header readonly bind mounted from current file system: /lib /lib64 /bin /usr
- work directory tmpfs mount: /w (work dir), /tmp (compiler temp files)
- additional compiler scripts / exec readonly bind mounted: /c
- additional header readonly bind mounted: /i
Interfaces
- client: receive judge tasks (websocket / socket.io / RabbitMQ / REST API)
- data: interface to download, cache, lock and access test data files from website (by dataId)
- taskqueue: message queue to send run task and receive run task result (In memory / (RabbitMQ, Redis))
- file: general file interface (disk / memory)
- language: programming language compile & execute configurations
- problem: parse problem definition from configuration files
Judge Logic
- judger: execute judge logics (compile / standard / interactive / answer submit) and distribute as run task to queue, the collect and calculate results
- runner: receive run task and execute in sandbox environment
Models
- JudgeTask: judge task pushed from website (type, source, data)
- JudgeResult: judge task result send back to website
- JudgeSetting: problem setting (from yaml) and JudgeCase
- RunTask: run task parameters send to run_queue
- RunResult: run task result sent back from queue
Utilities
- pkg/envexec: run single / group of programs in parallel within restricted environment and resource constraints
Planned API
Progress
Client is able to report progress to the web front-end. Task should maintain its states
Planned events are:
- Parsed: problem data have been downloaded and problem configuration have been parsed (pass problem config to task)
- Compiled: user code have been compiled (success / fail)
- Progressed: single test case finished (success / fail - detail message)
- Finished: all test cases finished / compile failed
TODO
- socket.io client with namespace
- judge_v3 protocol
- executor server
- syzoj problem YAML config parser
- syzoj data downloader
- syzoj compile configuration
- file io
- special judger
- interact problem
- answer submit
- demo site