diff --git a/cmd/kjudge/main.go b/cmd/kjudge/main.go index f1051a44..e4e7a3c6 100644 --- a/cmd/kjudge/main.go +++ b/cmd/kjudge/main.go @@ -12,8 +12,7 @@ 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" + "github.com/natsukagami/kjudge/worker/queue" ) var ( @@ -34,15 +33,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{} @@ -51,7 +44,7 @@ func main() { } // Start the queue - queue := worker.Queue{Sandbox: sandbox, DB: db} + queue := queue.NewQueue(db, sandbox) // Build the server server, err := server.New(db, opts...) diff --git a/db/migrations.go b/db/migrations.go index 6156d266..9fb3b7dc 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -4,7 +4,7 @@ import ( "database/sql" "io/fs" "log" - "path/filepath" + "path" "regexp" "sort" @@ -44,7 +44,7 @@ func (db *DB) migrate() error { // Do migrations one by one for _, name := range versions { - sqlFile := filepath.Join(assetsSql, name+".sql") + sqlFile := path.Join(assetsSql, name+".sql") file, err := fs.ReadFile(embed.Content, sqlFile) if err != nil { return errors.Wrapf(err, "File %s", sqlFile) diff --git a/embed/embed_dev.go b/embed/embed_dev.go index 2a9c5c08..da28f608 100644 --- a/embed/embed_dev.go +++ b/embed/embed_dev.go @@ -8,18 +8,25 @@ import ( "log" "os" "path/filepath" + "runtime" ) // Content serves content in the /embed directory. var Content fs.FS +func getEmbedDir() string { + _, path, _, _ := runtime.Caller(0) + return filepath.Dir(path) +} + func init() { - wd, err := os.Getwd() - if err != nil { - log.Panicf("cannot get current directory: %v", err) - } + // wd, err := os.Getwd() + // if err != nil { + // log.Panicf("cannot get current directory: %v", err) + // } - embedDir := filepath.Join(wd, "embed") + // embedDir := filepath.Join(wd, "embed") + embedDir := getEmbedDir() stat, err := os.Stat(embedDir) if err != nil { log.Panicf("cannot stat embed directory: %v", err) diff --git a/go.mod b/go.mod index 5a2f8e29..ce595bdd 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.0 github.com/pkg/errors v0.9.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e - golang.org/x/net v0.7.0 + golang.org/x/net v0.7.0 // indirect golang.org/x/text v0.7.0 golang.org/x/tools v0.1.12 google.golang.org/appengine v1.6.5 // indirect diff --git a/scripts/windows/production_test.ps1 b/scripts/windows/production_test.ps1 new file mode 100644 index 00000000..8d9fafe2 --- /dev/null +++ b/scripts/windows/production_test.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = "Stop" +Remove-Item kjudge.db* + +& "scripts\windows\production_build.ps1" + +Invoke-Expression ".\kjudge $args" + +# pwsh -c scripts/windows/production_test.ps1 --sandbox=raw to run this test script diff --git a/worker/compile.go b/worker/compile.go index 7f087a6f..5b0a3775 100644 --- a/worker/compile.go +++ b/worker/compile.go @@ -25,9 +25,17 @@ import ( // CompileContext is the information needed to perform compilation. type CompileContext struct { - DB *sqlx.Tx - Sub *models.Submission - Problem *models.Problem + DB *sqlx.Tx + Sub *models.Submission + Problem *models.Problem + AllowLogs bool +} + +func (c *CompileContext) Log(format string, v ...interface{}) { + if !c.AllowLogs { + return + } + log.Printf(format, v...) } // Compile performs compilation. @@ -65,7 +73,7 @@ func Compile(c *CompileContext) (bool, error) { return false, c.Sub.Write(c.DB) } - log.Printf("[WORKER] Compiling submission %v\n", c.Sub.ID) + c.Log("[WORKER] Compiling submission %v\n", c.Sub.ID) // Now, create a temporary directory. dir, err := os.MkdirTemp("", "*") @@ -96,7 +104,8 @@ func Compile(c *CompileContext) (bool, error) { c.Sub.CompiledSource = nil c.Sub.Verdict = models.VerdictCompileError } - log.Printf("[WORKER] Compiling submission %v succeeded (result = %v).", c.Sub.ID, result) + + c.Log("[WORKER] Compiling submission %v succeeded (result = %v).", c.Sub.ID, result) return result, c.Sub.Write(c.DB) } diff --git a/worker/queue.go b/worker/queue/queue.go similarity index 64% rename from worker/queue.go rename to worker/queue/queue.go index 10f157da..426af61d 100644 --- a/worker/queue.go +++ b/worker/queue/queue.go @@ -1,4 +1,4 @@ -package worker +package queue import ( "log" @@ -7,20 +7,30 @@ import ( "github.com/mattn/go-sqlite3" "github.com/natsukagami/kjudge/db" "github.com/natsukagami/kjudge/models" + "github.com/natsukagami/kjudge/worker" + "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 + DB *db.DB + Sandbox sandbox.Runner + Settings Settings +} + +func NewQueue(db *db.DB, sandbox sandbox.Runner, options ...Option) Queue { + setting := DefaultSettings + for _, option := range options { + setting = option(setting) + } + return Queue{DB: db, Sandbox: sandbox, Settings: setting} } // 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) @@ -41,6 +51,25 @@ func (q *Queue) Start() { } } +// Run starts the queue, solves all pending jobs, then returns +func (q *Queue) Run() { + for { + job, err := models.FirstJob(q.DB) + if err != nil { + log.Printf("[WORKER] Fetching job failed: %+v\n", err) + continue + } + if job == nil { + return + } + + if err := q.HandleJob(job); err != nil { + log.Printf("[WORKER] Handling job failed: %+v\n", err) + } + _ = job.Delete(q.DB) + } +} + // HandleJob dispatches a job. func (q *Queue) HandleJob(job *models.Job) error { // Start a job with a context and submission @@ -60,7 +89,8 @@ func (q *Queue) HandleJob(job *models.Job) error { } switch job.Type { case models.JobTypeCompile: - if _, err := Compile(&CompileContext{DB: tx, Sub: sub, Problem: problem}); err != nil { + if _, err := worker.Compile(&worker.CompileContext{ + DB: tx, Sub: sub, Problem: problem, AllowLogs: q.Settings.LogCompile}); err != nil { return err } case models.JobTypeRun: @@ -72,8 +102,8 @@ func (q *Queue) HandleJob(job *models.Job) error { if err != nil { return err } - if err := Run(q.Sandbox, &RunContext{ - DB: tx, Sub: sub, Problem: problem, TestGroup: tg, Test: test}); err != nil { + if err := worker.Run(q.Sandbox, &worker.RunContext{ + DB: tx, Sub: sub, Problem: problem, TestGroup: tg, Test: test, AllowLogs: q.Settings.LogRun}); err != nil { return err } case models.JobTypeScore: @@ -81,7 +111,8 @@ func (q *Queue) HandleJob(job *models.Job) error { if err != nil { return err } - if err := Score(&ScoreContext{DB: tx, Sub: sub, Problem: problem, Contest: contest}); err != nil { + if err := worker.Score(&worker.ScoreContext{ + DB: tx, Sub: sub, Problem: problem, Contest: contest, AllowLogs: q.Settings.LogScore}); err != nil { return err } } diff --git a/worker/queue/settings.go b/worker/queue/settings.go new file mode 100644 index 00000000..e5ecf81a --- /dev/null +++ b/worker/queue/settings.go @@ -0,0 +1,32 @@ +package queue + +type Settings struct { + LogCompile bool + LogRun bool + LogScore bool +} + +var DefaultSettings = Settings{LogCompile: true, LogRun: true, LogScore: true} + +type Option func(Settings) Settings + +func CompileLogs(enable bool) Option { + return func(o Settings) Settings { + o.LogCompile = enable + return o + } +} + +func RunLogs(enable bool) Option { + return func(o Settings) Settings { + o.LogRun = enable + return o + } +} + +func ScoreLogs(enable bool) Option { + return func(o Settings) Settings { + o.LogScore = enable + return o + } +} diff --git a/worker/run.go b/worker/run.go index bb2e0b5c..5ba2346e 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" ) @@ -23,6 +24,14 @@ type RunContext struct { Problem *models.Problem TestGroup *models.TestGroup Test *models.Test + AllowLogs bool +} + +func (r *RunContext) Log(format string, v ...interface{}) { + if !r.AllowLogs { + return + } + log.Printf(format, v...) } // TimeLimit returns the time limit of the context, in time.Duration. @@ -67,12 +76,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 +95,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 +111,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 +149,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 +160,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,25 +175,25 @@ 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. - log.Printf("[WORKER] Submission %v not compiled, creating Compile job.\n", r.Sub.ID) + r.Log("[WORKER] Submission %v not compiled, creating Compile job.\n", r.Sub.ID) return models.BatchInsertJobs(r.DB, models.NewJobCompile(r.Sub.ID), models.NewJobRun(r.Sub.ID, r.Test.ID)) } if source == nil { - log.Printf("[WORKER] Not running a submission that failed to compile.\n") + r.Log("[WORKER] Not running a submission that failed to compile.\n") return nil } - log.Printf("[WORKER] Running submission %v on [test `%v`, group `%v`]\n", r.Sub.ID, r.Test.Name, r.TestGroup.Name) + r.Log("[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 +202,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 +223,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 } @@ -222,14 +231,14 @@ func Run(sandbox Sandbox, r *RunContext) error { return err } - log.Printf("[WORKER] Done running submission %v on [test `%v`, group `%v`]: %.1f (t = %v, m = %v)\n", + r.Log("[WORKER] Done running submission %v on [test `%v`, group `%v`]: %.1f (t = %v, m = %v)\n", r.Sub.ID, r.Test.Name, r.TestGroup.Name, result.Score, result.RunningTime, result.MemoryUsed) return result.Write(r.DB) } // 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 +267,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..9b876a0e 100644 --- a/worker/sandbox.go +++ b/worker/sandbox.go @@ -1,60 +1,23 @@ 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) - } +func NewSandbox(name string, options ...sandbox.Option) (sandbox.Runner, error) { + setting := sandbox.DefaultSettings + for _, option := range options { + setting = option(setting) } - // 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) - } + 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..ef528ad9 --- /dev/null +++ b/worker/sandbox/settings.go @@ -0,0 +1,24 @@ +package sandbox + +type Settings struct { + LogSandbox bool + IgnoreWarning bool +} + +var DefaultSettings = Settings{LogSandbox: true, IgnoreWarning: false} + +type Option func(Settings) Settings + +func IgnoreWarnings(ignore bool) Option { + return func(o Settings) Settings { + o.IgnoreWarning = ignore + return o + } +} + +func EnableSandboxLogs(enable bool) Option { + return func(o Settings) Settings { + o.LogSandbox = enable + return o + } +} diff --git a/worker/score.go b/worker/score.go index 36406c55..8e4268f4 100644 --- a/worker/score.go +++ b/worker/score.go @@ -13,10 +13,18 @@ import ( // ScoreContext is a context for calculating a submission's score // and update the user's problem scores. type ScoreContext struct { - DB *sqlx.Tx - Sub *models.Submission - Problem *models.Problem - Contest *models.Contest + DB *sqlx.Tx + Sub *models.Submission + Problem *models.Problem + Contest *models.Contest + AllowLogs bool +} + +func (s *ScoreContext) Log(format string, v ...interface{}) { + if !s.AllowLogs { + return + } + log.Printf(format, v...) } // Score does scoring on a submission and updates the user's ProblemResult. @@ -32,10 +40,10 @@ func Score(s *ScoreContext) error { } if compiled, source := s.CompiledSource(); !compiled { // Add a compilation job and re-add ourselves. - log.Printf("[WORKER] Submission %v not compiled, creating Compile job.\n", s.Sub.ID) + s.Log("[WORKER] Submission %v not compiled, creating Compile job.\n", s.Sub.ID) return models.BatchInsertJobs(s.DB, models.NewJobCompile(s.Sub.ID), models.NewJobScore(s.Sub.ID)) } else if source == nil { - log.Printf("[WORKER] Not running a submission that failed to compile.\n") + s.Log("[WORKER] Not running a submission that failed to compile.\n") s.Sub.Verdict = models.VerdictCompileError if err := s.Sub.Write(s.DB); err != nil { return err @@ -46,12 +54,12 @@ func Score(s *ScoreContext) error { return err } pr := s.CompareScores(subs) - log.Printf("[WORKER] Problem results updated for user %s, problem %d (score = %.1f, penalty = %d)\n", s.Sub.UserID, s.Problem.ID, pr.Score, pr.Penalty) + s.Log("[WORKER] Problem results updated for user %s, problem %d (score = %.1f, penalty = %d)\n", s.Sub.UserID, s.Problem.ID, pr.Score, pr.Penalty) return pr.Write(s.DB) } if missing := MissingTests(tests, testResults); len(missing) > 0 { - log.Printf("[WORKER] Submission %v needs to run %d tests before being scored.\n", s.Sub.ID, len(missing)) + s.Log("[WORKER] Submission %v needs to run %d tests before being scored.\n", s.Sub.ID, len(missing)) var jobs []*models.Job for _, m := range missing { jobs = append(jobs, models.NewJobRun(s.Sub.ID, m.ID)) @@ -60,7 +68,7 @@ func Score(s *ScoreContext) error { return models.BatchInsertJobs(s.DB, jobs...) } - log.Printf("[WORKER] Scoring submission %d\n", s.Sub.ID) + s.Log("[WORKER] Scoring submission %d\n", s.Sub.ID) // Calculate the score by summing scores on each test group. s.Sub.Score = sql.NullFloat64{Float64: 0.0, Valid: true} for _, tg := range tests { @@ -78,7 +86,7 @@ func Score(s *ScoreContext) error { if err := s.Sub.Write(s.DB); err != nil { return err } - log.Printf("[WORKER] Submission %d scored (verdict = %s, score = %.1f). Updating problem results\n", s.Sub.ID, s.Sub.Verdict, s.Sub.Score.Float64) + s.Log("[WORKER] Submission %d scored (verdict = %s, score = %.1f). Updating problem results\n", s.Sub.ID, s.Sub.Verdict, s.Sub.Score.Float64) // Update the ProblemResult subs, err := models.GetUserProblemSubmissions(s.DB, s.Sub.UserID, s.Problem.ID) @@ -86,7 +94,7 @@ func Score(s *ScoreContext) error { return err } pr := s.CompareScores(subs) - log.Printf("[WORKER] Problem results updated for user %s, problem %d (score = %.1f, penalty = %d)\n", s.Sub.UserID, s.Problem.ID, pr.Score, pr.Penalty) + s.Log("[WORKER] Problem results updated for user %s, problem %d (score = %.1f, penalty = %d)\n", s.Sub.UserID, s.Problem.ID, pr.Score, pr.Penalty) return pr.Write(s.DB) } @@ -180,18 +188,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 +221,6 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR default: panic(s) } - counted++ } for _, sub := range subs {