mirror of
https://github.com/criyle/go-judge.git
synced 2025-11-04 14:50:02 +08:00
243 lines
5.2 KiB
Go
243 lines
5.2 KiB
Go
package runner
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"time"
|
|
|
|
"github.com/criyle/go-sandbox/daemon"
|
|
"github.com/criyle/go-sandbox/pkg/cgroup"
|
|
"github.com/criyle/go-sandbox/pkg/pipe"
|
|
stypes "github.com/criyle/go-sandbox/types"
|
|
|
|
"github.com/criyle/go-judge/file"
|
|
"github.com/criyle/go-judge/file/memfile"
|
|
"github.com/criyle/go-judge/language"
|
|
"github.com/criyle/go-judge/types"
|
|
)
|
|
|
|
const maxOutput = 4 << 20 // 4M
|
|
const cgroupPrefix = "go-judge"
|
|
const minCPUPercent = 40 // 40%
|
|
const checkIntervalMS = 50
|
|
|
|
var env = []string{"PATH=/usr/local/bin:/usr/bin:/bin"}
|
|
|
|
func (r *Runner) run(done <-chan struct{}, task *types.RunTask) *types.RunTaskResult {
|
|
t := language.TypeExec
|
|
if task.Type == "compile" {
|
|
t = language.TypeCompile
|
|
}
|
|
param := r.Language.Get(task.Language, t)
|
|
|
|
// init input / output / error files
|
|
inputFile, err := task.InputFile.Open()
|
|
if err != nil {
|
|
return errResult("failed to initialize input file")
|
|
}
|
|
defer inputFile.Close()
|
|
|
|
outputPipe, err := pipe.NewBuffer(maxOutput)
|
|
if err != nil {
|
|
return errResult("failed to initialize output pipe")
|
|
}
|
|
defer outputPipe.W.Close()
|
|
|
|
errorPipe, err := pipe.NewBuffer(maxOutput)
|
|
if err != nil {
|
|
return errResult("failed to initialize output pipe")
|
|
}
|
|
defer errorPipe.W.Close()
|
|
|
|
// init cgroup
|
|
cg, err := cgroup.NewCGroup(cgroupPrefix)
|
|
if err != nil {
|
|
return errResult("failed to initialize cgroup")
|
|
}
|
|
defer cg.Destroy()
|
|
|
|
// get daemon runner
|
|
m, err := r.pool.Get()
|
|
if err != nil {
|
|
return errResult("failed to get daemon instance")
|
|
}
|
|
defer r.pool.Put(m)
|
|
|
|
// setup cgroup limits
|
|
memoryLimit := param.MemoryLimit
|
|
if task.MemoryLimit > 0 {
|
|
memoryLimit = task.MemoryLimit
|
|
}
|
|
|
|
cg.SetMemoryLimitInBytes(memoryLimit << 10)
|
|
cg.SetPidsMax(param.ProcLimit)
|
|
|
|
// set running parameters
|
|
execParam := daemon.ExecveParam{
|
|
Args: param.Args,
|
|
Envv: env,
|
|
Fds: []uintptr{inputFile.Fd(), outputPipe.W.Fd(), errorPipe.W.Fd()},
|
|
SyncFunc: cg.AddProc,
|
|
}
|
|
|
|
// cancellable signal channel
|
|
cancelC := newCancellableChannel()
|
|
defer cancelC.cancel()
|
|
|
|
// start the process
|
|
rc, err := m.Execve(cancelC.Done, &execParam)
|
|
if err != nil {
|
|
return errResult("failed to start program")
|
|
}
|
|
|
|
// close write end at parent process to avoid reader waiting
|
|
// duplicate closing error is silenced during defer
|
|
outputPipe.W.Close()
|
|
errorPipe.W.Close()
|
|
|
|
// wait task done (check each interval)
|
|
ticker := time.NewTicker(checkIntervalMS * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
timeLimit := time.Duration(param.TimeLimit) * time.Millisecond
|
|
if task.TimeLimit > 0 {
|
|
timeLimit = time.Duration(task.TimeLimit) * time.Millisecond
|
|
}
|
|
|
|
var lastCPUUsage uint64
|
|
var totalTime time.Duration
|
|
var rt stypes.Result
|
|
var rtreceived bool
|
|
lastCheckTime := time.Now()
|
|
|
|
// wait task finish
|
|
loop:
|
|
for {
|
|
select {
|
|
case now := <-ticker.C: // interval
|
|
cpuUsage, err := cg.CpuacctUsage()
|
|
if err != nil {
|
|
return errResult("failed to get cgroup cpu usage")
|
|
}
|
|
|
|
cpuUsageDelta := time.Duration(cpuUsage - lastCPUUsage)
|
|
timeDelta := now.Sub(lastCheckTime)
|
|
|
|
totalTime += durationMax(cpuUsageDelta, timeDelta*minCPUPercent/100)
|
|
if totalTime > timeLimit {
|
|
break loop
|
|
}
|
|
|
|
lastCheckTime = now
|
|
lastCPUUsage = cpuUsage
|
|
|
|
case rt = <-rc: // returned
|
|
rtreceived = true
|
|
break loop
|
|
|
|
case <-outputPipe.Done: // outputlimit exceeded
|
|
break loop
|
|
|
|
case <-errorPipe.Done: // outputlimit exceeded
|
|
break loop
|
|
}
|
|
}
|
|
|
|
// get result if did not received
|
|
cancelC.cancel()
|
|
if !rtreceived {
|
|
rt = <-rc
|
|
}
|
|
|
|
// generate resource usage
|
|
cpuUsage, err := cg.CpuacctUsage()
|
|
if err != nil {
|
|
return errResult("failed to read cgroup cpuusage")
|
|
}
|
|
memoryUsage, err := cg.MemoryMaxUsageInBytes()
|
|
if err != nil {
|
|
return errResult("failed to read cgroup memory")
|
|
}
|
|
|
|
// generate result status
|
|
status := ""
|
|
if totalTime > timeLimit {
|
|
status = "TLE"
|
|
}
|
|
if memoryUsage > memoryLimit<<10 {
|
|
status = "MLE"
|
|
}
|
|
if outputPipe.Buffer.Len() > maxOutput {
|
|
status = "OLE"
|
|
}
|
|
if errorPipe.Buffer.Len() > maxOutput {
|
|
status = "OLE"
|
|
}
|
|
if rt.Status != stypes.StatusNormal {
|
|
status = rt.Status.String()
|
|
}
|
|
|
|
inputContent, _ := task.InputFile.Content()
|
|
|
|
// If compile read compiled files
|
|
var exec []file.File
|
|
if task.Type == "compile" {
|
|
for _, fn := range param.CompiledFileNames {
|
|
f, err := m.Open(fn)
|
|
if err != nil {
|
|
return errResult(err.Error())
|
|
}
|
|
defer f.Close()
|
|
|
|
c, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
return errResult(err.Error())
|
|
}
|
|
exec = append(exec, memfile.New(fn, c))
|
|
}
|
|
}
|
|
|
|
// TODO: diff
|
|
|
|
return &types.RunTaskResult{
|
|
Status: status,
|
|
Time: cpuUsage / uint64(time.Millisecond),
|
|
Memory: memoryUsage >> 10,
|
|
Input: inputContent,
|
|
UserOutput: outputPipe.Buffer.Bytes(),
|
|
UserError: errorPipe.Buffer.Bytes(),
|
|
ExecFiles: exec,
|
|
}
|
|
}
|
|
|
|
func errResult(err string) *types.RunTaskResult {
|
|
return &types.RunTaskResult{
|
|
Status: "JGF",
|
|
Error: err,
|
|
}
|
|
}
|
|
|
|
type cancellableChannel struct {
|
|
Done chan struct{}
|
|
canceled bool
|
|
}
|
|
|
|
func newCancellableChannel() *cancellableChannel {
|
|
return &cancellableChannel{
|
|
Done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (c *cancellableChannel) cancel() {
|
|
if !c.canceled {
|
|
close(c.Done)
|
|
c.canceled = true
|
|
}
|
|
}
|
|
|
|
func durationMax(a, b time.Duration) time.Duration {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|