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 <cxy004@qq.com>
Co-authored-by: wzt <w.zhongtao@qq.com>
This commit is contained in:
Paul Pan 2022-10-22 17:38:39 +08:00
parent 062c5ef964
commit d42ee0ce54
91 changed files with 2257 additions and 406 deletions

View File

@ -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 }}

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
### Project
/server
/runner
my.secrets
### JetBrains template

11
Dockerfile.runner Normal file
View File

@ -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"]

View File

@ -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"]

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

75
cmd/common.go Normal file
View File

@ -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
}

34
cmd/runner/main.go Normal file
View File

@ -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)
}

34
cmd/server/main.go Normal file
View File

@ -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)
}

View File

@ -6,7 +6,7 @@ WebServer:
Redis:
Db: 0
QueueDb: 0
QueueDb: 1
Address: '127.0.0.1:6379'
Password: ''

5
go.mod
View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))

View File

@ -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",
}

View File

@ -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
}

View File

@ -1,6 +0,0 @@
package model
const (
LangC int32 = iota
LangCPP
)

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"`

View File

@ -1,17 +0,0 @@
package model
type Verdict int
const (
VerdictJudging Verdict = iota
VerdictAccepted
VerdictWrongAnswer
VerdictTimeLimitExceeded
VerdictMemoryLimitExceeded
VerdictRuntimeError
VerdictCompileError
VerdictSystemError
VerdictJuryFailed
VerdictSkipped
VerdictPartiallyCorrect
)

View File

@ -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},
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

55
pkg/down/down.go Normal file
View File

@ -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
}

60
pkg/unzip/unzip.go Normal file
View File

@ -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
}

View File

@ -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)
}

25
pkg/zapasynq/logger.go Normal file
View File

@ -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:]...) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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 \