From c3f7b0fa2b6600efcda714f3c10509f3f972eff5 Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Thu, 28 Nov 2024 10:05:44 -0500 Subject: [PATCH] feat(executor/local): simple local executor without limits --- internal/executor/all.go | 1 + internal/executor/local/buffer.go | 35 ++++++++ internal/executor/local/executor.go | 127 ++++++++++++++++++++++++++++ internal/executor/local/meta.go | 9 ++ 4 files changed, 172 insertions(+) create mode 100644 internal/executor/local/buffer.go create mode 100644 internal/executor/local/executor.go create mode 100644 internal/executor/local/meta.go diff --git a/internal/executor/all.go b/internal/executor/all.go index b0e5111..5c66365 100644 --- a/internal/executor/all.go +++ b/internal/executor/all.go @@ -2,6 +2,7 @@ package executors import ( _ "github.com/joint-online-judge/JOJ3/internal/executor/dummy" + _ "github.com/joint-online-judge/JOJ3/internal/executor/local" "github.com/joint-online-judge/JOJ3/internal/executor/sandbox" ) diff --git a/internal/executor/local/buffer.go b/internal/executor/local/buffer.go new file mode 100644 index 0000000..23f7a8c --- /dev/null +++ b/internal/executor/local/buffer.go @@ -0,0 +1,35 @@ +package local + +import ( + "bytes" + "errors" +) + +// LimitedBuffer wraps a bytes.Buffer and limits its size. +type LimitedBuffer struct { + buf *bytes.Buffer + maxSize int +} + +// Write writes data to the buffer and checks the size limit. +func (lb *LimitedBuffer) Write(p []byte) (n int, err error) { + if lb.buf.Len()+len(p) > lb.maxSize { + // Truncate to fit within the limit + allowed := lb.maxSize - lb.buf.Len() + if allowed > 0 { + n, _ = lb.buf.Write(p[:allowed]) + } + return n, errors.New("buffer size limit exceeded") + } + return lb.buf.Write(p) +} + +// Bytes returns the buffer's content. +func (lb *LimitedBuffer) Bytes() []byte { + return lb.buf.Bytes() +} + +// String returns the buffer's content as a string. +func (lb *LimitedBuffer) String() string { + return lb.buf.String() +} diff --git a/internal/executor/local/executor.go b/internal/executor/local/executor.go new file mode 100644 index 0000000..a4b15b0 --- /dev/null +++ b/internal/executor/local/executor.go @@ -0,0 +1,127 @@ +package local + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/criyle/go-judge/envexec" + "github.com/joint-online-judge/JOJ3/internal/stage" +) + +type Local struct{} + +func (e *Local) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) { + var results []stage.ExecutorResult + + for _, cmd := range cmds { + execCmd := exec.Command(cmd.Args[0], cmd.Args[1:]...) // #nosec G204 + + if len(cmd.Env) > 0 { + execCmd.Env = cmd.Env + } + + if cmd.Stdin != nil { + if cmd.Stdin.Content != nil { + execCmd.Stdin = strings.NewReader(*cmd.Stdin.Content) + } else if cmd.Stdin.Src != nil { + file, err := os.Open(*cmd.Stdin.Src) + if err != nil { + return nil, fmt.Errorf("failed to open stdin file: %v", err) + } + defer file.Close() + execCmd.Stdin = file + } + } + var stdoutBuffer, stderrBuffer bytes.Buffer + execCmd.Stdout = &stdoutBuffer + execCmd.Stderr = &stderrBuffer + + startTime := time.Now() + err := execCmd.Start() + if err != nil { + return nil, fmt.Errorf("failed to start command: %v", err) + } + + err = execCmd.Wait() + endTime := time.Now() + runTime := endTime.Sub(startTime) + + result := stage.ExecutorResult{ + Status: stage.Status(envexec.StatusAccepted), + ExitStatus: 0, + Error: "", + RunTime: func() uint64 { + nanos := runTime.Nanoseconds() + if nanos < 0 { + return 0 + } + return uint64(nanos) + }(), + Files: map[string]string{}, + FileIDs: map[string]string{}, + } + + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + result.ExitStatus = exitErr.ExitCode() + result.Status = stage.Status(envexec.StatusNonzeroExitStatus) + result.Error = exitErr.Error() + } else { + result.Status = stage.Status(envexec.StatusInternalError) + result.Error = err.Error() + } + } + + if cmd.Stdout != nil && cmd.Stdout.Name != nil { + result.Files[*cmd.Stdout.Name] = stdoutBuffer.String() + } + if cmd.Stderr != nil && cmd.Stderr.Name != nil { + result.Files[*cmd.Stderr.Name] = stderrBuffer.String() + } + + if err := handleCopyOut(&result, cmd); err != nil { + result.Status = stage.Status(envexec.StatusFileError) + result.Error = err.Error() + } + + results = append(results, result) + } + + return results, nil +} + +// Helper function to handle copyOut files +func handleCopyOut(result *stage.ExecutorResult, cmd stage.Cmd) error { + for _, filename := range cmd.CopyOut { + optional := false + if strings.HasSuffix(filename, "?") { + optional = true + filename = strings.TrimSuffix(filename, "?") + } + result.Files[filename] = "" + // Read file and add to result.Files + file, err := os.Open(filename) + if err != nil { + if !optional { + return err + } + continue + } + defer file.Close() + content, err := io.ReadAll(file) + if err != nil { + return err + } + result.Files[filename] = string(content) + } + return nil +} + +func (e *Local) Cleanup() error { + return nil +} diff --git a/internal/executor/local/meta.go b/internal/executor/local/meta.go new file mode 100644 index 0000000..b6bd99a --- /dev/null +++ b/internal/executor/local/meta.go @@ -0,0 +1,9 @@ +package local + +import "github.com/joint-online-judge/JOJ3/internal/stage" + +var name = "local" + +func init() { + stage.RegisterExecutor(name, &Local{}) +}