go-judge/env/macsandbox/environment_darwin.go

195 lines
4.1 KiB
Go

package macsandbox
import (
"context"
"os"
"path/filepath"
"syscall"
"time"
"github.com/criyle/go-judge/env/pool"
"github.com/criyle/go-judge/envexec"
"github.com/criyle/go-sandbox/pkg/forkexec"
"github.com/criyle/go-sandbox/pkg/rlimit"
"github.com/criyle/go-sandbox/runner"
"golang.org/x/sys/unix"
)
const outputLimit = 256 << 20 // 256M
var _ pool.Environment = &environment{}
type environment struct {
profile string
wdPath string
wd *os.File
}
func (e *environment) Execve(c context.Context, param envexec.ExecveParam) (envexec.Process, error) {
sTime := time.Now()
rLimits := rlimit.RLimits{
CPU: uint64(param.Limit.Time.Truncate(time.Second)/time.Second) + 1,
Data: param.Limit.Memory.Byte(),
FileSize: outputLimit,
Stack: param.Limit.Stack.Byte(),
}
ch := &forkexec.Runner{
Args: param.Args,
Env: param.Env,
Files: param.Files,
WorkDir: e.wdPath,
RLimits: rLimits.PrepareRLimit(),
SandboxProfile: e.profile,
}
pid, err := ch.Start()
if err != nil {
return nil, err
}
p := &process{
pid: pid,
done: make(chan struct{}),
}
go func() {
defer close(p.done)
mTime := time.Now()
// handle cancel
ctx, cancel := context.WithCancel(c)
defer cancel()
go func() {
<-ctx.Done()
killAll(pid)
}()
// collect potential zombies
defer func() {
killAll(pid)
collectZombie(pid)
}()
var (
wstatus syscall.WaitStatus
rusage syscall.Rusage
)
for {
_, err = syscall.Wait4(pid, &wstatus, 0, &rusage)
if err == syscall.EINTR {
continue
}
if err != nil {
p.result.Error = err.Error()
p.result.Status = runner.StatusRunnerError
return
}
fTime := time.Now()
p.result = runner.Result{
Status: runner.StatusNormal,
Time: time.Duration(rusage.Utime.Nano()),
Memory: runner.Size(rusage.Maxrss), // seems MacOS uses bytes instead of kb
SetUpTime: mTime.Sub(sTime),
RunningTime: fTime.Sub(mTime),
}
if p.result.Time > param.Limit.Time {
p.result.Status = runner.StatusTimeLimitExceeded
}
if p.result.Memory > param.Limit.Memory {
p.result.Status = runner.StatusMemoryLimitExceeded
}
switch {
case wstatus.Exited():
if status := wstatus.ExitStatus(); status != 0 {
p.result.Status = runner.StatusNonzeroExitStatus
p.result.ExitStatus = status
return
}
return
case wstatus.Signaled():
sig := wstatus.Signal()
switch sig {
case unix.SIGXCPU, unix.SIGKILL:
p.result.Status = runner.StatusTimeLimitExceeded
case unix.SIGXFSZ:
p.result.Status = runner.StatusOutputLimitExceeded
case unix.SIGSYS:
p.result.Status = runner.StatusDisallowedSyscall
default:
p.result.Status = runner.StatusSignalled
}
p.result.ExitStatus = int(sig)
return
}
}
}()
return p, nil
}
func (e *environment) WorkDir() *os.File {
return e.wd
}
func (e *environment) Open(p string, flags int, perm os.FileMode) (*os.File, error) {
return os.OpenFile(filepath.Join(e.wdPath, p), flags, perm)
}
func (e *environment) MkdirAll(p string, perm os.FileMode) error {
return os.MkdirAll(filepath.Join(e.wdPath, p), perm)
}
func (e *environment) Symlink(oldName, newName string) error {
return os.Symlink(oldName, filepath.Join(e.wdPath, newName))
}
func (e *environment) Destroy() error {
e.wd.Close()
return os.RemoveAll(e.wdPath)
}
func (e *environment) Reset() error {
return removeContents(e.wdPath)
}
// removeContents delete content of a directory
func removeContents(dir string) error {
d, err := os.Open(dir)
if err != nil {
return err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return err
}
for _, name := range names {
err = os.RemoveAll(filepath.Join(dir, name))
if err != nil {
return err
}
}
return nil
}
func killAll(pid int) {
syscall.Kill(pid, syscall.SIGKILL)
}
// collect died child processes
func collectZombie(pgid int) {
var wstatus syscall.WaitStatus
for {
if _, err := syscall.Wait4(-pgid, &wstatus, syscall.WNOHANG, nil); err != syscall.EINTR && err != nil {
break
}
}
}