From 262e253f264ac30a41d2dae58b907d3d4ed00c87 Mon Sep 17 00:00:00 2001 From: Boming Zhang Date: Fri, 1 Mar 2024 01:38:09 -0500 Subject: [PATCH] feat: cgroups v1 runner --- .editorconfig | 11 ++++++ .gitignore | 66 +++++++++++++++++++++++++++++++ Makefile | 10 +++++ README.md | 20 ++++++++++ cmd/tiger/root.go | 26 +++++++++++++ go.mod | 16 ++++++++ go.sum | 43 +++++++++++++++++++++ pkg/runner/root.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 288 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/tiger/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/runner/root.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..43384d0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{go.mod,go.sum,Makefile,Dockerfile,*.go}] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e0911 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..79c0ab6 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: all clean + +BUILD_DIR = ./build + +all: + go build -o $(BUILD_DIR)/tiger ./cmd/tiger + +clean: + rm -rf $(BUILD_DIR)/* + rm -rf *.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6e67cc --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# JOJ3 + +## Try `tiger` + +```bash +$ make clean && make && sudo ./build/tiger python3 -c 'bytearray(1024 * 1024); 10 ** 10 ** 3; print("out"); import sys; print("err", file=sys.stderr)' +rm -rf ./build/* +rm -rf *.out +go build -o ./build/tiger ./cmd/tiger +2024/03/01 01:25:34 INFO process created pid=3148763 +2024/03/01 01:25:34 INFO done success time=16.80708ms +ReturnCode: 0 +Stdout: out + +Stderr: err + +TimedOut: false +TimeNs: 0 +MemoryByte: 0 +``` diff --git a/cmd/tiger/root.go b/cmd/tiger/root.go new file mode 100644 index 0000000..35a4cdf --- /dev/null +++ b/cmd/tiger/root.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + + "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/pkg/runner" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("usage: " + os.Args[0] + " ") + os.Exit(1) + } + // hard limit of timeout 1000ms + runResult, err := runner.RunInCgroupsV1(os.Args[1:], "nobody", "/joj3.tiger", 1000) + if err != nil { + fmt.Printf("Error: %v\n", err) + } + fmt.Printf("ReturnCode: %d\n", runResult.ReturnCode) + fmt.Printf("Stdout: %s\n", runResult.Stdout) + fmt.Printf("Stderr: %s\n", runResult.Stderr) + fmt.Printf("TimedOut: %v\n", runResult.TimedOut) + fmt.Printf("TimeNs: %v\n", runResult.TimeNs) + fmt.Printf("MemoryByte: %v\n", runResult.MemoryByte) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0fd59b --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3 + +go 1.22.0 + +require ( + github.com/containerd/cgroups v1.1.0 + github.com/opencontainers/runtime-spec v1.2.0 +) + +require ( + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/godbus/dbus/v5 v5.0.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bb37a6f --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/runner/root.go b/pkg/runner/root.go new file mode 100644 index 0000000..95f026d --- /dev/null +++ b/pkg/runner/root.go @@ -0,0 +1,96 @@ +package runner + +import ( + "bytes" + "log/slog" + "os/exec" + "os/user" + "strconv" + "syscall" + "time" + + "github.com/containerd/cgroups" + "github.com/opencontainers/runtime-spec/specs-go" +) + +type RunResult struct { + ReturnCode int + Stdout []byte + Stderr []byte + TimedOut bool + TimeNs uint64 + MemoryByte uint64 +} + +func RunInCgroupsV1( + args []string, username string, cgroupsPath string, timeoutMs uint, +) (result *RunResult, err error) { + u, err := user.Lookup(username) + if err != nil { + return + } + control, err := cgroups.New( + cgroups.V1, + cgroups.StaticPath(cgroupsPath), + &specs.LinuxResources{}, + ) + if err != nil { + return + } + defer func() { + if err := control.Delete(); err != nil { + slog.Error("control.Delete", "error", err) + } + }() + cmd := exec.Command(args[0], args[1:]...) + cmd.SysProcAttr = &syscall.SysProcAttr{} + uid, _ := strconv.ParseUint(u.Uid, 10, 32) + gid, _ := strconv.ParseUint(u.Gid, 10, 32) + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + start := time.Now() + err = cmd.Start() + if err != nil { + return + } + pid := cmd.Process.Pid + slog.Info("process created", "pid", strconv.Itoa(pid)) + if err = control.Add(cgroups.Process{Pid: pid}); err != nil { + return + } + var returnCode int + exitCode := make(chan int, 1) + go func(exit_code chan int) { + if err = cmd.Wait(); err != nil { + exit_code <- err.(*exec.ExitError).ExitCode() + } else { + exit_code <- 0 + } + }(exitCode) + timeoutLimit := time.Duration(timeoutMs) * time.Millisecond + timedOut := false + select { + case returnCode = <-exitCode: + slog.Info("done success", "time", time.Since(start)) + case <-time.After(timeoutLimit): + slog.Info("done timeout", "time", time.Since(start)) + _ = cmd.Process.Kill() + returnCode = <-exitCode + timedOut = true + } + stats, err := control.Stat(cgroups.IgnoreNotExist) + if err != nil { + return + } + result = &RunResult{ + ReturnCode: returnCode, + Stdout: stdout.Bytes(), + Stderr: stderr.Bytes(), + TimedOut: timedOut, + TimeNs: stats.CPU.Usage.Kernel + stats.CPU.Usage.User, + MemoryByte: stats.Memory.Usage.Max, // Memory.Usage.Max = 0 when killed + } + return +}