feat: support more detailed problem config (#7)

This commit is contained in:
Paul Pan 2024-01-27 23:07:14 +08:00
parent bda209f794
commit 83d3913ba7
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
10 changed files with 330 additions and 236 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model" "git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"go.uber.org/zap" "go.uber.org/zap"
"time" "time"
@ -18,6 +19,7 @@ func (h *handler) Build(_ context.Context, t *asynq.Task) error {
} }
h.log.Debug("build", zap.Any("payload", p)) h.log.Debug("build", zap.Any("payload", p))
meta := runner.JudgeMeta{Version: p.ProblemVersionID}
status, ctx := func() (e.Status, string) { status, ctx := func() (e.Status, string) {
url, status := h.storageService.Get(p.StorageKey, time.Second*60*5) url, status := h.storageService.Get(p.StorageKey, time.Second*60*5)
@ -25,16 +27,20 @@ func (h *handler) Build(_ context.Context, t *asynq.Task) error {
return e.InternalError, "{\"Message\": \"storage error\"}" return e.InternalError, "{\"Message\": \"storage error\"}"
} }
config, status := h.runnerService.NewProblem(p.ProblemVersionID, url, true) config, status := h.runnerService.NewProblem(&meta, url, true)
if status != e.Success { if status != e.Success {
return e.InternalError, "{\"Message\": \"build error: " + status.String() + "\"}" return e.InternalError, "{\"Message\": \"build error: " + status.String() + "\"}"
} }
for i := range config.Languages { for i := range config.Languages {
// do not store in db // do not store in db
config.Languages[i].Type = "" config.Languages[i].Judge.Type = ""
config.Languages[i].Script = "" config.Languages[i].Judge.Script = ""
config.Languages[i].Cmp = "" config.Languages[i].Judge.Cmp = ""
// Compile and Run is ok
config.Languages[i].Runtime.Prebuild = runner.ConfigRuntime{}
config.Languages[i].Runtime.Check = runner.ConfigRuntime{}
} }
b, _ := json.Marshal(config) b, _ := json.Marshal(config)

View File

@ -23,50 +23,52 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
user := utils.RandomString(16) user := utils.RandomString(16)
h.log.Debug("judge", zap.Any("payload", p), zap.String("user", user)) h.log.Debug("judge", zap.Any("payload", p), zap.String("user", user))
meta := runner.JudgeMeta{Version: p.ProblemVersionID, User: user, Lang: p.Submission.Language}
status, point, ctx := func() (e.Status, int32, runner.JudgeStatus) { SystemError := &runner.JudgeStatus{
systemError := runner.JudgeStatus{ Message: "System Error",
Message: "System Error", Tasks: []runner.TaskStatus{{Verdict: runner.VerdictSystemError, Message: "API Error"}},
Tasks: []runner.TaskStatus{{Verdict: runner.VerdictSystemError, Message: "API Error"}}, }
}
status, point, ctx := func() (e.Status, int32, *runner.JudgeStatus) {
// 1. write user code // 1. write user code
userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language)) userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language))
if !file.Touch(userCode) { if !file.Touch(userCode) {
return e.InternalError, 0, systemError return e.InternalError, 0, SystemError
} }
err := file.Write(userCode, []byte(p.Submission.Code)) err := file.Write(userCode, []byte(p.Submission.Code))
if err != nil { if err != nil {
return e.InternalError, 0, systemError return e.InternalError, 0, SystemError
} }
// 2. check problem // 2. check problem
if !h.runnerService.ProblemExists(p.ProblemVersionID) { if !h.runnerService.ProblemExists(&meta) {
url, status := h.storageService.Get(p.StorageKey, time.Second*60*5) url, status := h.storageService.Get(p.StorageKey, time.Second*60*5)
if status != e.Success { if status != e.Success {
return status, 0, systemError return status, 0, SystemError
} }
_, status = h.runnerService.NewProblem(p.ProblemVersionID, url, false) _, status = h.runnerService.NewProblem(&meta, url, false)
if status != e.Success { if status != e.Success {
return status, 0, systemError return status, 0, SystemError
} }
} }
// 3. compile // 3. compile
compileResult, status := h.runnerService.Compile(p.ProblemVersionID, user, p.Submission.Language) compileResult, status := h.runnerService.Compile(&meta)
if status != e.Success { if status != e.Success {
return status, 0, compileResult return status, 0, compileResult
} }
// 4. run and judge // 4. run and judge
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language) result, point, status := h.runnerService.RunAndJudge(&meta)
return status, point, result return status, point, result
}() }()
if status == e.InternalError { if status == e.InternalError {
// notice asynq to retry // notice asynq to retry
return fmt.Errorf("internal error, ctx: %v", ctx) return fmt.Errorf("internal error, ctx: %v", *ctx)
} }
h.taskService.SubmitUpdate(&model.SubmitUpdatePayload{ h.taskService.SubmitUpdate(&model.SubmitUpdatePayload{
@ -75,7 +77,7 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
ProblemVersionID: p.ProblemVersionID, ProblemVersionID: p.ProblemVersionID,
UserDir: user, UserDir: user,
Point: point, Point: point,
}, ctx) }, *ctx)
return nil return nil
} }

