diff --git a/cmd/kjudge/main.go b/cmd/kjudge/main.go index 0df82f3f..e8ffc902 100644 --- a/cmd/kjudge/main.go +++ b/cmd/kjudge/main.go @@ -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 ( @@ -36,15 +34,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{} diff --git a/worker/queue.go b/worker/queue.go index 10f157da..d8ba7562 100644 --- a/worker/queue.go +++ b/worker/queue.go @@ -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) diff --git a/worker/run.go b/worker/run.go index bb2e0b5c..8f87a534 100644 --- a/worker/run.go +++ b/worker/run.go @@ -10,6 +10,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/natsukagami/kjudge/models" + "github.com/natsukagami/kjudge/worker/sandbox" "github.com/pkg/errors" ) @@ -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, @@ -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}, @@ -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 @@ -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, @@ -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 } @@ -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. @@ -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 } @@ -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 } @@ -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 } @@ -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)) @@ -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 diff --git a/worker/sandbox.go b/worker/sandbox.go index 9336dd1b..41a68654 100644 --- a/worker/sandbox.go +++ b/worker/sandbox.go @@ -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 } diff --git a/worker/isolate/meta.go b/worker/sandbox/isolate/meta.go similarity index 100% rename from worker/isolate/meta.go rename to worker/sandbox/isolate/meta.go diff --git a/worker/isolate/sandbox.go b/worker/sandbox/isolate/sandbox.go similarity index 81% rename from worker/isolate/sandbox.go rename to worker/sandbox/isolate/sandbox.go index 4010adb5..57125e04 100644 --- a/worker/isolate/sandbox.go +++ b/worker/sandbox/isolate/sandbox.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "github.com/natsukagami/kjudge/worker" + "github.com/natsukagami/kjudge/worker/sandbox" "github.com/pkg/errors" ) @@ -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() { @@ -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() @@ -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(), } @@ -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 @@ -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) @@ -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() } diff --git a/worker/raw/sandbox.go b/worker/sandbox/raw/sandbox.go similarity index 71% rename from worker/raw/sandbox.go rename to worker/sandbox/raw/sandbox.go index a8270afa..9089f074 100644 --- a/worker/raw/sandbox.go +++ b/worker/sandbox/raw/sandbox.go @@ -18,19 +18,34 @@ import ( "strings" "time" - "github.com/natsukagami/kjudge/worker" + "github.com/natsukagami/kjudge/worker/sandbox" ) -// Sandbox implements worker.Sandbox. -type Sandbox struct{} +// Runner implements worker.Runner. +type Runner struct { + settings sandbox.Settings +} + +var _ sandbox.Runner = (*Runner)(nil) + +func New(settings sandbox.Settings) *Runner { + if !settings.IgnoreWarning { + log.Println("'raw' sandbox selected. WE ARE NOT RESPONSIBLE FOR ANY BREAKAGE CAUSED BY FOREIGN CODE.") + } + return &Runner{settings: settings} +} -var _ worker.Sandbox = (*Sandbox)(nil) +func (s *Runner) Settings() *sandbox.Settings { + return &s.settings +} -// Run implements Sandbox.Run -func (s *Sandbox) Run(input *worker.SandboxInput) (*worker.SandboxOutput, error) { +// Run implements Runner.Run +func (s *Runner) Run(input *sandbox.Input) (*sandbox.Output, error) { dir := os.TempDir() - log.Printf("[SANDBOX] Running %s %v\n", input.Command, input.Args) + if s.Settings().LogSandbox { + log.Printf("[SANDBOX] Running %s %v\n", input.Command, input.Args) + } return s.RunFrom(dir, input) } @@ -41,7 +56,7 @@ func (s *Sandbox) Run(input *worker.SandboxInput) (*worker.SandboxOutput, error) // - MEMORY LIMITS ARE NOT SET. It always reports a memory usage of 0 (it cannot measure them). // - THE PROGRAM DOES NOT MESS WITH THE COMPUTER. LMAO // - The folder will be thrown away later. -func (s *Sandbox) RunFrom(cwd string, input *worker.SandboxInput) (*worker.SandboxOutput, error) { +func (s *Runner) RunFrom(cwd string, input *sandbox.Input) (*sandbox.Output, error) { if err := input.CopyTo(cwd); err != nil { return nil, err } @@ -73,7 +88,7 @@ func (s *Sandbox) RunFrom(cwd string, input *worker.SandboxInput) (*worker.Sandb case <-time.After(input.TimeLimit): cancel() <-done - return &worker.SandboxOutput{ + return &sandbox.Output{ Success: false, MemoryUsed: 0, RunningTime: input.TimeLimit, @@ -83,7 +98,7 @@ func (s *Sandbox) RunFrom(cwd string, input *worker.SandboxInput) (*worker.Sandb }, nil case commandErr := <-done: runningTime := time.Since(startTime) - return &worker.SandboxOutput{ + return &sandbox.Output{ Success: commandErr == nil, MemoryUsed: 0, RunningTime: runningTime, @@ -93,5 +108,4 @@ func (s *Sandbox) RunFrom(cwd string, input *worker.SandboxInput) (*worker.Sandb }, nil } - } diff --git a/worker/sandbox/sandbox.go b/worker/sandbox/sandbox.go new file mode 100644 index 00000000..20acb01d --- /dev/null +++ b/worker/sandbox/sandbox.go @@ -0,0 +1,61 @@ +package sandbox + +import ( + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" +) + +// Runner 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 Runner interface { + Settings() *Settings + Run(*Input) (*Output, error) +} + +// Input is the input to a sandbox. +type Input 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"` +} + +// Output is the output which the sandbox needs to give back. +type Output 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 *Input) 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) + } + } + return nil +} diff --git a/worker/sandbox/settings.go b/worker/sandbox/settings.go new file mode 100644 index 00000000..b77587d8 --- /dev/null +++ b/worker/sandbox/settings.go @@ -0,0 +1,36 @@ +package sandbox + +type Settings struct { + LogSandbox bool + IgnoreWarning bool +} + +var defaultSettings = Settings{LogSandbox: true, IgnoreWarning: false} + +// Option represents a sandbox option. +type Option func(Settings) Settings + +// IgnoreWarnings sets whether sandbox warnings should be silenced. +func IgnoreWarnings(ignore bool) Option { + return func(o Settings) Settings { + o.IgnoreWarning = ignore + return o + } +} + +// EnableSandboxLogs sets whether sandbox logs should be printed. +func EnableSandboxLogs(enable bool) Option { + return func(o Settings) Settings { + o.LogSandbox = enable + return o + } +} + +// MakeSettings creates a Settings object for sandboxes using options passed +func MakeSettings(options ...Option) Settings { + setting := defaultSettings + for _, option := range options { + setting = option(setting) + } + return setting +} diff --git a/worker/score.go b/worker/score.go index 36406c55..fb4aa5db 100644 --- a/worker/score.go +++ b/worker/score.go @@ -180,18 +180,18 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR subs[i], subs[j] = subs[j], subs[i] } +getScoredSub: for _, sub := range subs { score, _, counts := scoreOf(sub) if !counts { continue } + counted++ switch s.Problem.ScoringMode { case models.ScoringModeOnce: - if which == nil { - which = sub - maxScore = score - break - } + which = sub + maxScore = score + break getScoredSub case models.ScoringModeLast: which = sub maxScore = score @@ -213,7 +213,6 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR default: panic(s) } - counted++ } for _, sub := range subs {