From d42ee0ce54420c037d5fae1c9743c5d21d52e50f Mon Sep 17 00:00:00 2001 From: Paul Pan Date: Sat, 22 Oct 2022 17:38:39 +0800 Subject: [PATCH] feat: a big update 1. merge woj-runner scripts into woj-server 2. add woj-runner app 3. refactor submission status problem ... 4. jwt middleware update Co-authored-by: cxy004 Co-authored-by: wzt --- .github/workflows/docker-image.yml | 11 +- .gitignore | 1 + Dockerfile.runner | 11 + Dockerfile => Dockerfile.server | 4 +- Makefile | 25 +- cmd/app/main.go | 68 ------ cmd/app/run.go | 21 -- cmd/common.go | 75 ++++++ cmd/runner/main.go | 34 +++ cmd/server/main.go | 34 +++ config.yaml | 2 +- go.mod | 5 +- internal/api/problem/details.go | 45 ++++ internal/api/problem/handler.go | 10 +- internal/api/problem/search.go | 19 +- internal/api/problem/update.go | 48 ++-- internal/api/runner/build.go | 34 +++ internal/api/runner/handler.go | 41 ++++ internal/api/runner/judge.go | 73 ++++++ internal/api/status/handler.go | 32 +++ internal/api/status/query.go | 24 ++ internal/api/status/queryByVersion.go | 42 ++++ internal/api/submission/create.go | 64 +++++ internal/api/submission/handler.go | 41 ++++ internal/api/submission/query.go | 73 ++++++ internal/api/user/create.go | 11 +- internal/api/user/handler.go | 10 +- internal/api/user/login.go | 20 +- internal/api/user/logout.go | 1 + internal/api/user/profile.go | 24 +- internal/app/runner/runner.go | 43 ++++ internal/app/{app.go => server/server.go} | 43 +++- internal/e/code.go | 93 +++++-- internal/global/jwt.go | 2 +- internal/model/Language.go | 6 - internal/model/Problem.go | 27 ++- internal/model/Status.go | 17 +- internal/model/Submission.go | 13 +- internal/model/Task.go | 42 +++- internal/model/User.go | 2 +- internal/model/Verdict.go | 17 -- internal/router/api.go | 4 + internal/service/jwt/middleware.go | 47 ++-- internal/service/problem/create.go | 22 +- internal/service/problem/createVersion.go | 27 +++ internal/service/problem/query.go | 16 +- internal/service/problem/queryFuzz.go | 25 +- .../service/problem/queryLatestVersion.go | 29 +++ internal/service/problem/queryVersion.go | 27 +++ internal/service/problem/service.go | 13 +- internal/service/problem/update.go | 11 +- internal/service/problem/updateVersion.go | 17 ++ internal/service/runner/common.go | 75 ++++++ internal/service/runner/compile.go | 30 +++ internal/service/runner/config.go | 125 ++++++++++ internal/service/runner/deps.go | 31 +++ internal/service/runner/newProblem.go | 72 ++++++ internal/service/runner/runAndJudge.go | 22 ++ internal/service/runner/service.go | 34 +++ internal/service/runner/status.go | 227 ++++++++++++++++++ internal/service/status/create.go | 33 +++ internal/service/status/query.go | 56 +++++ internal/service/status/rejudge.go | 11 + internal/service/status/service.go | 30 +++ internal/service/submission/create.go | 31 +++ internal/service/submission/query.go | 33 +++ internal/service/submission/service.go | 28 +++ internal/service/task/common.go | 8 +- internal/service/task/judge.go | 20 -- internal/service/task/problem.go | 46 ++++ internal/service/task/push.go | 23 -- internal/service/task/service.go | 11 +- internal/service/task/submit.go | 55 +++++ internal/service/user/create.go | 21 +- internal/service/user/login.go | 11 +- internal/service/user/profile.go | 6 +- internal/service/user/service.go | 6 +- internal/service/user/version.go | 7 +- pkg/down/down.go | 55 +++++ pkg/unzip/unzip.go | 60 +++++ pkg/utils/file.go | 3 + pkg/zapasynq/logger.go | 25 ++ resource/runner/framework/scripts/setup.sh | 2 +- .../framework/template/default/c.Makefile | 4 +- .../framework/template/default/cpp.Makefile | 4 +- resource/runner/framework/template/setup.sh | 2 +- .../runner/problem/example/judge/XYZ.Makefile | 2 +- resource/runner/scripts/prepare_container.sh | 2 +- resource/runner/scripts/problem_compile.sh | 2 +- resource/runner/scripts/problem_judge.sh | 2 +- resource/runner/scripts/problem_prebuild.sh | 2 +- 91 files changed, 2257 insertions(+), 406 deletions(-) create mode 100644 Dockerfile.runner rename Dockerfile => Dockerfile.server (73%) delete mode 100644 cmd/app/main.go delete mode 100644 cmd/app/run.go create mode 100644 cmd/common.go create mode 100644 cmd/runner/main.go create mode 100644 cmd/server/main.go create mode 100644 internal/api/problem/details.go create mode 100644 internal/api/runner/build.go create mode 100644 internal/api/runner/handler.go create mode 100644 internal/api/runner/judge.go create mode 100644 internal/api/status/handler.go create mode 100644 internal/api/status/query.go create mode 100644 internal/api/status/queryByVersion.go create mode 100644 internal/api/submission/create.go create mode 100644 internal/api/submission/handler.go create mode 100644 internal/api/submission/query.go create mode 100644 internal/app/runner/runner.go rename internal/app/{app.go => server/server.go} (53%) delete mode 100644 internal/model/Language.go delete mode 100644 internal/model/Verdict.go create mode 100644 internal/service/problem/createVersion.go create mode 100644 internal/service/problem/queryLatestVersion.go create mode 100644 internal/service/problem/queryVersion.go create mode 100644 internal/service/problem/updateVersion.go create mode 100644 internal/service/runner/common.go create mode 100644 internal/service/runner/compile.go create mode 100644 internal/service/runner/config.go create mode 100644 internal/service/runner/deps.go create mode 100644 internal/service/runner/newProblem.go create mode 100644 internal/service/runner/runAndJudge.go create mode 100644 internal/service/runner/service.go create mode 100644 internal/service/runner/status.go create mode 100644 internal/service/status/create.go create mode 100644 internal/service/status/query.go create mode 100644 internal/service/status/rejudge.go create mode 100644 internal/service/status/service.go create mode 100644 internal/service/submission/create.go create mode 100644 internal/service/submission/query.go create mode 100644 internal/service/submission/service.go delete mode 100644 internal/service/task/judge.go create mode 100644 internal/service/task/problem.go delete mode 100644 internal/service/task/push.go create mode 100644 internal/service/task/submit.go create mode 100644 pkg/down/down.go create mode 100644 pkg/unzip/unzip.go create mode 100644 pkg/zapasynq/logger.go diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2c97491..e8a654c 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -18,14 +18,15 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker meta - id: meta + - name: Server Meta + id: server_meta uses: docker/metadata-action@v4 with: images: panpaul/woj-server - - name: Build and Push the Docker Image + - name: Build and Push the Server Image uses: docker/build-push-action@v3 with: push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.server_meta.outputs.tags }} + file: ./Dockerfile.server + labels: ${{ steps.server_meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 2d4c8dc..75afa1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ ### Project /server +/runner my.secrets ### JetBrains template diff --git a/Dockerfile.runner b/Dockerfile.runner new file mode 100644 index 0000000..af56a80 --- /dev/null +++ b/Dockerfile.runner @@ -0,0 +1,11 @@ +FROM golang:latest AS builder +WORKDIR /builder +COPY . /builder +RUN make runner + +FROM alpine:latest +WORKDIR /app +RUN apk --no-cache add tzdata ca-certificates libc6-compat +COPY --from=builder /builder/server /app +COPY --from=builder /builder/resource/runner /app/resource/runner +ENTRYPOINT ["/app/runner"] diff --git a/Dockerfile b/Dockerfile.server similarity index 73% rename from Dockerfile rename to Dockerfile.server index 4ddf587..a66ed21 100644 --- a/Dockerfile +++ b/Dockerfile.server @@ -1,12 +1,12 @@ FROM golang:latest AS builder WORKDIR /builder COPY . /builder -RUN make build +RUN make server FROM alpine:latest WORKDIR /app RUN apk --no-cache add tzdata ca-certificates libc6-compat COPY --from=builder /builder/server /app -COPY --from=builder /builder/resource /app/resource +COPY --from=builder /builder/resource/frontend /app/resource/frontend EXPOSE 8000 ENTRYPOINT ["/app/server"] diff --git a/Makefile b/Makefile index 2e82bd5..af08537 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,29 @@ -PROJECT=server - GO := go -LDFLAGS += -X main.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S') -LDFLAGS += -X main.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD) +LDFLAGS += -X cmd.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S') +LDFLAGS += -X cmd.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD) LDFLAGS += -s -w -GOBUILD := $(GO) build -o $(PROJECT) -ldflags '$(LDFLAGS)' ./cmd/app +GOBUILD := $(GO) build -ldflags '$(LDFLAGS)' GOBIN := $(shell go env GOPATH)/bin -.PHONY: all build clean run dep swagger fmt +.PHONY: all server runner build clean dep swagger fmt default: all all: clean build -build: swagger dep - $(GOBUILD) +server: swagger dep + $(GOBUILD) -o server ./cmd/server + +runner: dep + $(GOBUILD) -o runner ./cmd/runner + +build: runner server clean: - rm -f $(PROJECT) - -run: clean swagger dep build - ./$(PROJECT) run + rm -f runner + rm -f server dep: go mod tidy && go mod download diff --git a/cmd/app/main.go b/cmd/app/main.go deleted file mode 100644 index 0d1f51f..0000000 --- a/cmd/app/main.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "github.com/urfave/cli/v2" - "log" - "os" - "time" -) - -func main() { - a := &cli.App{ - Name: "OJ", - Usage: "woj-server", - Compiled: getBuildTime(), - Version: Version, - EnableBashCompletion: true, - Authors: []*cli.Author{ - { - Name: "Paul", - Email: "i@0x7f.app", - }, - { - Name: "cxy004", - Email: "cxy004@qq.com", - }, - { - Name: "wzt", - Email: "w.zhongtao@qq.com", - }, - }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "path to the config file", - Value: "config.yaml", - EnvVars: []string{"APP_CONFIG"}, - }, - }, - Commands: []*cli.Command{ - { - Name: "run", - Aliases: []string{"r"}, - Usage: "start the server", - Action: run, - }, - }, - } - - err := a.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} - -var ( - BuildTime = "2022-09-06-01-00-00" - Version = "1.0.0+None" -) - -func getBuildTime() time.Time { - build, err := time.Parse("2006-01-02-15-04-05", BuildTime) - if err != nil { - log.Printf("failed to parse build time: %v", err) - build = time.Now() - } - return build -} diff --git a/cmd/app/run.go b/cmd/app/run.go deleted file mode 100644 index 0da7fbc..0000000 --- a/cmd/app/run.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "github.com/WHUPRJ/woj-server/internal/app" - "github.com/WHUPRJ/woj-server/internal/global" - "github.com/urfave/cli/v2" - "math/rand" - "time" -) - -func run(c *cli.Context) error { - rand.Seed(time.Now().Unix()) - - g := new(global.Global) - g.SetupConfig(c.String("config")) - g.SetupZap() - defer func() { _ = g.Log.Sync() }() - - g.Log.Info("starting server...") - return app.Run(g) -} diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 0000000..377871f --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/urfave/cli/v2" + "log" + "math/rand" + "time" +) + +var App = &cli.App{ + Name: "WOJ", + EnableBashCompletion: true, + Authors: []*cli.Author{ + { + Name: "Paul", + Email: "i@0x7f.app", + }, + { + Name: "cxy004", + Email: "cxy004@qq.com", + }, + { + Name: "wzt", + Email: "w.zhongtao@qq.com", + }, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "path to the config file", + Value: "config.yaml", + EnvVars: []string{"APP_CONFIG"}, + }, + }, +} + +var ( + BuildTime string + Version string +) + +func init() { + if BuildTime == "" { + BuildTime = "2022-09-06-01-00-00" + } + App.Compiled = getBuildTime() + + if Version == "" { + Version = "0.0.0+None" + } + App.Version = Version +} + +func getBuildTime() time.Time { + build, err := time.Parse("2006-01-02-15-04-05", BuildTime) + if err != nil { + log.Printf("failed to parse build time: %v", err) + build = time.Now() + } + return build +} + +func CommonSetup(c *cli.Context) *global.Global { + rand.Seed(time.Now().Unix()) + + g := new(global.Global) + g.SetupConfig(c.String("config")) + g.SetupZap() + + g.Log.Info("starting...") + + return g +} diff --git a/cmd/runner/main.go b/cmd/runner/main.go new file mode 100644 index 0000000..aa7f406 --- /dev/null +++ b/cmd/runner/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/WHUPRJ/woj-server/cmd" + "github.com/WHUPRJ/woj-server/internal/app/runner" + "github.com/urfave/cli/v2" + "log" + "os" +) + +func main() { + a := cmd.App + a.Usage = "woj-runner" + a.Commands = []*cli.Command{ + { + Name: "run", + Aliases: []string{"r"}, + Usage: "start the runner", + Action: run, + }, + } + + err := a.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func run(c *cli.Context) error { + g := cmd.CommonSetup(c) + defer func() { _ = g.Log.Sync() }() + + return runner.RunRunner(g) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..4b23945 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/WHUPRJ/woj-server/cmd" + "github.com/WHUPRJ/woj-server/internal/app/server" + "github.com/urfave/cli/v2" + "log" + "os" +) + +func main() { + a := cmd.App + a.Usage = "woj-server" + a.Commands = []*cli.Command{ + { + Name: "run", + Aliases: []string{"r"}, + Usage: "start the server", + Action: run, + }, + } + + err := a.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func run(c *cli.Context) error { + g := cmd.CommonSetup(c) + defer func() { _ = g.Log.Sync() }() + + return server.RunServer(g) +} diff --git a/config.yaml b/config.yaml index de46fd7..7ff81ec 100644 --- a/config.yaml +++ b/config.yaml @@ -6,7 +6,7 @@ WebServer: Redis: Db: 0 - QueueDb: 0 + QueueDb: 1 Address: '127.0.0.1:6379' Password: '' diff --git a/go.mod b/go.mod index 79d3d95..f3d872a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt/v4 v4.4.2 github.com/hibiken/asynq v0.23.0 - github.com/lib/pq v1.10.2 + github.com/jackc/pgtype v1.11.0 github.com/prometheus/client_golang v1.13.0 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.3 @@ -18,6 +18,7 @@ require ( github.com/urfave/cli/v2 v2.14.1 go.uber.org/zap v1.23.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.3.9 gorm.io/gorm v1.23.8 @@ -49,7 +50,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/pgx/v4 v4.16.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.4 // indirect @@ -74,7 +74,6 @@ require ( go.uber.org/multierr v1.7.0 // indirect golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect - golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect golang.org/x/tools v0.1.10 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/internal/api/problem/details.go b/internal/api/problem/details.go new file mode 100644 index 0000000..f9c42b3 --- /dev/null +++ b/internal/api/problem/details.go @@ -0,0 +1,45 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type detailsRequest struct { + Pid uint `form:"pid"` +} + +// Details +// @Summary get details of a problem +// @Description get details of a problem +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int true "problem id" +// @Response 200 {object} e.Response "problem details" +// @Router /v1/problem/details [post] +func (h *handler) Details(c *gin.Context) { + req := new(detailsRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + claim, exist := c.Get("claim") + shouldEnable := !exist || claim.(*global.Claim).Role < model.RoleAdmin + + p, status := h.problemService.Query(req.Pid, true, shouldEnable) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + pv, status := h.problemService.QueryLatestVersion(req.Pid) + e.Pong(c, status, gin.H{ + "problem": p, + "context": pv.Context, + }) + return +} diff --git a/internal/api/problem/handler.go b/internal/api/problem/handler.go index 12378f8..4e3a9a2 100644 --- a/internal/api/problem/handler.go +++ b/internal/api/problem/handler.go @@ -10,23 +10,25 @@ import ( var _ Handler = (*handler)(nil) type Handler interface { - Update(c *gin.Context) + Details(c *gin.Context) Search(c *gin.Context) + Update(c *gin.Context) } type handler struct { log *zap.Logger - problemService problem.Service jwtService global.JwtService + problemService problem.Service } func RouteRegister(g *global.Global, group *gin.RouterGroup) { app := &handler{ log: g.Log, - problemService: problem.NewService(g), jwtService: g.Jwt, + problemService: problem.NewService(g), } + group.POST("/details", app.jwtService.Handler(false), app.Details) group.POST("/search", app.Search) - group.POST("/update", app.jwtService.Handler(), app.Update) + group.POST("/update", app.jwtService.Handler(true), app.Update) } diff --git a/internal/api/problem/search.go b/internal/api/problem/search.go index 5e932f6..4384647 100644 --- a/internal/api/problem/search.go +++ b/internal/api/problem/search.go @@ -6,7 +6,6 @@ import ( ) type searchRequest struct { - Pid uint `form:"pid"` Search string `form:"search"` } @@ -15,9 +14,8 @@ type searchRequest struct { // @Description get detail of a problem // @Accept application/x-www-form-urlencoded // @Produce json -// @Param pid formData int false "problem id" -// @Param search formData string false "search problem" -// @Response 200 {object} e.Response "problem info" +// @Param search formData string false "word search" +// @Response 200 {object} e.Response "problemset" // @Router /v1/problem/search [post] func (h *handler) Search(c *gin.Context) { req := new(searchRequest) @@ -27,17 +25,14 @@ func (h *handler) Search(c *gin.Context) { return } - if req.Pid == 0 && req.Search == "" { - e.Pong(c, e.InvalidParameter, nil) - return - } - - if req.Pid != 0 { - problem, status := h.problemService.Query(req.Pid) + // TODO: pagination + if req.Search == "" { + // TODO: query without LIKE + problem, status := h.problemService.QueryFuzz(req.Search, true, true) e.Pong(c, status, problem) return } else { - problem, status := h.problemService.QueryFuzz(req.Search) + problem, status := h.problemService.QueryFuzz(req.Search, true, true) e.Pong(c, status, problem) return } diff --git a/internal/api/problem/update.go b/internal/api/problem/update.go index 03d5bd3..6905b16 100644 --- a/internal/api/problem/update.go +++ b/internal/api/problem/update.go @@ -4,16 +4,15 @@ import ( "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/problem" "github.com/gin-gonic/gin" ) type updateRequest struct { - Pid uint `form:"pid"` - Title string `form:"title" binding:"required"` - Content string `form:"content" binding:"required"` - TimeLimit uint `form:"time_limit" binding:"required"` - MemoryLimit uint `form:"memory_limit" binding:"required"` - IsEnabled bool `form:"is_enabled"` + Pid uint `form:"pid"` + Title string `form:"title" binding:"required"` + Statement string `form:"statement" binding:"required"` + IsEnabled bool `form:"is_enabled"` } // Update @@ -23,9 +22,7 @@ type updateRequest struct { // @Produce json // @Param pid formData int false "problem id, 0 for create" // @Param title formData string true "title" -// @Param content formData string true "content" -// @Param time_limit formData int true "time limit in ms" -// @Param memory_limit formData int true "memory limit in kb" +// @Param statement formData string true "statement" // @Param is_enabled formData bool false "is enabled" // @Response 200 {object} e.Response "problem info without provider information" // @Security Authentication @@ -50,32 +47,33 @@ func (h *handler) Update(c *gin.Context) { return } - problem := &model.Problem{ - Title: req.Title, - Content: req.Content, - TimeLimit: req.TimeLimit, - MemoryLimit: req.MemoryLimit, - IsEnabled: req.IsEnabled, - } - if req.Pid == 0 { - problem, status := h.problemService.Create(uid, problem) - e.Pong(c, status, problem) + createData := &problem.CreateData{ + Title: req.Title, + Statement: req.Statement, + ProviderID: uid, + IsEnabled: false, + } + p, status := h.problemService.Create(createData) + e.Pong(c, status, p) return } else { - inDb, status := h.problemService.Query(req.Pid) - if status != e.Success && status != e.ProblemNotAvailable { + p, status := h.problemService.Query(req.Pid, true, false) + if status != e.Success { e.Pong(c, status, nil) return } - - if inDb.ProviderID != uid { + if p.ProviderID != uid { e.Pong(c, e.UserUnauthorized, nil) return } - problem, status := h.problemService.Update(req.Pid, problem) - e.Pong(c, status, problem) + p.Title = req.Title + p.Statement = req.Statement + p.IsEnabled = req.IsEnabled + + p, status = h.problemService.Update(p) + e.Pong(c, status, p) return } } diff --git a/internal/api/runner/build.go b/internal/api/runner/build.go new file mode 100644 index 0000000..1f7f74a --- /dev/null +++ b/internal/api/runner/build.go @@ -0,0 +1,34 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +func (h *handler) Build(_ context.Context, t *asynq.Task) error { + // TODO: configure timeout with context + + var p model.ProblemBuildPayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + h.log.Info("build", zap.Any("payload", p)) + + config, status := h.runnerService.NewProblem(p.ProblemVersionID, p.ProblemFile) + + for i := range config.Languages { + config.Languages[i].Type = "" + config.Languages[i].Script = "" + config.Languages[i].Cmp = "" + } + + b, _ := json.Marshal(config) + h.taskService.ProblemUpdate(status, p.ProblemVersionID, string(b)) + + return nil +} diff --git a/internal/api/runner/handler.go b/internal/api/runner/handler.go new file mode 100644 index 0000000..d8ddbff --- /dev/null +++ b/internal/api/runner/handler.go @@ -0,0 +1,41 @@ +package runner + +import ( + "context" + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Build(_ context.Context, t *asynq.Task) error + Judge(_ context.Context, t *asynq.Task) error +} + +type handler struct { + log *zap.Logger + runnerService runner.Service + taskService task.Service +} + +func NewRunner(g *global.Global) (Handler, error) { + hnd := &handler{ + log: g.Log, + runnerService: runner.NewService(g), + taskService: task.NewService(g), + } + + status := hnd.runnerService.EnsureDeps(false) + if status != e.Success { + g.Log.Error("failed to ensure runner dependencies", zap.String("status", status.String())) + return nil, errors.New("failed to ensure dependencies") + } + + return hnd, nil +} diff --git a/internal/api/runner/judge.go b/internal/api/runner/judge.go new file mode 100644 index 0000000..594af4d --- /dev/null +++ b/internal/api/runner/judge.go @@ -0,0 +1,73 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "path/filepath" +) + +func (h *handler) Judge(_ context.Context, t *asynq.Task) error { + var p model.SubmitJudgePayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + user := utils.RandomString(16) + h.log.Info("judge", zap.Any("payload", p), zap.String("user", user)) + + // common + systemError := runner.JudgeStatus{Message: "System Error"} + + // write code + userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language)) + if !utils.FileTouch(userCode) { + h.log.Info("Touch file failed", zap.String("userCode", userCode)) + h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError) + return nil + } + err := utils.FileWrite(userCode, []byte(p.Submission.Code)) + if err != nil { + h.log.Info("Write file failed", zap.String("code", p.Submission.Code)) + h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError) + return nil + } + + // compile + result, status := h.runnerService.Compile(p.ProblemVersionId, user, p.Submission.Language) + if status == e.RunnerProblemNotExist { + _, status := h.runnerService.NewProblem(p.ProblemVersionId, p.StorageKey) + if status != e.Success { + h.log.Warn("download problem failed", + zap.Any("status", status), + zap.Uint("pvid", p.ProblemVersionId), + zap.String("storageKey", p.StorageKey)) + h.taskService.SubmitUpdate(status, p.ProblemVersionId, 0, systemError) + return nil + } + } else if status != e.Success { + h.taskService.SubmitUpdate(status, p.Submission.ID, 0, result) + return nil + } + + // config + config, err := h.runnerService.ParseConfig(p.ProblemVersionId, true) + if err != nil { + h.log.Info("parse config failed", zap.Error(err), zap.Uint("pvid", p.ProblemVersionId)) + h.taskService.SubmitUpdate(e.InternalError, p.ProblemVersionId, 0, systemError) + return nil + } + + // run + var points int32 + result, points, status = h.runnerService.RunAndJudge(p.ProblemVersionId, user, p.Submission.Language, &config) + h.taskService.SubmitUpdate(status, p.Submission.ID, points, result) + + return nil +} diff --git a/internal/api/status/handler.go b/internal/api/status/handler.go new file mode 100644 index 0000000..1b85a77 --- /dev/null +++ b/internal/api/status/handler.go @@ -0,0 +1,32 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Query(c *gin.Context) + QueryByProblemVersion(c *gin.Context) +} + +type handler struct { + log *zap.Logger + statusService status.Service + jwtService global.JwtService +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + statusService: status.NewService(g), + jwtService: g.Jwt, + } + + group.POST("/query", app.Query) + group.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion) +} diff --git a/internal/api/status/query.go b/internal/api/status/query.go new file mode 100644 index 0000000..54b353d --- /dev/null +++ b/internal/api/status/query.go @@ -0,0 +1,24 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/gin-gonic/gin" +) + +type queryRequest struct { + SubmissionID uint `form:"sid"` +} + +func (h *handler) Query(c *gin.Context) { + req := new(queryRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + status, eStatus := h.statusService.Query(req.SubmissionID, true) + + e.Pong(c, eStatus, status) + return +} diff --git a/internal/api/status/queryByVersion.go b/internal/api/status/queryByVersion.go new file mode 100644 index 0000000..d7e1728 --- /dev/null +++ b/internal/api/status/queryByVersion.go @@ -0,0 +1,42 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type queryByVersionRequest struct { + ProblemVersionID uint `form:"pvid"` + Offset int `form:"offset"` + Limit int `form:"limit"` +} + +func (h *handler) QueryByProblemVersion(c *gin.Context) { + + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + role := claim.(*global.Claim).Role + + req := new(queryByVersionRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, nil) + return + } + + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + statuses, eStatus := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) + + e.Pong(c, eStatus, statuses) + return +} diff --git a/internal/api/submission/create.go b/internal/api/submission/create.go new file mode 100644 index 0000000..a49fea4 --- /dev/null +++ b/internal/api/submission/create.go @@ -0,0 +1,64 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/submission" + "github.com/gin-gonic/gin" +) + +type createRequest struct { + Pid uint `form:"pid" binding:"required"` + Language string `form:"language" binding:"required"` + Code string `form:"statement" binding:"required"` +} + +// Create +// @Summary create a submission +// @Description create a submission +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int true "problem id" +// @Param language formData string true "language" +// @Param code formData string true "code" +// @Response 200 {object} e.Response "" +// @Security Authentication +// @Router /v1/submission/create [post] +func (h *handler) Create(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + uid := claim.(*global.Claim).UID + + req := new(createRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + createData := &submission.CreateData{ + ProblemID: req.Pid, + UserID: uid, + Language: req.Language, + Code: req.Code, + } + s, status := h.submissionService.Create(createData) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + pv, status := h.problemService.QueryLatestVersion(req.Pid) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + _, status = h.taskService.SubmitJudge(pv.ID, pv.StorageKey, *s) + e.Pong(c, status, nil) + return +} diff --git a/internal/api/submission/handler.go b/internal/api/submission/handler.go new file mode 100644 index 0000000..9a0c9c3 --- /dev/null +++ b/internal/api/submission/handler.go @@ -0,0 +1,41 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/WHUPRJ/woj-server/internal/service/submission" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Create(c *gin.Context) + Query(c *gin.Context) +} + +type handler struct { + log *zap.Logger + jwtService global.JwtService + problemService problem.Service + statusService status.Service + submissionService submission.Service + taskService task.Service +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + jwtService: g.Jwt, + problemService: problem.NewService(g), + statusService: status.NewService(g), + submissionService: submission.NewService(g), + taskService: task.NewService(g), + } + + group.POST("/create", app.jwtService.Handler(true), app.Create) + group.POST("/query", app.Query) +} diff --git a/internal/api/submission/query.go b/internal/api/submission/query.go new file mode 100644 index 0000000..7259fdf --- /dev/null +++ b/internal/api/submission/query.go @@ -0,0 +1,73 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type queryRequest struct { + Pid uint `form:"pid"` + Uid uint `form:"uid"` + Offset int `form:"offset"` + Limit int `form:"limit"` +} + +type queryResponse struct { + Submission model.Submission `json:"submission"` + Point int32 `json:"point"` +} + +// Query +// @Summary Query submissions +// @Description Query submissions +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData uint false "problem id" +// @Param uid formData uint false "user id" +// @Param offset formData int false "start position" +// @Param limit formData int false "limit number of records" +// @Response 200 {object} e.Response "queryResponse" +// @Router /v1/submission/query [post] + +func (h *handler) Query(c *gin.Context) { + req := new(queryRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + if req.Pid == 0 && req.Uid == 0 { + e.Pong(c, e.InvalidParameter, nil) + return + } + + submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit) + + var response []*queryResponse + + for _, submission := range submissions { + currentStatus, _ := h.statusService.Query(submission.ID, false) + var currentPoint int32 + + if currentStatus == nil { + currentPoint = -1 + } else { + currentPoint = currentStatus.Point + } + + newResponse := &queryResponse{ + Submission: *submission, + Point: currentPoint, + } + + newResponse.Submission.Code = "" + + response = append(response, newResponse) + } + + e.Pong(c, status, submissions) + return + +} diff --git a/internal/api/user/create.go b/internal/api/user/create.go index 8e2dc25..f8c8fb0 100644 --- a/internal/api/user/create.go +++ b/internal/api/user/create.go @@ -8,9 +8,9 @@ import ( ) type createRequest struct { - Username string `form:"username" binding:"required"` - Nickname string `form:"nickname" binding:"required"` + UserName string `form:"username" binding:"required"` Password string `form:"password" binding:"required"` + NickName string `form:"nickname" binding:"required"` } // Create @@ -32,11 +32,10 @@ func (h *handler) Create(c *gin.Context) { } createData := &user.CreateData{ - Username: req.Username, - Nickname: req.Nickname, + UserName: req.UserName, Password: req.Password, + NickName: req.NickName, } - u, status := h.userService.Create(createData) if status != e.Success { e.Pong(c, status, nil) @@ -48,6 +47,7 @@ func (h *handler) Create(c *gin.Context) { e.Pong(c, status, nil) return } + claim := &global.Claim{ UID: u.ID, Role: u.Role, @@ -55,4 +55,5 @@ func (h *handler) Create(c *gin.Context) { } token, status := h.jwtService.SignClaim(claim) e.Pong(c, status, token) + return } diff --git a/internal/api/user/handler.go b/internal/api/user/handler.go index 1dcc8b5..70785b1 100644 --- a/internal/api/user/handler.go +++ b/internal/api/user/handler.go @@ -18,19 +18,19 @@ type Handler interface { type handler struct { log *zap.Logger - userService user.Service jwtService global.JwtService + userService user.Service } func RouteRegister(g *global.Global, group *gin.RouterGroup) { app := &handler{ log: g.Log, - userService: user.NewService(g), jwtService: g.Jwt, + userService: user.NewService(g), } - group.POST("/login", app.Login) group.POST("/create", app.Create) - group.POST("/logout", app.jwtService.Handler(), app.Logout) - group.POST("/profile", app.jwtService.Handler(), app.Profile) + group.POST("/login", app.Login) + group.POST("/logout", app.jwtService.Handler(true), app.Logout) + group.POST("/profile", app.jwtService.Handler(true), app.Profile) } diff --git a/internal/api/user/login.go b/internal/api/user/login.go index 1252c33..fdd3243 100644 --- a/internal/api/user/login.go +++ b/internal/api/user/login.go @@ -3,12 +3,12 @@ package user import ( "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/global" - "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/user" "github.com/gin-gonic/gin" ) type loginRequest struct { - Username string `form:"username" binding:"required"` + UserName string `form:"username" binding:"required"` Password string `form:"password" binding:"required"` } @@ -30,30 +30,30 @@ func (h *handler) Login(c *gin.Context) { } // check password - userData := &model.User{ - UserName: req.Username, - Password: []byte(req.Password), + loginData := &user.LoginData{ + UserName: req.UserName, + Password: req.Password, } - user, status := h.userService.Login(userData) + u, status := h.userService.Login(loginData) if status != e.Success { e.Pong(c, status, nil) return } // sign and return token - version, status := h.userService.IncrVersion(user.ID) + version, status := h.userService.IncrVersion(u.ID) if status != e.Success { e.Pong(c, status, nil) return } claim := &global.Claim{ - UID: user.ID, - Role: user.Role, + UID: u.ID, + Role: u.Role, Version: version, } token, status := h.jwtService.SignClaim(claim) e.Pong(c, status, gin.H{ "token": token, - "nickname": user.NickName, + "nickname": u.NickName, }) } diff --git a/internal/api/user/logout.go b/internal/api/user/logout.go index f9a288c..c2554ec 100644 --- a/internal/api/user/logout.go +++ b/internal/api/user/logout.go @@ -23,4 +23,5 @@ func (h *handler) Logout(c *gin.Context) { _, status := h.userService.IncrVersion(claim.(*global.Claim).UID) e.Pong(c, status, nil) + return } diff --git a/internal/api/user/profile.go b/internal/api/user/profile.go index 07a2f45..072ed3a 100644 --- a/internal/api/user/profile.go +++ b/internal/api/user/profile.go @@ -31,18 +31,22 @@ func (h *handler) Profile(c *gin.Context) { uid := claim.(*global.Claim).UID role := claim.(*global.Claim).Role + req := new(profileRequest) - if err := c.ShouldBind(req); err == nil { - if req.UID != 0 && req.UID != uid { - if role >= model.RoleGeneral { - uid = req.UID - } else { - e.Pong(c, e.UserUnauthorized, nil) - return - } - } + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, nil) + return } - user, status := h.userService.Profile(uid) + if req.UID == 0 { + req.UID = uid + } else if req.UID != uid && role < model.RoleGeneral { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + user, status := h.userService.Profile(req.UID) e.Pong(c, status, user) + return } diff --git a/internal/app/runner/runner.go b/internal/app/runner/runner.go new file mode 100644 index 0000000..a57aa36 --- /dev/null +++ b/internal/app/runner/runner.go @@ -0,0 +1,43 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/api/runner" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/WHUPRJ/woj-server/pkg/zapasynq" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "runtime" +) + +func RunRunner(g *global.Global) error { + hnd, err := runner.NewRunner(g) + if err != nil { + return err + } + + mux := asynq.NewServeMux() + mux.HandleFunc(model.TypeProblemBuild, hnd.Build) + mux.HandleFunc(model.TypeSubmitJudge, hnd.Judge) + + srv := asynq.NewServer( + asynq.RedisClientOpt{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.QueueDb, + }, + asynq.Config{ + Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int), + Logger: zapasynq.New(g.Log), + Queues: map[string]int{model.QueueRunner: 1}, + }, + ) + + if err := srv.Run(mux); err != nil { + g.Log.Warn("could not run server", zap.Error(err)) + return err + } + + return nil +} diff --git a/internal/app/app.go b/internal/app/server/server.go similarity index 53% rename from internal/app/app.go rename to internal/app/server/server.go index 58139f5..ed7d4a9 100644 --- a/internal/app/app.go +++ b/internal/app/server/server.go @@ -1,22 +1,27 @@ -package app +package server import ( "context" "fmt" "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" "github.com/WHUPRJ/woj-server/internal/repo/postgresql" "github.com/WHUPRJ/woj-server/internal/repo/redis" "github.com/WHUPRJ/woj-server/internal/router" "github.com/WHUPRJ/woj-server/internal/service/jwt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/WHUPRJ/woj-server/pkg/zapasynq" + "github.com/hibiken/asynq" "go.uber.org/zap" "net/http" "os" "os/signal" + "runtime" "syscall" "time" ) -func Run(g *global.Global) error { +func RunServer(g *global.Global) error { // Setup Database g.Db = new(postgresql.Repo) g.Db.Setup(g) @@ -29,13 +34,13 @@ func Run(g *global.Global) error { g.Jwt = jwt.NewJwtService(g) // Prepare Router - handler := router.InitRouters(g) + routers := router.InitRouters(g) // Create Server addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port) server := &http.Server{ Addr: addr, - Handler: handler, + Handler: routers, } // Run Server @@ -45,19 +50,47 @@ func Run(g *global.Global) error { } }() + // Create Queue + queueMux := asynq.NewServeMux() + // TODO: fill + queueMux.HandleFunc(model.TypeProblemUpdate, func(ctx context.Context, t *asynq.Task) error { return nil }) + queueMux.HandleFunc(model.TypeSubmitUpdate, func(ctx context.Context, t *asynq.Task) error { return nil }) + queueSrv := asynq.NewServer( + asynq.RedisClientOpt{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.QueueDb, + }, + asynq.Config{ + Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int), + Logger: zapasynq.New(g.Log), + Queues: map[string]int{model.QueueServer: 1}, + }, + ) + + // Run Queue + if err := queueSrv.Start(queueMux); err != nil { + g.Log.Fatal("queueSrv.Start Failed", zap.Error(err)) + } + // Handle SIGINT and SIGTERM. quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit g.Log.Info("Shutting down server ...") - // Graceful Shutdown + // Graceful Shutdown Server ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := server.Shutdown(ctx) if err != nil { g.Log.Warn("Server Shutdown Failed", zap.Error(err)) } + + // Graceful Shutdown Queue + queueSrv.Shutdown() + + // Graceful Shutdown Database err = g.Db.Close() if err != nil { g.Log.Warn("Database Close Failed", zap.Error(err)) diff --git a/internal/e/code.go b/internal/e/code.go index d23f510..9161c73 100644 --- a/internal/e/code.go +++ b/internal/e/code.go @@ -1,35 +1,61 @@ package e const ( - Success Status = 0 - Unknown Status = 1 + Success Status = iota + Unknown +) - InternalError Status = 100 - InvalidParameter Status = 101 - NotFound Status = 102 - DatabaseError Status = 103 - RedisError Status = 104 +const ( + InternalError Status = 100 + iota + InvalidParameter + NotFound + DatabaseError + RedisError +) - TokenUnknown Status = 200 - TokenEmpty Status = 201 - TokenMalformed Status = 202 - TokenTimeError Status = 203 - TokenInvalid Status = 204 - TokenSignError Status = 205 - TokenRevoked Status = 206 +const ( + TokenUnknown Status = 200 + iota + TokenEmpty + TokenMalformed + TokenTimeError + TokenInvalid + TokenSignError + TokenRevoked +) - UserNotFound Status = 300 - UserWrongPassword Status = 301 - UserDuplicated Status = 302 - UserUnauthenticated Status = 303 - UserUnauthorized Status = 304 - UserDisabled Status = 305 +const ( + UserNotFound Status = 300 + iota + UserWrongPassword + UserDuplicated + UserUnauthenticated + UserUnauthorized + UserDisabled +) - ProblemNotFound Status = 500 - ProblemNotAvailable Status = 501 +const ( + ProblemNotFound Status = 500 + iota + ProblemNotAvailable + ProblemVersionNotFound + ProblemVersionNotAvailable + StatusNotFound +) - TaskEnqueueFailed Status = 600 - TaskGetInfoFailed Status = 601 +const ( + TaskEnqueueFailed Status = 600 + iota + TaskGetInfoFailed +) + +const ( + RunnerDepsBuildFailed Status = 700 + iota + RunnerDownloadFailed + RunnerUnzipFailed + RunnerProblemNotExist + RunnerProblemPrebuildFailed + RunnerProblemParseFailed + RunnerUserNotExist + RunnerUserCompileFailed + RunnerRunFailed + RunnerJudgeFailed ) var msgText = map[Status]string{ @@ -57,9 +83,24 @@ var msgText = map[Status]string{ UserUnauthorized: "User Unauthorized", UserDisabled: "User Disabled", - ProblemNotFound: "Problem Not Found", - ProblemNotAvailable: "Problem Not Available", + ProblemNotFound: "Problem Not Found", + ProblemNotAvailable: "Problem Not Available", + ProblemVersionNotFound: "Problem Version Not Found", + ProblemVersionNotAvailable: "Problem Version Not Available", + + StatusNotFound: "Status Not Found", TaskEnqueueFailed: "Task Enqueue Failed", TaskGetInfoFailed: "Task Get Info Failed", + + RunnerDepsBuildFailed: "Runner Deps Build Failed", + RunnerDownloadFailed: "Runner Download Failed", + RunnerUnzipFailed: "Runner Unzip Failed", + RunnerProblemNotExist: "Runner Problem Not Exist", + RunnerProblemPrebuildFailed: "Runner Problem Prebuild Failed", + RunnerProblemParseFailed: "Runner Problem Parse Failed", + RunnerUserNotExist: "Runner User Not Exist", + RunnerUserCompileFailed: "Runner User Compile Failed", + RunnerRunFailed: "Runner Run Failed", + RunnerJudgeFailed: "Runner Judge Failed", } diff --git a/internal/global/jwt.go b/internal/global/jwt.go index cac10dd..b39faf2 100644 --- a/internal/global/jwt.go +++ b/internal/global/jwt.go @@ -19,5 +19,5 @@ type JwtService interface { SignClaim(claim *Claim) (string, e.Status) Validate(claim *Claim) bool - Handler() gin.HandlerFunc + Handler(forced bool) gin.HandlerFunc } diff --git a/internal/model/Language.go b/internal/model/Language.go deleted file mode 100644 index 9a30357..0000000 --- a/internal/model/Language.go +++ /dev/null @@ -1,6 +0,0 @@ -package model - -const ( - LangC int32 = iota - LangCPP -) diff --git a/internal/model/Problem.go b/internal/model/Problem.go index 458daf2..c72b0b9 100644 --- a/internal/model/Problem.go +++ b/internal/model/Problem.go @@ -1,20 +1,23 @@ package model import ( - "github.com/lib/pq" + "github.com/jackc/pgtype" "gorm.io/gorm" ) type Problem struct { - gorm.Model `json:"meta"` - Title string `json:"title" gorm:"not null"` - Content string `json:"content" gorm:"not null"` - TimeLimit uint `json:"time_limit" gorm:"not null"` - MemoryLimit uint `json:"memory_limit" gorm:"not null"` - ProviderID uint `json:"provider_id" gorm:"not null;index"` - Provider User `json:"-" gorm:"foreignKey:ProviderID"` - Languages pq.Int32Array `json:"languages" gorm:"type:int[]"` - Points pq.Int32Array `json:"points" gorm:"type:int[]"` - StorageKey string `json:"storage_key" gorm:"not null"` - IsEnabled bool `json:"is_enabled" gorm:"not null;index"` + gorm.Model `json:"meta"` + Title string `json:"title" gorm:"not null"` + Statement string `json:"statement" gorm:"not null"` + ProviderID uint `json:"-" gorm:"not null;index"` + Provider User `json:"provider" gorm:"foreignKey:ProviderID"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` +} + +type ProblemVersion struct { + gorm.Model `json:"meta"` + ProblemID uint `json:"-" gorm:"not null;index"` + Context pgtype.JSON `json:"context" gorm:"type:json"` + StorageKey string `json:"-" gorm:"not null"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` } diff --git a/internal/model/Status.go b/internal/model/Status.go index 76fab37..2e14cf1 100644 --- a/internal/model/Status.go +++ b/internal/model/Status.go @@ -1,11 +1,16 @@ package model -import "gorm.io/gorm" +import ( + "github.com/jackc/pgtype" + "gorm.io/gorm" +) type Status struct { - gorm.Model `json:"-"` - SubmissionID uint `json:"submission_id" gorm:"not null;index"` - Submission Submission `json:"-" gorm:"foreignKey:SubmissionID"` - Verdict Verdict `json:"verdict" gorm:"not null"` - Point int32 `json:"point" gorm:"not null"` + gorm.Model `json:"meta"` + SubmissionID uint `json:"-" gorm:"not null;index"` + Submission Submission `json:"submission" gorm:"foreignKey:SubmissionID"` + ProblemVersionID uint `json:"problem_version_id" gorm:"not null;index"` + Context pgtype.JSON `json:"context" gorm:"type:json;not null"` + Point int32 `json:"point" gorm:"not null"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` } diff --git a/internal/model/Submission.go b/internal/model/Submission.go index 66b61ad..37bb14d 100644 --- a/internal/model/Submission.go +++ b/internal/model/Submission.go @@ -3,11 +3,10 @@ package model import "gorm.io/gorm" type Submission struct { - gorm.Model `json:"-"` - ProblemID uint `json:"problem_id" gorm:"not null;index"` - Problem Problem `json:"-" gorm:"foreignKey:ProblemID"` - UserID uint `json:"user_id" gorm:"not null;index"` - User User `json:"-" gorm:"foreignKey:UserID"` - Language int32 `json:"language" gorm:"not null"` - Code string `json:"code" gorm:"not null"` + gorm.Model `json:"meta"` + ProblemID uint `json:"problem_id" gorm:"not null;index"` + UserID uint `json:"-" gorm:"not null;index"` + User User `json:"user" gorm:"foreignKey:UserID"` + Language string `json:"language" gorm:"not null"` + Code string `json:"code" gorm:"not null"` } diff --git a/internal/model/Task.go b/internal/model/Task.go index 453ea8a..ad2096a 100644 --- a/internal/model/Task.go +++ b/internal/model/Task.go @@ -1,15 +1,41 @@ package model -const ( - TypeProblemPush = "problem:push" - TypeSubmitJudge = "submit:judge" +import ( + "github.com/WHUPRJ/woj-server/internal/e" ) -type ProblemPushPayload struct { - ProblemID uint - ProblemFile string +const ( + TypeProblemBuild = "problem:build" + TypeProblemUpdate = "problem:update" + TypeSubmitJudge = "submit:judge" + TypeSubmitUpdate = "submit:update" +) + +const ( + QueueServer = "server" + QueueRunner = "runner" +) + +type ProblemBuildPayload struct { + ProblemVersionID uint + ProblemFile string } -type SubmitJudge struct { - Submission Submission +type ProblemUpdatePayload struct { + Status e.Status + ProblemVersionID uint + Context string +} + +type SubmitJudgePayload struct { + ProblemVersionId uint + StorageKey string + Submission Submission +} + +type SubmitUpdatePayload struct { + Status e.Status + Sid uint + Point int32 + Context string } diff --git a/internal/model/User.go b/internal/model/User.go index e2c7644..cc6507a 100644 --- a/internal/model/User.go +++ b/internal/model/User.go @@ -5,7 +5,7 @@ import ( ) type User struct { - gorm.Model `json:"-"` + gorm.Model `json:"meta"` UserName string `json:"user_name" gorm:"not null;uniqueIndex"` NickName string `json:"nick_name" gorm:"not null"` Role Role `json:"role" gorm:"not null"` diff --git a/internal/model/Verdict.go b/internal/model/Verdict.go deleted file mode 100644 index 3551512..0000000 --- a/internal/model/Verdict.go +++ /dev/null @@ -1,17 +0,0 @@ -package model - -type Verdict int - -const ( - VerdictJudging Verdict = iota - VerdictAccepted - VerdictWrongAnswer - VerdictTimeLimitExceeded - VerdictMemoryLimitExceeded - VerdictRuntimeError - VerdictCompileError - VerdictSystemError - VerdictJuryFailed - VerdictSkipped - VerdictPartiallyCorrect -) diff --git a/internal/router/api.go b/internal/router/api.go index 80fab98..ce7fa19 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -3,6 +3,8 @@ package router import ( "github.com/WHUPRJ/woj-server/internal/api/debug" "github.com/WHUPRJ/woj-server/internal/api/problem" + "github.com/WHUPRJ/woj-server/internal/api/status" + "github.com/WHUPRJ/woj-server/internal/api/submission" "github.com/WHUPRJ/woj-server/internal/api/user" "github.com/WHUPRJ/woj-server/internal/global" "github.com/gin-gonic/gin" @@ -25,4 +27,6 @@ var endpoints = []global.EndpointInfo{ {Version: "", Path: "/debug", Register: debug.RouteRegister}, {Version: "/v1", Path: "/user", Register: user.RouteRegister}, {Version: "/v1", Path: "/problem", Register: problem.RouteRegister}, + {Version: "/v1", Path: "/submission", Register: submission.RouteRegister}, + {Version: "/v1", Path: "/status", Register: status.RouteRegister}, } diff --git a/internal/service/jwt/middleware.go b/internal/service/jwt/middleware.go index c998f59..2d2eddc 100644 --- a/internal/service/jwt/middleware.go +++ b/internal/service/jwt/middleware.go @@ -2,35 +2,40 @@ package jwt import ( "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" "github.com/gin-gonic/gin" "strings" ) -func (s *service) Handler() gin.HandlerFunc { +func (s *service) Handler(forced bool) gin.HandlerFunc { return func(c *gin.Context) { - const tokenPrefix = "bearer " - tokenHeader := c.GetHeader("Authorization") - if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) { - e.Pong(c, e.TokenEmpty, nil) - c.Abort() - return - } - token := tokenHeader[len(tokenPrefix):] + claim, status := func() (*global.Claim, e.Status) { + const tokenPrefix = "bearer " + tokenHeader := c.GetHeader("Authorization") + if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) { + return nil, e.TokenEmpty + } - claim, status := s.ParseToken(token) - if status != e.Success { + token := tokenHeader[len(tokenPrefix):] + claim, status := s.ParseToken(token) + if status != e.Success { + return nil, status + } + + if !s.Validate(claim) { + return nil, e.TokenRevoked + } + return claim, e.Success + }() + + if status == e.Success { + c.Set("claim", claim) + } + if forced && status != e.Success { e.Pong(c, status, nil) c.Abort() - return + } else { + c.Next() } - - if !s.Validate(claim) { - e.Pong(c, e.TokenRevoked, nil) - c.Abort() - return - } - - c.Set("claim", claim) - c.Next() } } diff --git a/internal/service/problem/create.go b/internal/service/problem/create.go index ad48e3a..d79c4dd 100644 --- a/internal/service/problem/create.go +++ b/internal/service/problem/create.go @@ -6,12 +6,24 @@ import ( "go.uber.org/zap" ) -func (s *service) Create(uid uint, problem *model.Problem) (*model.Problem, e.Status) { - problem.ProviderID = uid - problem.IsEnabled = true +type CreateData struct { + Title string + Statement string + ProviderID uint + IsEnabled bool +} - if err := s.db.Create(problem).Error; err != nil { - s.log.Debug("create problem error", zap.Error(err), zap.Any("problem", problem)) +func (s *service) Create(data *CreateData) (*model.Problem, e.Status) { + problem := &model.Problem{ + Title: data.Title, + Statement: data.Statement, + ProviderID: data.ProviderID, + IsEnabled: data.IsEnabled, + } + + err := s.db.Create(problem).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem)) return nil, e.DatabaseError } diff --git a/internal/service/problem/createVersion.go b/internal/service/problem/createVersion.go new file mode 100644 index 0000000..f355c6a --- /dev/null +++ b/internal/service/problem/createVersion.go @@ -0,0 +1,27 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +type CreateVersionData struct { + ProblemID uint + StorageKey string +} + +func (s *service) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) { + problemVersion := &model.ProblemVersion{ + ProblemID: data.ProblemID, + StorageKey: data.StorageKey, + } + + err := s.db.Create(problemVersion).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion)) + return nil, e.DatabaseError + } + + return problemVersion, e.Success +} diff --git a/internal/service/problem/query.go b/internal/service/problem/query.go index f6ef0df..aa10be5 100644 --- a/internal/service/problem/query.go +++ b/internal/service/problem/query.go @@ -4,21 +4,29 @@ import ( "errors" "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/model" - "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) -func (s *service) Query(problemId uint) (*model.Problem, e.Status) { +func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) { problem := new(model.Problem) - err := s.db.Preload(clause.Associations).First(&problem, problemId).Error + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + err := query.First(&problem, pid).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, e.ProblemNotFound } if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid)) return nil, e.DatabaseError } - return problem, utils.If(problem.IsEnabled, e.Success, e.ProblemNotAvailable).(e.Status) + if shouldEnable && !problem.IsEnabled { + return nil, e.ProblemNotAvailable + } + return problem, e.Success } diff --git a/internal/service/problem/queryFuzz.go b/internal/service/problem/queryFuzz.go index 0fad2fc..a461e37 100644 --- a/internal/service/problem/queryFuzz.go +++ b/internal/service/problem/queryFuzz.go @@ -1,25 +1,28 @@ package problem import ( - "errors" "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/model" - "gorm.io/gorm" + "go.uber.org/zap" "gorm.io/gorm/clause" ) -func (s *service) QueryFuzz(search string) ([]*model.Problem, e.Status) { - var problems []*model.Problem +func (s *service) QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) { + problems := make([]*model.Problem, 0) - err := s.db.Preload(clause.Associations). - Where("is_enabled = true"). - Where(s.db.Where("title LIKE ?", "%"+search+"%"). - Or("content LIKE ?", "%"+search+"%")). - Find(&problems).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, e.ProblemNotFound + query := s.db + if associations { + query = query.Preload(clause.Associations) } + if shouldEnable { + query = query.Where("is_enabled = true") + } + query = query. + Where(s.db.Where("title LIKE ?", "%"+search+"%"). + Or("statement LIKE ?", "%"+search+"%")) + err := query.Find(&problems).Error if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("search", search)) return nil, e.DatabaseError } diff --git a/internal/service/problem/queryLatestVersion.go b/internal/service/problem/queryLatestVersion.go new file mode 100644 index 0000000..55f0fc8 --- /dev/null +++ b/internal/service/problem/queryLatestVersion.go @@ -0,0 +1,29 @@ +package problem + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func (s *service) QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status) { + problemVersion := &model.ProblemVersion{ + ProblemID: pid, + IsEnabled: true, + } + + err := s.db. + Where(problemVersion). + Last(&problemVersion).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.ProblemVersionNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion)) + return nil, e.DatabaseError + } + + return problemVersion, e.Success +} diff --git a/internal/service/problem/queryVersion.go b/internal/service/problem/queryVersion.go new file mode 100644 index 0000000..9ca08ae --- /dev/null +++ b/internal/service/problem/queryVersion.go @@ -0,0 +1,27 @@ +package problem + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func (s *service) QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) { + problemVersion := new(model.ProblemVersion) + + err := s.db.First(&problemVersion, pvid).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.ProblemVersionNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pvid", pvid)) + return nil, e.DatabaseError + } + + if shouldEnable && !problemVersion.IsEnabled { + return nil, e.ProblemVersionNotAvailable + } + return problemVersion, e.Success +} diff --git a/internal/service/problem/service.go b/internal/service/problem/service.go index 693951d..8e6df02 100644 --- a/internal/service/problem/service.go +++ b/internal/service/problem/service.go @@ -11,10 +11,15 @@ import ( var _ Service = (*service)(nil) type Service interface { - Create(uint, *model.Problem) (*model.Problem, e.Status) - Update(uint, *model.Problem) (*model.Problem, e.Status) - Query(uint) (*model.Problem, e.Status) - QueryFuzz(string) ([]*model.Problem, e.Status) + Create(data *CreateData) (*model.Problem, e.Status) + Update(problem *model.Problem) (*model.Problem, e.Status) + Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) + QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) + + CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) + UpdateVersion(problemVersion *model.ProblemVersion) (*model.ProblemVersion, e.Status) + QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) + QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status) } type service struct { diff --git a/internal/service/problem/update.go b/internal/service/problem/update.go index 079bb13..026b934 100644 --- a/internal/service/problem/update.go +++ b/internal/service/problem/update.go @@ -4,15 +4,12 @@ import ( "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/model" "go.uber.org/zap" - "gorm.io/gorm/clause" ) -func (s *service) Update(pid uint, problem *model.Problem) (*model.Problem, e.Status) { - if err := s.db.Clauses(clause.Returning{}).Model(problem). - Where("ID = (?)", pid). - Select("Title", "Content", "TimeLimit", "MemoryLimit", "IsEnabled"). - Updates(problem).Error; err != nil { - s.log.Debug("update problem error", zap.Error(err), zap.Any("problem", problem)) +func (s *service) Update(problem *model.Problem) (*model.Problem, e.Status) { + err := s.db.Save(problem).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem)) return nil, e.DatabaseError } diff --git a/internal/service/problem/updateVersion.go b/internal/service/problem/updateVersion.go new file mode 100644 index 0000000..c5e360c --- /dev/null +++ b/internal/service/problem/updateVersion.go @@ -0,0 +1,17 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +func (s *service) UpdateVersion(problemVersion *model.ProblemVersion) (*model.ProblemVersion, e.Status) { + err := s.db.Save(problemVersion).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion)) + return nil, e.DatabaseError + } + + return problemVersion, e.Success +} diff --git a/internal/service/runner/common.go b/internal/service/runner/common.go new file mode 100644 index 0000000..01260c7 --- /dev/null +++ b/internal/service/runner/common.go @@ -0,0 +1,75 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "os/exec" + "path" + "path/filepath" +) + +var ( + Prefix = "./resource/runner" + ProblemDir = "./problems/" + ScriptsDir = "./scripts/" + UserDir = "./users/" + TmpDir = "./tmp/" +) + +func init() { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + Prefix = path.Join(wd, Prefix) + ProblemDir = path.Join(Prefix, ProblemDir) + ScriptsDir = path.Join(Prefix, ScriptsDir) + UserDir = path.Join(Prefix, UserDir) + TmpDir = path.Join(Prefix, TmpDir) +} + +func (s *service) execute(script string, args ...string) error { + p := filepath.Join(ScriptsDir, script) + cmd := exec.Command(p, args...) + cmd.Dir = ScriptsDir + return cmd.Run() +} + +func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status { + if !s.problemExists(version) { + s.log.Info("problem not exists", zap.Uint("version", version)) + return e.RunnerProblemNotExist + } + + if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) { + s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang)) + return e.RunnerUserNotExist + } + + err := s.execute(script, fmt.Sprintf("%d", version), fmt.Sprintf("%s", user), lang) + + if err != nil { + s.log.Info("execute failed", + zap.Error(err), + zap.Uint("version", version), + zap.String("user", user), + zap.String("lang", lang)) + return fail + } + + return e.Success +} + +func (s *service) problemExists(version uint) bool { + problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + return utils.FileExist(problemPath) +} + +func (s *service) userExists(user string, file string) bool { + userPath := filepath.Join(UserDir, fmt.Sprintf("%s", user), file) + return utils.FileExist(userPath) +} diff --git a/internal/service/runner/compile.go b/internal/service/runner/compile.go new file mode 100644 index 0000000..2b0b270 --- /dev/null +++ b/internal/service/runner/compile.go @@ -0,0 +1,30 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "os" + "path/filepath" +) + +func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) { + target := filepath.Join(UserDir, fmt.Sprintf("%s", user), fmt.Sprintf("%s.out", user)) + + _ = os.Remove(target) + status := s.checkAndExecute(version, user, lang, "problem_compile.sh", e.RunnerUserCompileFailed) + + log := filepath.Join(UserDir, fmt.Sprintf("%s.compile.log", user)) + msg, err := utils.FileRead(log) + msg = utils.If(err == nil, msg, nil).([]byte) + msgText := string(msg) + + if utils.FileExist(target) { + return JudgeStatus{}, e.Success + } else { + return JudgeStatus{ + Message: "compile failed", + Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}}, + utils.If(status == e.Success, e.RunnerUserCompileFailed, status).(e.Status) + } +} diff --git a/internal/service/runner/config.go b/internal/service/runner/config.go new file mode 100644 index 0000000..94131c0 --- /dev/null +++ b/internal/service/runner/config.go @@ -0,0 +1,125 @@ +package runner + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" +) + +type Config struct { + Runtime struct { + TimeLimit int `json:"TimeLimit"` + MemoryLimit int `json:"MemoryLimit"` + NProcLimit int `json:"NProcLimit"` + } `json:"Runtime"` + Languages []struct { + Lang string `json:"Lang"` + Type string `json:"Type"` + Script string `json:"Script"` + Cmp string `json:"Cmp"` + } `json:"Languages"` + Tasks []struct { + Id int `json:"Id"` + Points int32 `json:"Points"` + } `json:"Tasks"` +} + +func (s *service) ParseConfig(version uint, skipCheck bool) (Config, 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.checkConfig(&config, base) + if err != nil { + return Config{}, err + } + + return config, nil +} + +func (s *service) checkConfig(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{}{ + "c": {}, + "cpp": {}, + } + for _, lang := range config.Languages { + if _, ok := allowedLang[lang.Lang]; !ok { + return fmt.Errorf("language %s is not allowed", lang.Lang) + } + + if lang.Type != "custom" && lang.Type != "default" { + return fmt.Errorf("language %s has invalid type %s", lang.Lang, lang.Type) + } + + 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 { + return errors.New("no tasks") + } + ids := map[int]struct{}{} + total := (1 + len(config.Tasks)) * len(config.Tasks) / 2 + for _, task := range config.Tasks { + if task.Id <= 0 { + return fmt.Errorf("task %d has non-positive id", task.Id) + } + + if task.Points < 0 { + return fmt.Errorf("task %d has negative points", task.Id) + } + + if _, ok := ids[task.Id]; ok { + return fmt.Errorf("task %d has duplicate id", task.Id) + } + + total -= task.Id + ids[task.Id] = struct{}{} + } + if total != 0 { + return errors.New("task ids are not continuous") + } + + return nil +} diff --git a/internal/service/runner/deps.go b/internal/service/runner/deps.go new file mode 100644 index 0000000..96940f5 --- /dev/null +++ b/internal/service/runner/deps.go @@ -0,0 +1,31 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "os/exec" + "path/filepath" +) + +func (s *service) EnsureDeps(force bool) e.Status { + mark := filepath.Join(Prefix, ".mark.docker") + + if force { + _ = os.Remove(mark) + } else if utils.FileExist(mark) { + return e.Success + } + + script := filepath.Join(ScriptsDir, "prepare_container.sh") + cmd := exec.Command(script) + cmd.Dir = ScriptsDir + err := cmd.Run() + if err != nil { + s.log.Warn("prebuild docker images failed", zap.Error(err)) + return e.RunnerDepsBuildFailed + } + + return e.Success +} diff --git a/internal/service/runner/newProblem.go b/internal/service/runner/newProblem.go new file mode 100644 index 0000000..6c9c91a --- /dev/null +++ b/internal/service/runner/newProblem.go @@ -0,0 +1,72 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/down" + "github.com/WHUPRJ/woj-server/pkg/unzip" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "path/filepath" +) + +func (s *service) download(version uint, url string) e.Status { + zipPath := filepath.Join(TmpDir, fmt.Sprintf("%d.zip", version)) + problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + + err := down.Down(zipPath, url) + if err != nil { + s.log.Error("download problem failed", zap.Error(err)) + return e.RunnerDownloadFailed + } + + err = unzip.Unzip(zipPath, problemPath) + if err != nil { + s.log.Warn("unzip problem failed", zap.Error(err)) + return e.RunnerUnzipFailed + } + + return e.Success +} + +func (s *service) prebuild(version uint, force bool) e.Status { + if !s.problemExists(version) { + return e.RunnerProblemNotExist + } + + mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild") + if force { + _ = os.Remove(mark) + } else if utils.FileExist(mark) { + return e.Success + } + + err := s.execute("problem_prebuild.sh", fmt.Sprintf("%d", version)) + + if err != nil { + s.log.Warn("prebuild problem failed", zap.Error(err), zap.Uint("version", version)) + return e.RunnerProblemPrebuildFailed + } + + return e.Success +} + +func (s *service) NewProblem(version uint, url string) (Config, e.Status) { + status := s.download(version, url) + if status != e.Success { + return Config{}, status + } + + cfg, err := s.ParseConfig(version, false) + if err != nil { + return Config{}, e.RunnerProblemParseFailed + } + + status = s.prebuild(version, true) + if status != e.Success { + return Config{}, status + } + + return cfg, e.Success +} diff --git a/internal/service/runner/runAndJudge.go b/internal/service/runner/runAndJudge.go new file mode 100644 index 0000000..c093c22 --- /dev/null +++ b/internal/service/runner/runAndJudge.go @@ -0,0 +1,22 @@ +package runner + +import "github.com/WHUPRJ/woj-server/internal/e" + +func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) { + // run user program + status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed) + if status != e.Success { + return JudgeStatus{Message: "run failed"}, 0, status + } + + // run judger + status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed) + if status != e.Success { + return JudgeStatus{Message: "judge failed"}, 0, status + } + + // check result + result, pts := s.checkResults(user, config) + + return result, pts, e.Success +} diff --git a/internal/service/runner/service.go b/internal/service/runner/service.go new file mode 100644 index 0000000..b4fd57b --- /dev/null +++ b/internal/service/runner/service.go @@ -0,0 +1,34 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "go.uber.org/zap" +) + +var _ Service = (*service)(nil) + +type Service interface { + // EnsureDeps build docker images + EnsureDeps(force bool) e.Status + // NewProblem = Download + Parse + Prebuild + NewProblem(version uint, url string) (Config, e.Status) + + // Compile compile user submission + Compile(version uint, user string, lang string) (JudgeStatus, e.Status) + // RunAndJudge execute user program + RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) + + // ParseConfig parse config file + ParseConfig(version uint, skipCheck bool) (Config, error) +} + +type service struct { + log *zap.Logger +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + } +} diff --git a/internal/service/runner/status.go b/internal/service/runner/status.go new file mode 100644 index 0000000..8c9ddbe --- /dev/null +++ b/internal/service/runner/status.go @@ -0,0 +1,227 @@ +package runner + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "golang.org/x/text/encoding/charmap" + "io" + "path/filepath" +) + +const ( + VerdictAccepted = iota + VerdictWrongAnswer + VerdictJuryFailed + VerdictPartialCorrect + VerdictTimeLimitExceeded + VerdictMemoryLimitExceeded + VerdictRuntimeError + VerdictCompileError + VerdictSystemError +) + +type TestLibReport struct { + XMLName xml.Name `xml:"result"` + Outcome string `xml:"outcome,attr"` + PCType int `xml:"pctype,attr"` + Points float64 `xml:"points,attr"` + Result string `xml:",chardata"` +} + +type TaskStatus struct { + Id int `json:"id"` + Points int32 `json:"points"` + RealTime int `json:"real_time"` + CpuTime int `json:"cpu_time"` + Memory int `json:"memory"` + Verdict int `json:"verdict"` + Message string `json:"message"` + + infoText []byte + info map[string]interface{} + judgeText string + judge TestLibReport +} + +type JudgeStatus struct { + Message string `json:"message"` + Tasks []TaskStatus `json:"tasks"` +} + +func (t *TaskStatus) getInfoText(infoFile string) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + var err error + t.infoText, err = utils.FileRead(infoFile) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot read info file" + } + + return t +} + +func (t *TaskStatus) getInfo() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + err := json.Unmarshal(t.infoText, &t.info) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot parse info file" + } else { + t.RealTime = int(t.info["real_time"].(float64)) + t.CpuTime = int(t.info["cpu_time"].(float64)) + t.Memory = int(t.info["memory"].(float64)) + } + + return t +} + +func (t *TaskStatus) checkExit() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["status"] != "exited" || t.info["code"] != 0.0 { + t.Verdict = VerdictRuntimeError + t.Message = fmt.Sprintf("status: %v, code: %v", t.info["status"], t.info["code"]) + } + + return t +} + +func (t *TaskStatus) checkTime(config *Config) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["real_time"].(float64) > float64(config.Runtime.TimeLimit)+5 { + t.Verdict = VerdictTimeLimitExceeded + t.Message = fmt.Sprintf("real_time: %v cpu_time: %v", t.info["real_time"], t.info["cpu_time"]) + } + + return t +} + +func (t *TaskStatus) checkMemory(config *Config) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["memory"].(float64) > float64((config.Runtime.MemoryLimit+1)*1024) { + t.Verdict = VerdictMemoryLimitExceeded + t.Message = fmt.Sprintf("memory: %v", t.info["memory"]) + } + + return t +} + +func (t *TaskStatus) getJudgeText(judgeFile string) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + j, err := utils.FileRead(judgeFile) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot read judge file" + } else { + t.judgeText = string(j) + } + + return t +} + +func (t *TaskStatus) getJudge() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + b := bytes.NewReader([]byte(t.judgeText)) + d := xml.NewDecoder(b) + d.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { + switch charset { + case "windows-1251": + return charmap.Windows1251.NewDecoder().Reader(input), nil + default: + return nil, fmt.Errorf("unknown charset: %s", charset) + } + } + + err := d.Decode(&t.judge) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot parse judge file" + } + + return t +} + +func (t *TaskStatus) checkJudge(pts *map[int]int32) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + mp := map[string]int{ + "accepted": VerdictAccepted, + "wrong-answer": VerdictWrongAnswer, + "presentation-error": VerdictWrongAnswer, + "points": VerdictPartialCorrect, + "relative-scoring": VerdictPartialCorrect, + } + + if v, ok := mp[t.judge.Outcome]; ok { + t.Verdict = v + t.Message = t.judge.Result + if v == VerdictAccepted { + t.Points = (*pts)[t.Id] + } else if v == VerdictPartialCorrect { + t.Points = int32(t.judge.Points) + int32(t.judge.PCType) + } + } else { + t.Verdict = VerdictJuryFailed + t.Message = fmt.Sprintf("unknown outcome: %v, result: %v", t.judge.Outcome, t.judge.Result) + } + + return t +} + +func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32) { + // CE will be processed in phase compile + + pts := map[int]int32{} + for _, task := range config.Tasks { + pts[task.Id] = task.Points + } + + var results []TaskStatus + dir := filepath.Join(UserDir, fmt.Sprintf("%s", user)) + var sum int32 = 0 + + for i := 1; i <= len(config.Tasks); i++ { + result := TaskStatus{Id: i, Verdict: VerdictAccepted, Points: 0} + + info := filepath.Join(dir, fmt.Sprintf("%d.info", i)) + judge := filepath.Join(dir, fmt.Sprintf("%d.judge", i)) + + result.getInfoText(info). + getInfo(). + checkExit(). + checkTime(config). + checkMemory(config). + getJudgeText(judge). + getJudge(). + checkJudge(&pts) + + sum += result.Points + } + + return JudgeStatus{Message: "", Tasks: results}, sum +} diff --git a/internal/service/status/create.go b/internal/service/status/create.go new file mode 100644 index 0000000..ad8093f --- /dev/null +++ b/internal/service/status/create.go @@ -0,0 +1,33 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/jackc/pgtype" + "go.uber.org/zap" +) + +type CreateData struct { + SubmissionID uint + ProblemVersionID uint + Context pgtype.JSON + Point int32 +} + +func (s service) Create(data *model.Status) (*model.Status, e.Status) { + status := &model.Status{ + SubmissionID: data.SubmissionID, + ProblemVersionID: data.ProblemVersionID, + Context: data.Context, + Point: data.Point, + IsEnabled: true, + } + + err := s.db.Create(status).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + + return status, e.Success +} diff --git a/internal/service/status/query.go b/internal/service/status/query.go new file mode 100644 index 0000000..dd474b5 --- /dev/null +++ b/internal/service/status/query.go @@ -0,0 +1,56 @@ +package status + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func (s service) Query(sid uint, associations bool) (*model.Status, e.Status) { + + status := &model.Status{ + SubmissionID: sid, + IsEnabled: true, + } + + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + + err := query. + Where(status). + Last(&status).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.StatusNotFound + } + + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + return status, e.Success +} + +func (s service) QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) { + var statuses []*model.Status + status := &model.Status{ + ProblemVersionID: pvid, + IsEnabled: true, + } + + err := s.db.Preload(clause.Associations). + Where(status). + Limit(limit). + Offset(offset). + Find(&statuses).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + return statuses, e.Success +} diff --git a/internal/service/status/rejudge.go b/internal/service/status/rejudge.go new file mode 100644 index 0000000..919b0db --- /dev/null +++ b/internal/service/status/rejudge.go @@ -0,0 +1,11 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" +) + +func (s service) Rejudge(statusID uint) ([]*model.Status, e.Status) { + //TODO implement me + panic("implement me") +} diff --git a/internal/service/status/service.go b/internal/service/status/service.go new file mode 100644 index 0000000..221b83f --- /dev/null +++ b/internal/service/status/service.go @@ -0,0 +1,30 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(*model.Status) (*model.Status, e.Status) + Query(sid uint, associations bool) (*model.Status, e.Status) + QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) + Rejudge(statusID uint) ([]*model.Status, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + } +} diff --git a/internal/service/submission/create.go b/internal/service/submission/create.go new file mode 100644 index 0000000..2c919d6 --- /dev/null +++ b/internal/service/submission/create.go @@ -0,0 +1,31 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +type CreateData struct { + ProblemID uint + UserID uint + Language string + Code string +} + +func (s *service) Create(data *CreateData) (*model.Submission, e.Status) { + submission := &model.Submission{ + ProblemID: data.ProblemID, + UserID: data.UserID, + Language: data.Language, + Code: data.Code, + } + + err := s.db.Create(submission).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("submission", submission)) + return nil, e.DatabaseError + } + + return submission, e.Success +} diff --git a/internal/service/submission/query.go b/internal/service/submission/query.go new file mode 100644 index 0000000..1a3838b --- /dev/null +++ b/internal/service/submission/query.go @@ -0,0 +1,33 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm/clause" +) + +func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) { + submissions := make([]*model.Submission, 0) + + submission := &model.Submission{ + ProblemID: pid, + UserID: uid, + } + + err := s.db.Preload(clause.Associations). + Where(submission). + Limit(limit). + Offset(offset). + Find(&submissions).Error + + //if errors.Is(err, gorm.ErrRecordNotFound) { + // return nil, e.ProblemNotFound + //} + + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid), zap.Any("uid", uid)) + return nil, e.DatabaseError + } + return submissions, e.Success +} diff --git a/internal/service/submission/service.go b/internal/service/submission/service.go new file mode 100644 index 0000000..4c8de37 --- /dev/null +++ b/internal/service/submission/service.go @@ -0,0 +1,28 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(data *CreateData) (*model.Submission, e.Status) + Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + } +} diff --git a/internal/service/task/common.go b/internal/service/task/common.go index 3629330..b626d6b 100644 --- a/internal/service/task/common.go +++ b/internal/service/task/common.go @@ -6,10 +6,10 @@ import ( "go.uber.org/zap" ) -func (s *service) submit(typename string, payload []byte) (*asynq.TaskInfo, e.Status) { +func (s *service) submit(typename string, payload []byte, queue string) (*asynq.TaskInfo, e.Status) { task := asynq.NewTask(typename, payload) - info, err := s.queue.Enqueue(task) + info, err := s.queue.Enqueue(task, asynq.Queue(queue)) if err != nil { s.log.Warn("failed to enqueue task", zap.Error(err), zap.Any("task", task)) return nil, e.TaskEnqueueFailed @@ -20,8 +20,8 @@ func (s *service) submit(typename string, payload []byte) (*asynq.TaskInfo, e.St return info, e.Success } -func (s *service) GetTaskInfo(id string) (*asynq.TaskInfo, e.Status) { - task, err := s.inspector.GetTaskInfo("default", id) +func (s *service) GetTaskInfo(id string, queue string) (*asynq.TaskInfo, e.Status) { + task, err := s.inspector.GetTaskInfo(queue, id) if err != nil { s.log.Debug("get task info failed", zap.Error(err), zap.String("id", id)) return nil, e.TaskGetInfoFailed diff --git a/internal/service/task/judge.go b/internal/service/task/judge.go deleted file mode 100644 index 057c150..0000000 --- a/internal/service/task/judge.go +++ /dev/null @@ -1,20 +0,0 @@ -package task - -import ( - "encoding/json" - "github.com/WHUPRJ/woj-server/internal/e" - "github.com/WHUPRJ/woj-server/internal/model" - "go.uber.org/zap" -) - -func (s *service) NewJudge(submission model.Submission) (string, e.Status) { - payload, err := json.Marshal(model.SubmitJudge{Submission: submission}) - if err != nil { - s.log.Warn("json marshal error", zap.Error(err), zap.Any("payload", submission)) - return "", e.InternalError - } - - info, status := s.submit(model.TypeSubmitJudge, payload) - - return info.ID, status -} diff --git a/internal/service/task/problem.go b/internal/service/task/problem.go new file mode 100644 index 0000000..818934a --- /dev/null +++ b/internal/service/task/problem.go @@ -0,0 +1,46 @@ +package task + +import ( + "encoding/json" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +func (s *service) ProblemBuild(pvId uint, file string) (string, e.Status) { + payload, err := json.Marshal(model.ProblemBuildPayload{ + ProblemVersionID: pvId, + ProblemFile: file, + }) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("ProblemVersionID", pvId), + zap.String("ProblemFile", file)) + return "", e.InternalError + } + + info, status := s.submit(model.TypeProblemBuild, payload, model.QueueRunner) + + return info.ID, status +} + +func (s *service) ProblemUpdate(status e.Status, pvId uint, ctx string) (string, e.Status) { + payload, err := json.Marshal(model.ProblemUpdatePayload{ + Status: status, + ProblemVersionID: pvId, + Context: ctx, + }) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("Status", status), + zap.Any("ProblemVersionID", pvId), + zap.Any("Context", ctx)) + return "", e.InternalError + } + + info, status := s.submit(model.TypeProblemUpdate, payload, model.QueueServer) + + return info.ID, status +} diff --git a/internal/service/task/push.go b/internal/service/task/push.go deleted file mode 100644 index e252789..0000000 --- a/internal/service/task/push.go +++ /dev/null @@ -1,23 +0,0 @@ -package task - -import ( - "encoding/json" - "github.com/WHUPRJ/woj-server/internal/e" - "github.com/WHUPRJ/woj-server/internal/model" - "go.uber.org/zap" -) - -func (s *service) PushProblem(id uint, file string) (string, e.Status) { - payload, err := json.Marshal(model.ProblemPushPayload{ - ProblemID: id, - ProblemFile: file, - }) - if err != nil { - s.log.Warn("json marshal error", zap.Error(err), zap.Any("id", id), zap.String("file", file)) - return "", e.InternalError - } - - info, status := s.submit(model.TypeSubmitJudge, payload) - - return info.ID, status -} diff --git a/internal/service/task/service.go b/internal/service/task/service.go index 66d48ff..af64354 100644 --- a/internal/service/task/service.go +++ b/internal/service/task/service.go @@ -4,6 +4,7 @@ import ( "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" "github.com/hibiken/asynq" "go.uber.org/zap" ) @@ -11,10 +12,12 @@ import ( var _ Service = (*service)(nil) type Service interface { - NewJudge(submission model.Submission) (string, e.Status) - PushProblem(id uint, file string) (string, e.Status) - GetTaskInfo(id string) (*asynq.TaskInfo, e.Status) - submit(typename string, payload []byte) (*asynq.TaskInfo, e.Status) + ProblemBuild(pvId uint, file string) (string, e.Status) + ProblemUpdate(status e.Status, pvId uint, ctx string) (string, e.Status) + SubmitJudge(pvid uint, storageKey string, submission model.Submission) (string, e.Status) + SubmitUpdate(status e.Status, sid uint, point int32, ctx runner.JudgeStatus) (string, e.Status) + + GetTaskInfo(string, string) (*asynq.TaskInfo, e.Status) } type service struct { diff --git a/internal/service/task/submit.go b/internal/service/task/submit.go new file mode 100644 index 0000000..4c3e0e7 --- /dev/null +++ b/internal/service/task/submit.go @@ -0,0 +1,55 @@ +package task + +import ( + "encoding/json" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "go.uber.org/zap" +) + +func (s *service) SubmitJudge(pvid uint, storageKey string, submission model.Submission) (string, e.Status) { + payload, err := json.Marshal( + model.SubmitJudgePayload{ + ProblemVersionId: pvid, + StorageKey: storageKey, + Submission: submission, + }) + if err != nil { + s.log.Warn("json marshal error", zap.Error(err), zap.Any("Submission", submission)) + return "", e.InternalError + } + + info, status := s.submit(model.TypeSubmitJudge, payload, model.QueueRunner) + + return info.ID, status +} + +func (s *service) SubmitUpdate(status e.Status, sid uint, point int32, ctx runner.JudgeStatus) (string, e.Status) { + ctxText, err := json.Marshal(ctx) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("ctx", ctx)) + return "", e.InternalError + } + + payload, err := json.Marshal(model.SubmitUpdatePayload{ + Status: status, + Sid: sid, + Point: point, + Context: string(ctxText), + }) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("Status", status), + zap.Int32("Point", point), + zap.Any("Context", ctx)) + return "", e.InternalError + } + + info, status := s.submit(model.TypeSubmitUpdate, payload, model.QueueServer) + + return info.ID, status +} diff --git a/internal/service/user/create.go b/internal/service/user/create.go index faf5e16..f91cd53 100644 --- a/internal/service/user/create.go +++ b/internal/service/user/create.go @@ -9,31 +9,32 @@ import ( ) type CreateData struct { - Username string - Nickname string + UserName string Password string + NickName string } func (s *service) Create(data *CreateData) (*model.User, e.Status) { hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost) if err != nil { - s.log.Debug("bcrypt error", zap.Error(err), zap.String("password", data.Password)) + s.log.Warn("BcryptError", zap.Error(err), zap.String("password", data.Password)) return nil, e.InternalError } user := &model.User{ - UserName: data.Username, + UserName: data.UserName, Password: hashed, - NickName: data.Nickname, + NickName: data.NickName, Role: model.RoleGeneral, IsEnabled: true, } - if err := s.db.Create(user).Error; err != nil { - if strings.Contains(err.Error(), "duplicate key") { - return nil, e.UserDuplicated - } - s.log.Debug("create user error", zap.Error(err), zap.Any("data", data)) + err = s.db.Create(user).Error + if err != nil && strings.Contains(err.Error(), "duplicate key") { + return nil, e.UserDuplicated + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user)) return nil, e.DatabaseError } return user, e.Success diff --git a/internal/service/user/login.go b/internal/service/user/login.go index 77824e5..d80acbd 100644 --- a/internal/service/user/login.go +++ b/internal/service/user/login.go @@ -4,11 +4,17 @@ import ( "errors" "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) -func (s *service) Login(data *model.User) (*model.User, e.Status) { +type LoginData struct { + UserName string + Password string +} + +func (s *service) Login(data *LoginData) (*model.User, e.Status) { user := &model.User{UserName: data.UserName} err := s.db.Where(user).First(&user).Error @@ -16,6 +22,7 @@ func (s *service) Login(data *model.User) (*model.User, e.Status) { return nil, e.UserNotFound } if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user)) return nil, e.DatabaseError } @@ -23,7 +30,7 @@ func (s *service) Login(data *model.User) (*model.User, e.Status) { return nil, e.UserDisabled } - err = bcrypt.CompareHashAndPassword(user.Password, data.Password) + err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password)) if err != nil { return nil, e.UserWrongPassword } diff --git a/internal/service/user/profile.go b/internal/service/user/profile.go index 4705f31..7cd706c 100644 --- a/internal/service/user/profile.go +++ b/internal/service/user/profile.go @@ -4,17 +4,19 @@ import ( "errors" "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" "gorm.io/gorm" ) -func (s *service) Profile(id uint) (*model.User, e.Status) { +func (s *service) Profile(uid uint) (*model.User, e.Status) { user := new(model.User) - err := s.db.First(&user, id).Error + err := s.db.First(&user, uid).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, e.UserNotFound } if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("uid", uid)) return nil, e.DatabaseError } diff --git a/internal/service/user/service.go b/internal/service/user/service.go index e5e398a..4baeffd 100644 --- a/internal/service/user/service.go +++ b/internal/service/user/service.go @@ -13,9 +13,9 @@ var _ Service = (*service)(nil) type Service interface { Create(data *CreateData) (*model.User, e.Status) - Login(data *model.User) (*model.User, e.Status) - IncrVersion(id uint) (int64, e.Status) - Profile(id uint) (*model.User, e.Status) + Login(data *LoginData) (*model.User, e.Status) + IncrVersion(uid uint) (int64, e.Status) + Profile(uid uint) (*model.User, e.Status) } type service struct { diff --git a/internal/service/user/version.go b/internal/service/user/version.go index a342668..8303af8 100644 --- a/internal/service/user/version.go +++ b/internal/service/user/version.go @@ -7,11 +7,12 @@ import ( "go.uber.org/zap" ) -func (s *service) IncrVersion(id uint) (int64, e.Status) { - version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", id)).Result() +func (s *service) IncrVersion(uid uint) (int64, e.Status) { + version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", uid)).Result() if err != nil { - s.log.Debug("redis.Incr error", zap.Error(err)) + s.log.Warn("RedisError", zap.Error(err), zap.Any("uid", uid)) return -1, e.RedisError } + return version, e.Success } diff --git a/pkg/down/down.go b/pkg/down/down.go new file mode 100644 index 0000000..d4b30b9 --- /dev/null +++ b/pkg/down/down.go @@ -0,0 +1,55 @@ +package down + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "io" + "net/http" + "os" + "path/filepath" +) + +func Down(dest string, url string) error { + dir := filepath.Dir(dest) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + tmp := fmt.Sprintf("%s.%s", dest, utils.RandomString(5)) + f, err := os.Create(tmp) + if err != nil { + return err + } + + resp, err := http.Get(url) + if err != nil { + return err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + _ = os.Remove(tmp) + return fmt.Errorf("bad status %s when accessing %s", resp.Status, url) + } + + _, err = io.Copy(f, resp.Body) + if err != nil { + return err + } + + err = f.Close() + if err != nil { + return err + } + + err = os.Rename(tmp, dest) + if err != nil { + _ = os.Remove(dest) + return err + } + + return nil +} diff --git a/pkg/unzip/unzip.go b/pkg/unzip/unzip.go new file mode 100644 index 0000000..98bac57 --- /dev/null +++ b/pkg/unzip/unzip.go @@ -0,0 +1,60 @@ +package unzip + +import ( + "archive/zip" + "io" + "os" + "path/filepath" +) + +func Unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer func(r *zip.ReadCloser) { + _ = r.Close() + }(r) + + handler := func(f *zip.File, dest string) error { + rc, err := f.Open() + if err != nil { + return err + } + defer func(rc io.ReadCloser) { + _ = rc.Close() + }(rc) + + path := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + _ = os.MkdirAll(path, 0755) + } else { + _ = os.MkdirAll(filepath.Dir(path), 0755) + + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + _, err = io.Copy(file, rc) + if err != nil { + return err + } + } + + return nil + } + + for _, f := range r.File { + err := handler(f, dest) + if err != nil { + _ = os.RemoveAll(dest) + return err + } + } + + return nil +} diff --git a/pkg/utils/file.go b/pkg/utils/file.go index c05458c..5857aac 100644 --- a/pkg/utils/file.go +++ b/pkg/utils/file.go @@ -3,6 +3,7 @@ package utils import ( "io" "os" + "path/filepath" ) func FileRead(filePath string) ([]byte, error) { @@ -23,6 +24,8 @@ func FileExist(filePath string) bool { } func FileTouch(filePath string) bool { + base := filepath.Dir(filePath) + _ = os.MkdirAll(base, 0755) _, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) return If(err == nil, true, false).(bool) } diff --git a/pkg/zapasynq/logger.go b/pkg/zapasynq/logger.go new file mode 100644 index 0000000..f23f8b3 --- /dev/null +++ b/pkg/zapasynq/logger.go @@ -0,0 +1,25 @@ +package zapasynq + +import ( + "go.uber.org/zap" +) + +type Logger struct { + logger *zap.Logger +} + +func New(zapLogger *zap.Logger) Logger { + return Logger{ + logger: zapLogger, + } +} + +func (l Logger) Debug(args ...interface{}) { l.logger.Sugar().Debugf(args[0].(string), args[1:]...) } + +func (l Logger) Info(args ...interface{}) { l.logger.Sugar().Infof(args[0].(string), args[1:]...) } + +func (l Logger) Warn(args ...interface{}) { l.logger.Sugar().Warnf(args[0].(string), args[1:]...) } + +func (l Logger) Error(args ...interface{}) { l.logger.Sugar().Errorf(args[0].(string), args[1:]...) } + +func (l Logger) Fatal(args ...interface{}) { l.logger.Sugar().Fatalf(args[0].(string), args[1:]...) } diff --git a/resource/runner/framework/scripts/setup.sh b/resource/runner/framework/scripts/setup.sh index 5b60d8b..b93b328 100755 --- a/resource/runner/framework/scripts/setup.sh +++ b/resource/runner/framework/scripts/setup.sh @@ -5,7 +5,7 @@ rm -rf woj-sandbox git clone https://github.com/WHUPRJ/woj-sandbox.git >/dev/null 2>&1 || exit 1 cd woj-sandbox && ./build_libseccomp.sh || exit 1 -mkdir -p build && cd build +mkdir -p build && cd build || exit 1 cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1 make -j || exit 1 diff --git a/resource/runner/framework/template/default/c.Makefile b/resource/runner/framework/template/default/c.Makefile index 4f22696..25be642 100644 --- a/resource/runner/framework/template/default/c.Makefile +++ b/resource/runner/framework/template/default/c.Makefile @@ -1,7 +1,7 @@ include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk compile: - $(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) + @$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) judge: - $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1 + $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/framework/template/default/cpp.Makefile b/resource/runner/framework/template/default/cpp.Makefile index bcb89be..ea648d2 100644 --- a/resource/runner/framework/template/default/cpp.Makefile +++ b/resource/runner/framework/template/default/cpp.Makefile @@ -1,7 +1,7 @@ include ${TEMPLATE}/cpp.mk ${TEMPLATE}/Judger.mk compile: - $(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) + @$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) judge: - $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1 + $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/framework/template/setup.sh b/resource/runner/framework/template/setup.sh index 5576343..59c117f 100755 --- a/resource/runner/framework/template/setup.sh +++ b/resource/runner/framework/template/setup.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -x rm -rf testlib -git clone https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&1 || exit 1 +git clone --depth=1 https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&1 || exit 1 rm -rf testlib/.git rm -rf testlib/tests cd testlib/checkers || exit 1 diff --git a/resource/runner/problem/example/judge/XYZ.Makefile b/resource/runner/problem/example/judge/XYZ.Makefile index 32b8f1c..d15d183 100644 --- a/resource/runner/problem/example/judge/XYZ.Makefile +++ b/resource/runner/problem/example/judge/XYZ.Makefile @@ -19,4 +19,4 @@ compile: judge: # Rename on *.out.usr or *.judge is not allowed sed '/gadgets/d' $(PREFIX)/user/$(TEST_NUM).out.usr > $(PREFIX)/user/$(TEST_NUM).out.usr1 - $(NCMP) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr1 $(PREFIX)/data/output/$(TEST_NUM).output > $(PREFIX)/user/$(TEST_NUM).judge 2>&1 + $(NCMP) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr1 $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/scripts/prepare_container.sh b/resource/runner/scripts/prepare_container.sh index af10035..e02c989 100755 --- a/resource/runner/scripts/prepare_container.sh +++ b/resource/runner/scripts/prepare_container.sh @@ -6,7 +6,7 @@ cd "$(dirname "$0")"/../ || exit 1 if [ -f ./.mark.docker ]; then log_warn "Docker containers already prepared" - log_warn "If you want to re-prepare the containers, please remove the file `pwd`/.mark.docker" + log_warn "If you want to re-prepare the containers, please remove the file $(pwd)/.mark.docker" exit 1 fi diff --git a/resource/runner/scripts/problem_compile.sh b/resource/runner/scripts/problem_compile.sh index 874dbb8..8f35ac6 100755 --- a/resource/runner/scripts/problem_compile.sh +++ b/resource/runner/scripts/problem_compile.sh @@ -14,7 +14,7 @@ get_problem_info "$WORKSPACE" "$1" "$3" SRC_FILE="$WORKSPACE"/user/"$2"/"$2"."$3" EXE_FILE="$WORKSPACE"/user/"$2"/"$2".out -LOG_FILE="$WORKSPACE"/user/"$2"/"$2".compile.log +export LOG_FILE="$WORKSPACE"/user/"$2"/"$2".compile.log rm -f "$EXE_FILE" && touch "$EXE_FILE" diff --git a/resource/runner/scripts/problem_judge.sh b/resource/runner/scripts/problem_judge.sh index 9a743aa..0142ee2 100755 --- a/resource/runner/scripts/problem_judge.sh +++ b/resource/runner/scripts/problem_judge.sh @@ -12,7 +12,7 @@ fi get_problem_info "$WORKSPACE" "$1" "$3" -TIMEOUT=${4:-60} +export TIMEOUT=${4:-60} for test_num in $(seq "$Info_Num"); do std_file="$WORKSPACE/problem/$1/data/output/$test_num.output" ans_file="$WORKSPACE/user/$2/$test_num.out.usr" diff --git a/resource/runner/scripts/problem_prebuild.sh b/resource/runner/scripts/problem_prebuild.sh index de3b45d..3b97164 100755 --- a/resource/runner/scripts/problem_prebuild.sh +++ b/resource/runner/scripts/problem_prebuild.sh @@ -21,7 +21,7 @@ if [ ! -f "$WORKSPACE/problem/$1/judge/prebuild.Makefile" ]; then exit 0 fi -TIMEOUT=${2:-300} +export TIMEOUT=${2:-300} docker_run \ -v "$WORKSPACE/problem/$1/data":/woj/problem/data \ -v "$WORKSPACE/problem/$1/judge":/woj/problem/judge \