Skip to content

Commit

Permalink
Move sandbox code to seperated module (#104)
Browse files Browse the repository at this point in the history
We would be adding more sandbox impls in the future :) 

Co-authored-by: Natsu Kagami <[email protected]>
  • Loading branch information
minhnhatnoe and natsukagami authored Jun 24, 2023
1 parent aa5d111 commit 47610d8
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 113 deletions.
14 changes: 3 additions & 11 deletions cmd/kjudge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
_ "github.com/natsukagami/kjudge/models"
"github.com/natsukagami/kjudge/server"
"github.com/natsukagami/kjudge/worker"
"github.com/natsukagami/kjudge/worker/isolate"
"github.com/natsukagami/kjudge/worker/raw"
)

var (
Expand All @@ -34,15 +32,9 @@ func main() {
}
defer db.Close()

var sandbox worker.Sandbox
switch *sandboxImpl {
case "raw":
log.Println("'raw' sandbox selected. WE ARE NOT RESPONSIBLE FOR ANY BREAKAGE CAUSED BY FOREIGN CODE.")
sandbox = &raw.Sandbox{}
case "isolate":
sandbox = isolate.New()
default:
log.Fatalf("Sandbox %s doesn't exists or not yet implemented.", *sandboxImpl)
sandbox, err := worker.NewSandbox(*sandboxImpl)
if err != nil {
log.Fatalf("%v", err)
}

opts := []server.Opt{}
Expand Down
4 changes: 2 additions & 2 deletions worker/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import (
"github.com/mattn/go-sqlite3"
"github.com/natsukagami/kjudge/db"
"github.com/natsukagami/kjudge/models"
"github.com/natsukagami/kjudge/worker/sandbox"
"github.com/pkg/errors"
)

// Queue implements a queue that runs each job one by one.
type Queue struct {
DB *db.DB
Sandbox Sandbox
Sandbox sandbox.Runner
}

// Start starts the queue. It is blocking, so might wanna "go run" it.
func (q *Queue) Start() {
// Register the update callback
toUpdate := q.startHook()

for {
// Get the newest job
job, err := models.FirstJob(q.DB)
Expand Down
37 changes: 19 additions & 18 deletions worker/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/jmoiron/sqlx"
"github.com/natsukagami/kjudge/models"
"github.com/natsukagami/kjudge/worker/sandbox"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -67,12 +68,12 @@ func (r *RunContext) CompiledSource() (bool, []byte) {
}

// RunInput creates a SandboxInput for running the submission's source.
func (r *RunContext) RunInput(source []byte) (*SandboxInput, error) {
func (r *RunContext) RunInput(source []byte) (*sandbox.Input, error) {
command, args, err := RunCommand(r.Sub.Language)
if err != nil {
return nil, err
}
return &SandboxInput{
return &sandbox.Input{
Command: command,
Args: args,
Files: nil,
Expand All @@ -86,11 +87,11 @@ func (r *RunContext) RunInput(source []byte) (*SandboxInput, error) {

// CompareInput creates a SandboxInput for running the comparator.
// Also returns whether we have diff-based or comparator-based input.
func (r *RunContext) CompareInput(submissionOutput []byte) (input *SandboxInput, useComparator bool, err error) {
func (r *RunContext) CompareInput(submissionOutput []byte) (input *sandbox.Input, useComparator bool, err error) {
file, err := models.GetFileWithName(r.DB, r.Problem.ID, "compare")
if errors.Is(err, sql.ErrNoRows) {
// Use a simple diff
return &SandboxInput{
return &sandbox.Input{
Command: "/usr/bin/diff",
Args: []string{"-wqts", "output", "expected"},
Files: map[string][]byte{"output": submissionOutput, "expected": r.Test.Output},
Expand All @@ -102,31 +103,31 @@ func (r *RunContext) CompareInput(submissionOutput []byte) (input *SandboxInput,
return nil, false, err
}
// Use the given comparator.
return &SandboxInput{
return &sandbox.Input{
Command: "code",
Args: []string{"input", "expected", "output"},
Files: map[string][]byte{"input": r.Test.Input, "expected": r.Test.Output, "output": submissionOutput},
TimeLimit: 20 * time.Second,
MemoryLimit: (2 << 20), // 1 GB
MemoryLimit: (1 << 20), // 1 GB

CompiledSource: file.Content,
}, true, nil
}

func RunSingleCommand(sandbox Sandbox, r *RunContext, source []byte) (output *SandboxOutput, err error) {
func RunSingleCommand(s sandbox.Runner, r *RunContext, source []byte) (output *sandbox.Output, err error) {
// First, use the sandbox to run the submission itself.
input, err := r.RunInput(source)
if err != nil {
return nil, err
}
output, err = sandbox.Run(input)
output, err = s.Run(input)
if err != nil {
return nil, errors.WithStack(err)
}
return output, nil
}

func RunMultipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages []string) (output *SandboxOutput, err error) {
func RunMultipleCommands(s sandbox.Runner, r *RunContext, source []byte, stages []string) (output *sandbox.Output, err error) {
command, args, err := RunCommand(r.Sub.Language)
if err != nil {
return nil, err
Expand All @@ -140,7 +141,7 @@ func RunMultipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages [
}
stageArgs := strings.Split(stage, " ")

sandboxInput := &SandboxInput{
sandboxInput := &sandbox.Input{
Command: command,
Args: append(stageArgs, args...),
Files: nil,
Expand All @@ -151,7 +152,7 @@ func RunMultipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages [
Input: input,
}

output, err = sandbox.Run(sandboxInput)
output, err = s.Run(sandboxInput)
if err != nil {
return nil, err
}
Expand All @@ -166,7 +167,7 @@ func RunMultipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages [
}

// Run runs a RunContext.
func Run(sandbox Sandbox, r *RunContext) error {
func Run(s sandbox.Runner, r *RunContext) error {
compiled, source := r.CompiledSource()
if !compiled {
// Add a compilation job and re-add ourselves.
Expand All @@ -180,11 +181,11 @@ func Run(sandbox Sandbox, r *RunContext) error {

log.Printf("[WORKER] Running submission %v on [test `%v`, group `%v`]\n", r.Sub.ID, r.Test.Name, r.TestGroup.Name)

var output *SandboxOutput
var output *sandbox.Output
file, err := models.GetFileWithName(r.DB, r.Problem.ID, ".stages")
if errors.Is(err, sql.ErrNoRows) {
// Problem type is not Chained Type, run a single command
output, err = RunSingleCommand(sandbox, r, source)
output, err = RunSingleCommand(s, r, source)
if err != nil {
return err
}
Expand All @@ -193,7 +194,7 @@ func Run(sandbox Sandbox, r *RunContext) error {
} else {
// Problem Type is Chained Type, we need to run mutiple commands with arguments from .stages (file)
stages := strings.Split(string(file.Content), "\n")
output, err = RunMultipleCommands(sandbox, r, source, stages)
output, err = RunMultipleCommands(s, r, source, stages)
if err != nil {
return err
}
Expand All @@ -214,7 +215,7 @@ func Run(sandbox Sandbox, r *RunContext) error {
if err != nil {
return err
}
output, err = sandbox.Run(input)
output, err = s.Run(input)
if err != nil {
return err
}
Expand All @@ -229,7 +230,7 @@ func Run(sandbox Sandbox, r *RunContext) error {
}

// Parse the comparator's output and reflect it into `result`.
func parseComparatorOutput(s *SandboxOutput, result *models.TestResult, useComparator bool) error {
func parseComparatorOutput(s *sandbox.Output, result *models.TestResult, useComparator bool) error {
if useComparator {
// Paste the comparator's output to result
result.Verdict = strings.TrimSpace(string(s.Stderr))
Expand Down Expand Up @@ -258,7 +259,7 @@ func parseComparatorOutput(s *SandboxOutput, result *models.TestResult, useCompa
}

// Parse the sandbox output into a TestResult.
func parseSandboxOutput(s *SandboxOutput, r *RunContext) *models.TestResult {
func parseSandboxOutput(s *sandbox.Output, r *RunContext) *models.TestResult {
score := 1.0
if !s.Success {
score = 0.0
Expand Down
64 changes: 12 additions & 52 deletions worker/sandbox.go
Original file line number Diff line number Diff line change
@@ -1,60 +1,20 @@
package worker

import (
"os"
"path/filepath"
"time"

"github.com/natsukagami/kjudge/worker/sandbox"
"github.com/natsukagami/kjudge/worker/sandbox/isolate"
"github.com/natsukagami/kjudge/worker/sandbox/raw"
"github.com/pkg/errors"
)

// Sandbox provides a way to run an arbitary command
// within a sandbox, with configured input/outputs and
// proper time and memory limits.
//
// kjudge currently implements two sandboxes, "isolate" (which requires "github.com/ioi/isolate" to be available)
// and "raw" (NOT RECOMMENDED, RUN AT YOUR OWN RISK).
// Which sandbox is used can be set at runtime with a command-line switch.
type Sandbox interface {
Run(*SandboxInput) (*SandboxOutput, error)
}

// SandboxInput is the input to a sandbox.
type SandboxInput struct {
Command string `json:"command"` // The passed command
Args []string `json:"args"` // any additional arguments, if needed
Files map[string][]byte `json:"files"` // Any additional files needed
TimeLimit time.Duration `json:"time_limit"` // The given time-limit
MemoryLimit int `json:"memory_limit"` // in KBs

CompiledSource []byte `json:"compiled_source"` // Should be written down to the CWD as a file named "code", as the command expects
Input []byte `json:"input"`
}

// SandboxOutput is the output which the sandbox needs to give back.
type SandboxOutput struct {
Success bool `json:"success"` // Whether the command exited zero.
RunningTime time.Duration `json:"running_time"` // The running time of the command.
MemoryUsed int `json:"memory_used"` // in KBs

Stdout []byte `json:"stdout"`
Stderr []byte `json:"stderr"`
ErrorMessage string `json:"error_message,omitempty"`
}

// CopyTo copies all the files it contains into cwd.
func (input *SandboxInput) CopyTo(cwd string) error {
// Copy all the files into "cwd"
for name, file := range input.Files {
if err := os.WriteFile(filepath.Join(cwd, name), file, 0666); err != nil {
return errors.Wrapf(err, "writing file %s", name)
}
}
// Copy and set chmod the "code" file
if input.CompiledSource != nil {
if err := os.WriteFile(filepath.Join(cwd, "code"), input.CompiledSource, 0777); err != nil {
return errors.WithStack(err)
}
func NewSandbox(name string, options ...sandbox.Option) (sandbox.Runner, error) {
setting := sandbox.MakeSettings(options...)
switch name {
case "raw":
return raw.New(setting), nil
case "isolate":
return isolate.New(setting), nil
default:
return nil, errors.Errorf("Sandbox %s doesn't exists or not yet implemented.", name)
}
return nil
}
File renamed without changes.
31 changes: 18 additions & 13 deletions worker/isolate/sandbox.go → worker/sandbox/isolate/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"strings"
"time"

"github.com/natsukagami/kjudge/worker"
"github.com/natsukagami/kjudge/worker/sandbox"
"github.com/pkg/errors"
)

Expand All @@ -29,12 +29,13 @@ func init() {
}
}

// Sandbox implements worker.Sandbox.
type Sandbox struct {
private struct{} // Makes the sandbox not simply constructible
// Runner implements worker.Runner.
type Runner struct {
settings sandbox.Settings
private struct{} // Makes the sandbox not simply constructible
}

var _ worker.Sandbox = (*Sandbox)(nil)
var _ sandbox.Runner = (*Runner)(nil)

// Panics on not having "isolate" accessible.
func mustHaveIsolate() {
Expand All @@ -49,13 +50,17 @@ func mustHaveIsolate() {

// New returns a new sandbox.
// Panics if isolate is not installed.
func New() *Sandbox {
func New(settings sandbox.Settings) *Runner {
mustHaveIsolate()
return &Sandbox{private: struct{}{}}
return &Runner{settings: settings, private: struct{}{}}
}

// Run implements Sandbox.Run.
func (s *Sandbox) Run(input *worker.SandboxInput) (*worker.SandboxOutput, error) {
func (s *Runner) Settings() *sandbox.Settings {
return &s.settings
}

// Run implements Runner.Run.
func (s *Runner) Run(input *sandbox.Input) (*sandbox.Output, error) {
// Init the sandbox
defer s.cleanup()
dirBytes, err := exec.Command(isolateCommand, "--init", "--cg").Output()
Expand Down Expand Up @@ -86,7 +91,7 @@ func (s *Sandbox) Run(input *worker.SandboxInput) (*worker.SandboxOutput, error)
}

// Parse the meta file
output := &worker.SandboxOutput{
output := &sandbox.Output{
Stdout: stdout.Bytes(),
Stderr: stderr.Bytes(),
}
Expand All @@ -97,7 +102,7 @@ func (s *Sandbox) Run(input *worker.SandboxInput) (*worker.SandboxOutput, error)
return output, nil
}

func parseMetaFile(path string, output *worker.SandboxOutput) error {
func parseMetaFile(path string, output *sandbox.Output) error {
meta, err := ReadMetaFile(path)
if err != nil {
return err
Expand All @@ -114,7 +119,7 @@ func parseMetaFile(path string, output *worker.SandboxOutput) error {
}

// Build the command for isolate --run.
func buildCmd(dir, metaFile string, input *worker.SandboxInput) *exec.Cmd {
func buildCmd(dir, metaFile string, input *sandbox.Input) *exec.Cmd {
// Calculate stuff
timeLimit := float64(input.TimeLimit) / float64(time.Second)

Expand Down Expand Up @@ -147,6 +152,6 @@ func buildCmd(dir, metaFile string, input *worker.SandboxInput) *exec.Cmd {
return cmd
}

func (s *Sandbox) cleanup() {
func (s *Runner) cleanup() {
_ = exec.Command(isolateCommand, "--cleanup", "--cg").Run()
}
Loading

0 comments on commit 47610d8

Please sign in to comment.