View File

@ -63,6 +63,7 @@ const (
RunnerUserCompileFailed RunnerUserCompileFailed
RunnerRunFailed RunnerRunFailed
RunnerJudgeFailed RunnerJudgeFailed
RunnerLanguageNotSupported
) )
const ( const (
@ -123,6 +124,7 @@ var msgText = map[Status]string{
RunnerUserCompileFailed: "Runner User Compile Failed", RunnerUserCompileFailed: "Runner User Compile Failed",
RunnerRunFailed: "Runner Run Failed", RunnerRunFailed: "Runner Run Failed",
RunnerJudgeFailed: "Runner Judge Failed", RunnerJudgeFailed: "Runner Judge Failed",
RunnerLanguageNotSupported: "Runner Language Not Supported",
StorageUploadFailed: "Storage Upload Failed", StorageUploadFailed: "Storage Upload Failed",
StorageGetFailed: "Storage Get Failed", StorageGetFailed: "Storage Get Failed",

View File

@ -22,6 +22,12 @@ const (
ContainerImageRun = "git.0x7f.app/woj/ubuntu-run:latest" ContainerImageRun = "git.0x7f.app/woj/ubuntu-run:latest"
) )
type JudgeMeta struct {
Version uint
User string
Lang string
}
func init() { func init() {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
@ -34,39 +40,34 @@ func init() {
TmpDir = path.Join(Prefix, TmpDir) TmpDir = path.Join(Prefix, TmpDir)
} }
func (s *service) ProblemExists(version uint) bool { func (s *service) ProblemExists(meta *JudgeMeta) bool {
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version))
return file.Exist(problemPath) return file.Exist(problemPath)
} }
func (s *service) ValidatePath(version uint, user string, lang string) e.Status { func (s *service) ValidatePath(meta *JudgeMeta) e.Status {
if !s.ProblemExists(version) { if !s.ProblemExists(meta) {
s.log.Info("problem not exists", zap.Uint("version", version)) s.log.Info("problem not exists", zap.Uint("version", meta.Version))
return e.RunnerProblemNotExist return e.RunnerProblemNotExist
} }
userPath := filepath.Join(UserDir, user, fmt.Sprintf("%s.%s", user, lang)) userPath := filepath.Join(UserDir, meta.User, fmt.Sprintf("%s.%s", meta.User, meta.Lang))
if !file.Exist(userPath) { if !file.Exist(userPath) {
s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang)) s.log.Info("user program not exists", zap.String("user", meta.User), zap.String("lang", meta.Lang))
return e.RunnerUserNotExist return e.RunnerUserNotExist
} }
return e.Success return e.Success
} }
func (s *service) GetLangInfo(config *Config, lang string) (configLanguage, bool) { func (s *service) GetConfig(meta *JudgeMeta, skipCheck bool) (*Config, *ConfigLanguage, e.Status) {
for _, l := range config.Languages { config, err := s.ParseConfig(meta, skipCheck)
if l.Lang == lang { if err != nil {
return l, true return nil, nil, e.RunnerProblemParseFailed
}
} }
return configLanguage{}, false cLang, ok := config.FilterLanguage(meta.Lang)
} if !ok {
return nil, nil, e.RunnerLanguageNotSupported
func (s *service) GetLangScript(l *configLanguage, lang string) string {
if l.Type == "default" {
return "/woj/framework/template/default/" + lang + ".Makefile"
} else {
return "/woj/problem/judge/" + l.Script
} }
return config, cLang, e.Success
} }

View File

@ -13,34 +13,34 @@ import (
"time" "time"
) )
func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) { func (s *service) Compile(meta *JudgeMeta) (*JudgeStatus, e.Status) {
// 1. ensure problem/user exists // 1. ensure problem/user exists
status := s.ValidatePath(version, user, lang) status := s.ValidatePath(meta)
if status != e.Success { if status != e.Success {
return JudgeStatus{Message: "check failed"}, status return &JudgeStatus{Message: "check failed"}, status
} }
config, err := s.ParseConfig(version, true) config, cLang, status := s.GetConfig(meta, true)
if err != nil { if status != e.Success {
s.log.Error("[compile] parse config failed", zap.Error(err)) s.log.Error("[compile] parse config failed", zap.Any("meta", *meta))
return JudgeStatus{ return &JudgeStatus{
Message: "parse config failed", Message: "parse config failed",
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "parse config failed"}}, Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "parse config failed"}},
}, e.RunnerProblemParseFailed }, e.RunnerProblemParseFailed
} }
// 2. prepare judge environment // 2. prepare judge environment
workDir := filepath.Join(UserDir, user) workDir := filepath.Join(UserDir, meta.User)
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge") judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "judge")
sourceFile := filepath.Join(workDir, fmt.Sprintf("%s.%s", user, lang)) sourceFile := filepath.Join(workDir, fmt.Sprintf("%s.%s", meta.User, meta.Lang))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user)) targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", meta.User))
logFile := filepath.Join(workDir, fmt.Sprintf("%s.compile.log", user)) logFile := filepath.Join(workDir, fmt.Sprintf("%s.compile.log", meta.User))
log, err := os.Create(logFile) log, err := os.Create(logFile)
if err != nil { if err != nil {
s.log.Error("[compile] create log file failed", zap.Error(err)) s.log.Error("[compile] create log file failed", zap.Error(err))
return JudgeStatus{ return &JudgeStatus{
Message: "create log file failed", Message: "create log file failed",
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "create log file failed"}}, Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "create log file failed"}},
}, e.RunnerUserCompileFailed }, e.RunnerUserCompileFailed
@ -54,28 +54,28 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
DoAny(func() error { return os.Remove(targetFile) }). DoAny(func() error { return os.Remove(targetFile) }).
Do(func() error { return file.TouchErr(targetFile) }). Do(func() error { return file.TouchErr(targetFile) }).
Do(func() error { Do(func() error {
l, ok := s.GetLangInfo(&config, lang) l, ok := config.FilterLanguage(meta.Lang)
script := s.GetLangScript(&l, lang)
if !ok { if !ok {
return e.RunnerProblemParseFailed.AsError() return e.RunnerProblemParseFailed.AsError()
} }
script := l.JudgeScript()
args := &RunArgs{ args := &RunArgs{
Program: ProgramArgs{ Program: ProgramArgs{
Args: []string{"sh", "-c", fmt.Sprintf("cd /woj/user && make -f %s compile", script)}, Args: []string{"sh", "-c", fmt.Sprintf("cd /woj/user && make -f %s compile", script)},
Env: []string{fmt.Sprintf("USER_PROG=%s", user), fmt.Sprintf("LANG=%s", lang)}, Env: []string{fmt.Sprintf("USER_PROG=%s", meta.User), fmt.Sprintf("LANG=%s", meta.Lang)},
}, },
Runtime: RuntimeArgs{ Runtime: RuntimeArgs{
Image: ContainerImageFull, Image: ContainerImageFull,
Memory: 256 * 1024 * 1024, // 256 MB Pid: int64(cLang.Runtime.Compile.NProcLimit + 2), // bash + make
Timeout: time.Minute, Memory: uint64(cLang.Runtime.Compile.MemoryLimit * 1024 * 1024),
Timeout: time.Duration((cLang.Runtime.Compile.TimeLimit+1000)/1000) * time.Second,
}, },
IO: IOArgs{ IO: IOArgs{
Output: log, Output: log,
Limit: 4 * 1024, // 4 KB Limit: 4 * 1024, // 4 KB
}, },
} }
args.Runtime.Mount = []specs.Mount{ args.Runtime.Mount = []specs.Mount{
{ {
Source: judgeDir, Source: judgeDir,
@ -85,13 +85,13 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
}, },
{ {
Source: sourceFile, Source: sourceFile,
Destination: fmt.Sprintf("/woj/user/%s.%s", user, lang), Destination: fmt.Sprintf("/woj/user/%s.%s", meta.User, meta.Lang),
Type: "bind", Type: "bind",
Options: []string{"rbind", "ro"}, Options: []string{"rbind", "ro"},
}, },
{ {
Source: targetFile, Source: targetFile,
Destination: fmt.Sprintf("/woj/user/%s.out", user), Destination: fmt.Sprintf("/woj/user/%s.out", meta.User),
Type: "bind", Type: "bind",
Options: []string{"rbind"}, Options: []string{"rbind"},
}, },
@ -102,12 +102,7 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
Done() Done()
if err != nil { if err != nil {
s.log.Info("[compile] compile failed", s.log.Info("[compile] compile failed", zap.Error(err), zap.Any("meta", *meta))
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
status = e.RunnerUserCompileFailed status = e.RunnerUserCompileFailed
} }
@ -118,11 +113,11 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
msgText := string(msg) msgText := string(msg)
if !file.Exist(targetFile) || file.Empty(targetFile) { if !file.Exist(targetFile) || file.Empty(targetFile) {
return JudgeStatus{ return &JudgeStatus{
Message: "compile failed", Message: "compile failed",
Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}}, Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}},
utils.If(status == e.Success, e.RunnerUserCompileFailed, status) utils.If(status == e.Success, e.RunnerUserCompileFailed, status)
} }
return JudgeStatus{}, e.Success return &JudgeStatus{}, e.Success
} }

View File

@ -4,106 +4,146 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"os" "os"
"path/filepath" "path/filepath"
) )
type configLanguage struct { type ConfigRuntime struct {
Lang string `json:"Lang"` TimeLimit int `json:"TimeLimit"`
Type string `json:"Type,omitempty"` MemoryLimit int `json:"MemoryLimit"`
Script string `json:"Script,omitempty"` NProcLimit int `json:"NProcLimit"`
Cmp string `json:"Cmp,omitempty"` }
var (
DefaultPrebuildRuntime = ConfigRuntime{
TimeLimit: 300000,
MemoryLimit: 256,
NProcLimit: 64,
}
DefaultCompileRuntime = ConfigRuntime{
TimeLimit: 60000,
MemoryLimit: 256,
NProcLimit: 64,
}
DefaultCheckRuntime = ConfigRuntime{
TimeLimit: 60000,
MemoryLimit: 128,
NProcLimit: 64,
}
)
func (c *ConfigRuntime) Validate() error {
if c.TimeLimit <= 0 {
return errors.New("time limit <= 0")
}
if c.MemoryLimit <= 0 {
return errors.New("memory limit <= 0")
}
if c.NProcLimit <= 0 {
return errors.New("nproc limit <= 0")
}
return nil
}
type ConfigJudge struct {
Type string `json:"Type"`
Script string `json:"Script"`
Cmp string `json:"Cmp"`
}
func (c *ConfigJudge) Validate(base string, lang string) error {
if c.Type != "custom" && c.Type != "default" {
return fmt.Errorf("language %s has invalid type %s", lang, c.Type)
}
if c.Type == "custom" {
if c.Script == "" {
return fmt.Errorf("language %s has empty script", lang)
}
file := filepath.Join(base, "judge", c.Script)
_, err := os.Stat(file)
if err != nil {
return fmt.Errorf("language %s has invalid script %s", lang, c.Script)
}
}
if c.Type == "default" {
if c.Cmp == "" {
return fmt.Errorf("language %s has empty cmp", lang)
}
}
return nil
}
type ConfigLanguage struct {
Lang string `json:"Lang"`
Judge ConfigJudge `json:"Judge"`
Runtime struct {
Prebuild ConfigRuntime `json:"Prebuild"`
Compile ConfigRuntime `json:"Compile"`
Run ConfigRuntime `json:"Run"`
Check ConfigRuntime `json:"Check"`
} `json:"Runtime"`
}
func (l *ConfigLanguage) JudgeScript() string {
if l.Judge.Type == "default" {
return "/woj/framework/template/default/" + l.Lang + ".Makefile"
} else {
return "/woj/problem/judge/" + l.Judge.Script
}
} }
type Config struct { type Config struct {
Runtime struct { Languages []ConfigLanguage `json:"Languages"`
TimeLimit int `json:"TimeLimit"`
MemoryLimit int `json:"MemoryLimit"`
NProcLimit int `json:"NProcLimit"`
} `json:"Runtime"`
Languages []configLanguage `json:"Languages"`
Tasks []struct { Tasks []struct {
Id int `json:"Id"` Id int `json:"Id"`
Points int32 `json:"Points"` Points int32 `json:"Points"`
} `json:"Tasks"` } `json:"Tasks"`
} }
func (s *service) ParseConfig(version uint, skipCheck bool) (Config, error) { func (c *Config) Validate(base string) error {
base := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
file := filepath.Join(base, "config.json")
data, err := os.ReadFile(file)
if err != nil {
return Config{}, err
}
config := Config{}
err = json.Unmarshal(data, &config)
if err != nil {
return Config{}, err
}
if skipCheck {
return config, nil
}
err = s.ValidateConfig(&config, base)
if err != nil {
return Config{}, err
}
return config, nil
}
func (s *service) ValidateConfig(config *Config, base string) error {
if config.Runtime.TimeLimit < 0 {
return errors.New("time limit is negative")
}
if config.Runtime.MemoryLimit < 0 {
return errors.New("memory limit is negative")
}
if config.Runtime.NProcLimit < 0 {
return errors.New("nproc limit is negative")
}
allowedLang := map[string]struct{}{ allowedLang := map[string]struct{}{
"c": {}, "c": {},
"cpp": {}, "cpp": {},
"go": {},
"rust": {},
"python3": {},
"pypy3": {},
} }
for _, lang := range config.Languages {
for _, lang := range c.Languages {
// check language
if _, ok := allowedLang[lang.Lang]; !ok { if _, ok := allowedLang[lang.Lang]; !ok {
return fmt.Errorf("language %s is not allowed", lang.Lang) return fmt.Errorf("language %s is not allowed", lang.Lang)
} }
if lang.Type != "custom" && lang.Type != "default" { err := utils.NewMust().
return fmt.Errorf("language %s has invalid type %s", lang.Lang, lang.Type) // check judge config
Do(func() error { return lang.Judge.Validate(base, lang.Lang) }).
// check runtime limits
Do(func() error { return lang.Runtime.Prebuild.Validate() }).
Do(func() error { return lang.Runtime.Compile.Validate() }).
Do(func() error { return lang.Runtime.Run.Validate() }).
Do(func() error { return lang.Runtime.Check.Validate() }).
Done()
if err != nil {
return err
} }
if lang.Type == "custom" {
if lang.Script == "" {
return fmt.Errorf("language %s has empty script", lang.Lang)
}
file := filepath.Join(base, "judge", lang.Script)
_, err := os.Stat(file)
if err != nil {
return fmt.Errorf("language %s has invalid script %s", lang.Lang, lang.Script)
}
}
if lang.Type == "default" {
if lang.Cmp == "" {
return fmt.Errorf("language %s has empty cmp", lang.Lang)
}
}
} }
if len(config.Tasks) == 0 { if len(c.Tasks) == 0 {
return errors.New("no tasks") return errors.New("no tasks")
} }
ids := map[int]struct{}{} ids := map[int]struct{}{}
total := (1 + len(config.Tasks)) * len(config.Tasks) / 2 total := (1 + len(c.Tasks)) * len(c.Tasks) / 2
for _, task := range config.Tasks { for _, task := range c.Tasks {
if task.Id <= 0 { if task.Id <= 0 {
return fmt.Errorf("task %d has non-positive id", task.Id) return fmt.Errorf("task %d has non-positive id", task.Id)
} }
@ -125,3 +165,53 @@ func (s *service) ValidateConfig(config *Config, base string) error {
return nil return nil
} }
func (c *Config) FilterLanguage(lang string) (*ConfigLanguage, bool) {
for idx := range c.Languages {
if c.Languages[idx].Lang == lang {
return &c.Languages[idx], true
}
}
return &ConfigLanguage{}, false
}
func (s *service) ParseConfig(meta *JudgeMeta, skipCheck bool) (*Config, error) {
base := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version))
file := filepath.Join(base, "config.json")
data, err := os.ReadFile(file)
if err != nil {
return &Config{}, err
}
// unmarshal
config := &Config{}
err = json.Unmarshal(data, config)
if err != nil {
return &Config{}, err
}
// fill default
for idx := range config.Languages {
if config.Languages[idx].Runtime.Prebuild.Validate() != nil {
config.Languages[idx].Runtime.Prebuild = DefaultPrebuildRuntime
}
if config.Languages[idx].Runtime.Compile.Validate() != nil {
config.Languages[idx].Runtime.Compile = DefaultCompileRuntime
}
if config.Languages[idx].Runtime.Check.Validate() != nil {
config.Languages[idx].Runtime.Check = DefaultCheckRuntime
}
}
if skipCheck {
return config, nil
}
err = config.Validate(base)
if err != nil {
return &Config{}, err
}
return config, nil
}

View File

@ -13,9 +13,9 @@ import (
"time" "time"
) )
func (s *service) DownloadProblem(version uint, url string) e.Status { func (s *service) DownloadProblem(meta *JudgeMeta, url string) e.Status {
zipPath := filepath.Join(TmpDir, fmt.Sprintf("%d.zip", version)) zipPath := filepath.Join(TmpDir, fmt.Sprintf("%d.zip", meta.Version))
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version))
err := down.Down(zipPath, url) err := down.Down(zipPath, url)
if err != nil { if err != nil {
@ -32,26 +32,32 @@ func (s *service) DownloadProblem(version uint, url string) e.Status {
return e.Success return e.Success
} }
func (s *service) PrebuildProblem(version uint, force bool) e.Status { func (s *service) PrebuildProblem(meta *JudgeMeta, force bool) e.Status {
if !s.ProblemExists(version) { if !s.ProblemExists(meta) {
return e.RunnerProblemNotExist return e.RunnerProblemNotExist
} }
mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild") mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), ".mark.prebuild")
if force { if force {
_ = os.Remove(mark) _ = os.Remove(mark)
} else if file.Exist(mark) { } else if file.Exist(mark) {
return e.Success return e.Success
} }
prebuildScript := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge", "prebuild.Makefile") prebuildScript := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "judge", "prebuild.Makefile")
if !file.Exist(prebuildScript) { if !file.Exist(prebuildScript) {
s.log.Info("[new] prebuild script not found", zap.String("path", prebuildScript), zap.Uint("version", version)) s.log.Info("[new] prebuild script not found", zap.String("path", prebuildScript), zap.Uint("version", meta.Version))
return e.Success return e.Success
} }
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data") _, cLang, status := s.GetConfig(meta, false)
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge") if status != e.Success {
s.log.Error("[new] parse config failed", zap.Any("meta", *meta))
return e.RunnerProblemParseFailed
}
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "data")
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "judge")
args := &RunArgs{ args := &RunArgs{
Program: ProgramArgs{ Program: ProgramArgs{
@ -59,8 +65,9 @@ func (s *service) PrebuildProblem(version uint, force bool) e.Status {
}, },
Runtime: RuntimeArgs{ Runtime: RuntimeArgs{
Image: ContainerImageFull, Image: ContainerImageFull,
Memory: 256 * 1024 * 1024, // 256MB Pid: int64(cLang.Runtime.Prebuild.NProcLimit + 3), // sh + bash + make
Timeout: 5 * time.Minute, Memory: uint64(cLang.Runtime.Prebuild.MemoryLimit * 1024 * 1024),
Timeout: time.Duration((cLang.Runtime.Prebuild.TimeLimit+1000)/1000) * time.Second,
}, },
} }
args.Runtime.Mount = []specs.Mount{ args.Runtime.Mount = []specs.Mount{
@ -81,35 +88,35 @@ func (s *service) PrebuildProblem(version uint, force bool) e.Status {
err := s.ContainerRun(args) err := s.ContainerRun(args)
if err != nil { if err != nil {
s.log.Warn("[new] prebuild problem failed", zap.Error(err), zap.Uint("version", version)) s.log.Warn("[new] prebuild problem failed", zap.Error(err), zap.Uint("version", meta.Version))
return e.RunnerProblemPrebuildFailed return e.RunnerProblemPrebuildFailed
} }
return e.Success return e.Success
} }
func (s *service) NewProblem(version uint, url string, force bool) (Config, e.Status) { func (s *service) NewProblem(meta *JudgeMeta, url string, force bool) (*Config, e.Status) {
if force { if force {
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version))
_ = os.RemoveAll(problemPath) _ = os.RemoveAll(problemPath)
} }
if !s.ProblemExists(version) { if !s.ProblemExists(meta) {
status := s.DownloadProblem(version, url) status := s.DownloadProblem(meta, url)
if status != e.Success { if status != e.Success {
return Config{}, status return &Config{}, status
} }
} }
cfg, err := s.ParseConfig(version, false) cfg, err := s.ParseConfig(meta, false)
if err != nil { if err != nil {
s.log.Info("[new] parse problem failed", zap.Error(err), zap.Uint("version", version)) s.log.Info("[new] parse problem failed", zap.Error(err), zap.Uint("version", meta.Version))
return Config{}, e.RunnerProblemParseFailed return &Config{}, e.RunnerProblemParseFailed
} }
status := s.PrebuildProblem(version, true) status := s.PrebuildProblem(meta, true)
if status != e.Success { if status != e.Success {
return Config{}, status return &Config{}, status
} }
return cfg, e.Success return cfg, e.Success

View File

@ -13,37 +13,43 @@ import (
"time" "time"
) )
func (s *service) SandboxArgsBuilder(config *Config, user string, lang string, id int) string { func (s *service) SandboxArgsBuilder(meta *JudgeMeta, cLang *ConfigLanguage, id int) string {
var args []string var args []string
args = append(args, fmt.Sprintf("--memory_limit=%d", config.Runtime.MemoryLimit)) args = append(args, fmt.Sprintf("--memory_limit=%d", cLang.Runtime.Run.MemoryLimit))
args = append(args, fmt.Sprintf("--nproc_limit=%d", config.Runtime.NProcLimit)) args = append(args, fmt.Sprintf("--nproc_limit=%d", cLang.Runtime.Run.NProcLimit))
args = append(args, fmt.Sprintf("--time_limit=%d", config.Runtime.TimeLimit)) args = append(args, fmt.Sprintf("--time_limit=%d", cLang.Runtime.Run.TimeLimit))
args = append(args, fmt.Sprintf("--sandbox_template=%s", lang)) args = append(args, fmt.Sprintf("--sandbox_template=%s", cLang.Lang))
args = append(args, fmt.Sprintf("--sandbox_action=ret")) args = append(args, fmt.Sprintf("--sandbox_action=ret"))
args = append(args, fmt.Sprintf("--uid=1000")) args = append(args, fmt.Sprintf("--uid=1000"))
args = append(args, fmt.Sprintf("--gid=1000")) args = append(args, fmt.Sprintf("--gid=1000"))
args = append(args, fmt.Sprintf("--file_input=/woj/problem/data/input/%d.input", id)) args = append(args, fmt.Sprintf("--file_input=/woj/problem/data/input/%d.input", id))
args = append(args, fmt.Sprintf("--file_output=/woj/user/%d.out.usr", id)) args = append(args, fmt.Sprintf("--file_output=/woj/user/%d.out.usr", id))
args = append(args, fmt.Sprintf("--file_info=/woj/user/%d.info", id)) args = append(args, fmt.Sprintf("--file_info=/woj/user/%d.info", id))
args = append(args, fmt.Sprintf("--program=/woj/user/%s.out", user)) args = append(args, fmt.Sprintf("--program=/woj/user/%s.out", meta.User))
return strings.Join(args, " ") return strings.Join(args, " ")
} }
func (s *service) ProblemRun(version uint, user string, lang string, config *Config) { func (s *service) ProblemRun(meta *JudgeMeta, config *Config, cLang *ConfigLanguage) {
workDir := filepath.Join(UserDir, user) workDir := filepath.Join(UserDir, meta.User)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data", "input") dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "data", "input")
// woj-sandbox killer will add 1 more second, here we add 1 also runtimeArgs := RuntimeArgs{
timeout := time.Duration((config.Runtime.TimeLimit+1000)/1000+1+1) * time.Second Image: ContainerImageRun,
// sh, woj_launcher:program, woj_launcher:killer, woj_launcher:stat
Pid: int64(cLang.Runtime.Run.NProcLimit + 4),
Memory: uint64(cLang.Runtime.Run.MemoryLimit * 1024 * 1024),
// woj-sandbox killer will add 1 more second, here we add 1 also
Timeout: time.Duration((cLang.Runtime.Run.TimeLimit+1000)/1000+1+1) * time.Second,
}
ids := make([]int, 0) ids := make([]int, 0)
for _, task := range config.Tasks { for _, task := range config.Tasks {
f := func(id int) func() { f := func(id int) func() {
return func() { return func() {
testCase := filepath.Join(dataDir, fmt.Sprintf("%d.input", id)) testCase := filepath.Join(dataDir, fmt.Sprintf("%d.input", id))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user)) targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", meta.User))
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id)) ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
ifoFile := filepath.Join(workDir, fmt.Sprintf("%d.info", id)) ifoFile := filepath.Join(workDir, fmt.Sprintf("%d.info", id))
@ -52,16 +58,10 @@ func (s *service) ProblemRun(version uint, user string, lang string, config *Con
Args: []string{ Args: []string{
"sh", "-c", "sh", "-c",
"cd /woj/user && /woj/framework/scripts/woj_launcher " + "cd /woj/user && /woj/framework/scripts/woj_launcher " +
s.SandboxArgsBuilder(config, user, lang, id), s.SandboxArgsBuilder(meta, cLang, id),
}, },
}, },
Runtime: RuntimeArgs{ Runtime: runtimeArgs,
Image: ContainerImageRun,
// sh, woj_launcher:program, woj_launcher:killer, woj_launcher:stat
Pid: int64(config.Runtime.NProcLimit + 4),
Memory: uint64(config.Runtime.MemoryLimit * 1024 * 1024),
Timeout: timeout,
},
} }
args.Runtime.Mount = []specs.Mount{ args.Runtime.Mount = []specs.Mount{
{ {
@ -72,7 +72,7 @@ func (s *service) ProblemRun(version uint, user string, lang string, config *Con
}, },
{ {
Source: targetFile, Source: targetFile,
Destination: fmt.Sprintf("/woj/user/%s.out", user), Destination: fmt.Sprintf("/woj/user/%s.out", meta.User),
Type: "bind", Type: "bind",
Options: []string{"rbind", "ro"}, Options: []string{"rbind", "ro"},
}, },
@ -99,12 +99,7 @@ func (s *service) ProblemRun(version uint, user string, lang string, config *Con
Done() Done()
if err != nil { if err != nil {
s.log.Info("[run] run failed", s.log.Info("[run] run failed", zap.Error(err), zap.Any("meta", *meta))
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
} }
} }
}(task.Id) }(task.Id)
@ -118,10 +113,18 @@ func (s *service) ProblemRun(version uint, user string, lang string, config *Con
} }
} }
func (s *service) ProblemJudge(version uint, user string, lang string, config *Config) { func (s *service) ProblemJudge(meta *JudgeMeta, config *Config, cLang *ConfigLanguage) {
workDir := filepath.Join(UserDir, user) workDir := filepath.Join(UserDir, meta.User)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data") dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "data")
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge") judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", meta.Version), "judge")
script := cLang.JudgeScript()
runtimeArgs := RuntimeArgs{
Image: ContainerImageFull,
Pid: int64(cLang.Runtime.Check.NProcLimit + 2), // bash + make
Memory: uint64(cLang.Runtime.Check.MemoryLimit * 1024 * 1024),
Timeout: time.Duration((cLang.Runtime.Check.TimeLimit+1000)/1000) * time.Second,
}
ids := make([]int, 0) ids := make([]int, 0)
for _, task := range config.Tasks { for _, task := range config.Tasks {
@ -130,25 +133,18 @@ func (s *service) ProblemJudge(version uint, user string, lang string, config *C
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id)) ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
jdgFile := filepath.Join(workDir, fmt.Sprintf("%d.judge", id)) jdgFile := filepath.Join(workDir, fmt.Sprintf("%d.judge", id))
c, ok := s.GetLangInfo(config, lang)
if !ok {
return
}
args := &RunArgs{ args := &RunArgs{
Program: ProgramArgs{ Program: ProgramArgs{
Args: []string{ Args: []string{
"sh", "-c", "sh", "-c",
fmt.Sprintf("cd /woj/user && make -f %s judge", s.GetLangScript(&c, lang)), fmt.Sprintf("cd /woj/user && make -f %s judge", script),
}, },
Env: []string{ Env: []string{
fmt.Sprintf("TEST_NUM=%d", id), fmt.Sprintf("TEST_NUM=%d", id),
fmt.Sprintf("CMP=%s", c.Cmp), fmt.Sprintf("CMP=%s", cLang.Judge.Cmp),
}, },
}, },
Runtime: RuntimeArgs{ Runtime: runtimeArgs,
Image: ContainerImageFull,
},
} }
args.Runtime.Mount = []specs.Mount{ args.Runtime.Mount = []specs.Mount{
{ {
@ -184,12 +180,7 @@ func (s *service) ProblemJudge(version uint, user string, lang string, config *C
Done() Done()
if err != nil { if err != nil {
s.log.Info("[judge] judge failed", s.log.Info("[judge] judge failed", zap.Error(err), zap.Any("meta", *meta))
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
} }
} }
}(task.Id) }(task.Id)
@ -203,27 +194,27 @@ func (s *service) ProblemJudge(version uint, user string, lang string, config *C
} }
} }
func (s *service) RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status) { func (s *service) RunAndJudge(meta *JudgeMeta) (*JudgeStatus, int32, e.Status) {
// 1. ensure problem/user exists // 1. ensure problem/user exists
status := s.ValidatePath(version, user, lang) status := s.ValidatePath(meta)
if status != e.Success { if status != e.Success {
return JudgeStatus{Message: "check failed"}, 0, status return &JudgeStatus{Message: "check failed"}, 0, status
} }
// 2. config // 2. config
config, err := s.ParseConfig(version, false) config, cLang, status := s.GetConfig(meta, true)
if err != nil { if status != e.Success {
return JudgeStatus{Message: "parse config failed"}, 0, e.RunnerProblemParseFailed return &JudgeStatus{Message: status.String()}, 0, status
} }
// 3. run user program // 3. run user program
s.ProblemRun(version, user, lang, &config) s.ProblemRun(meta, config, cLang)
// 4. run judger // 4. run judge
s.ProblemJudge(version, user, lang, &config) s.ProblemJudge(meta, config, cLang)
// 5. check result // 5. check result
result, pts := s.checkResults(user, &config) result, pts := s.CheckResults(meta, config, cLang)
return result, pts, e.Success return result, pts, e.Success
} }

View File

@ -21,17 +21,17 @@ type Service interface {
// EnsureDeps build docker images // EnsureDeps build docker images
EnsureDeps(force bool) e.Status EnsureDeps(force bool) e.Status
// NewProblem = Download + Parse + Prebuild // NewProblem = Download + Parse + Prebuild
NewProblem(version uint, url string, force bool) (Config, e.Status) NewProblem(meta *JudgeMeta, url string, force bool) (*Config, e.Status)
// Compile compile user submission // Compile compile user submission
Compile(version uint, user string, lang string) (JudgeStatus, e.Status) Compile(meta *JudgeMeta) (*JudgeStatus, e.Status)
// RunAndJudge execute user program // RunAndJudge execute user program
RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status) RunAndJudge(meta *JudgeMeta) (*JudgeStatus, int32, e.Status)
// ParseConfig parse config file // ParseConfig parse config file
ParseConfig(version uint, skipCheck bool) (Config, error) ParseConfig(meta *JudgeMeta, skipCheck bool) (*Config, error)
// ProblemExists check if problem exists // ProblemExists check if problem exists
ProblemExists(version uint) bool ProblemExists(meta *JudgeMeta) bool
HealthCheck() error HealthCheck() error
Shutdown() error Shutdown() error

View File

@ -97,12 +97,12 @@ func (t *TaskStatus) checkExit() *TaskStatus {
return t return t
} }
func (t *TaskStatus) checkTime(config *Config) *TaskStatus { func (t *TaskStatus) checkTime(cLang *ConfigLanguage) *TaskStatus {
if t.Verdict != VerdictAccepted { if t.Verdict != VerdictAccepted {
return t return t
} }
if t.info["real_time"].(float64) > float64(config.Runtime.TimeLimit)+5 { if t.info["real_time"].(float64) > float64(cLang.Runtime.Run.TimeLimit)+5 {
t.Verdict = VerdictTimeLimitExceeded t.Verdict = VerdictTimeLimitExceeded
t.Message = fmt.Sprintf("real_time: %v cpu_time: %v", t.info["real_time"], t.info["cpu_time"]) t.Message = fmt.Sprintf("real_time: %v cpu_time: %v", t.info["real_time"], t.info["cpu_time"])
} }
@ -110,12 +110,12 @@ func (t *TaskStatus) checkTime(config *Config) *TaskStatus {
return t return t
} }
func (t *TaskStatus) checkMemory(config *Config) *TaskStatus { func (t *TaskStatus) checkMemory(cLang *ConfigLanguage) *TaskStatus {
if t.Verdict != VerdictAccepted { if t.Verdict != VerdictAccepted {
return t return t
} }
if t.info["memory"].(float64) > float64((config.Runtime.MemoryLimit+1)*1024) { if t.info["memory"].(float64) > float64((cLang.Runtime.Run.MemoryLimit+1)*1024) {
t.Verdict = VerdictMemoryLimitExceeded t.Verdict = VerdictMemoryLimitExceeded
t.Message = fmt.Sprintf("memory: %v", t.info["memory"]) t.Message = fmt.Sprintf("memory: %v", t.info["memory"])
} }
@ -193,7 +193,7 @@ func (t *TaskStatus) checkJudge(pts *map[int]int32) *TaskStatus {
return t return t
} }
func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32) { func (s *service) CheckResults(meta *JudgeMeta, config *Config, cLang *ConfigLanguage) (*JudgeStatus, int32) {
// CE will be processed in phase compile // CE will be processed in phase compile
pts := map[int]int32{} pts := map[int]int32{}
@ -202,7 +202,7 @@ func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32)
} }
var results []TaskStatus var results []TaskStatus
dir := filepath.Join(UserDir, user) dir := filepath.Join(UserDir, meta.User)
var sum int32 = 0 var sum int32 = 0
for i := 1; i <= len(config.Tasks); i++ { for i := 1; i <= len(config.Tasks); i++ {
@ -213,8 +213,8 @@ func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32)
result.getInfoText(info). result.getInfoText(info).
getInfo(). getInfo().
checkTime(config). checkTime(cLang).
checkMemory(config). checkMemory(cLang).
checkExit(). checkExit().
getJudgeText(judge). getJudgeText(judge).
getJudge(). getJudge().
@ -224,5 +224,5 @@ func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32)
results = append(results, result) results = append(results, result)
} }
return JudgeStatus{Message: "", Tasks: results}, sum return &JudgeStatus{Message: "", Tasks: results}, sum
} }