package runner import ( "errors" "io" "fmt" "log" "os" "strings" exec "os/exec" cgroups "github.com/containerd/cgroups" rand "math/rand" "time" "git.sch9.ru/new_gate/ms-tester/pkg/models" ) const runIdLength = 20 type Limits struct { Processes int32 Core int32 Memory int32 Time int32 } const Mib = 1024*1024; const BuildMemory = 1024*Mib; const BuildTime = 5000; type RunnerService struct { CoresIsolated []int32 CoreFreed chan int32 } func genRunId() (runId string) { for i := 0; i < runIdLength; i++ { runId += string('a' + byte(rand.Int31n(26))) } return } func int32ToString(n int32) string { return fmt.Sprintf("%d", n) } func copyFile(src string,dst string) error { srcFile,err := os.Open(src) if(err!=nil) {return err} defer srcFile.Close() dstFile,err := os.Create(dst) if(err!=nil) {return err} defer dstFile.Close() _,err = io.Copy(dstFile,srcFile) if(err!=nil) {return err} err = dstFile.Sync() if(err!=nil) {return err} return nil } func newRunFolder() (string,error) { runId := genRunId() err := os.Mkdir("runs/" + runId, 0777) if(err!=nil) { return "",err } err = os.Chmod("runs/" + runId, 0777) if(err!=nil) { return "",err } return runId,nil } func extractNumbers(s string) (result []int32) { lastNumber, isNumber := false,false var curNumber int32 = 0 for _, char := range s { isNumber = (char >= '0' && char <= '9') if(isNumber) { curNumber *= 10 curNumber += int32(char - '0') } if(!isNumber && lastNumber) { result = append(result, curNumber) curNumber = 0 } lastNumber = isNumber } if(lastNumber) { result = append(result, curNumber) } return } func NewRunnerService() (*RunnerService, error) { runnerService := RunnerService{make([]int32,0),make(chan int32)} rand.Seed(time.Now().UnixNano()) //croup initialisation: if cgroups.Mode() == cgroups.Unified { log.Println("cgroups v2 usage approved") } else { return nil,fmt.Errorf("cgroups v2 are not enabled")//TODO: trouble description } //isolated cores initialisation: cmdlineBytes := make([]byte, 400) cmdlineFile, _ := os.Open("/proc/cmdline"); countCmdlineBytes, _ := cmdlineFile.Read(cmdlineBytes); cmdline := string(cmdlineBytes[:countCmdlineBytes]) kernelParams := strings.Split(cmdline," ") for _, param := range kernelParams{ if(len(param) >= 9 && param[:9] == "isolcpus=") { runnerService.CoresIsolated = append(runnerService.CoresIsolated, extractNumbers(param[9:])...) } } if(len(runnerService.CoresIsolated) == 0) { return nil,fmt.Errorf("no free cores available")//TODO: trouble description } go func() { for _,core := range runnerService.CoresIsolated { runnerService.CoreFreed <- core } }() log.Println("running on cores:", runnerService.CoresIsolated) //runs directory os.Mkdir("runs",0777) os.Chmod("runs",0777) //complete log.Println("Runner initialisation successful!") return &runnerService,nil } func (runnerService RunnerService) Build(languageId int32, filename string) { core := <-runnerService.CoreFreed runId,err := newRunFolder() if(err!=nil) {log.Fatal copyFile(filename,"runs/"+runId+"/src") runnerService.IsolatedRun(models.Languages[languageId].CompileCmd,"../runs/"+runId,Limits{Processes: 10,Core:core,Memory:BuildMemory,Time:BuildTime}); copyFile("runs/"+runId+"/executable","executable") os.RemoveAll("runs/" + runId) go func() { runnerService.CoreFreed <- core }() } func (runnerService RunnerService) RunTest(solutionLanguageId int32, solutionFilename string, inputFilename string, checkerLanguageId int32, checkerFilename string, limits Limits) (models.Result) { core := <-runnerService.CoreFreed testRunId,err := newRunFolder() if(err!=nil) {log.Fatal copyFile(solutionFilename,"runs/"+testRunId+"/executable") copyFile(inputFilename,"runs/"+testRunId+"/in") code,err = runnerService.IsolatedRun(models.Languages[languageId].RunCmd,"../runs/"+testRunId,limits); defer os.RemoveAll("runs/" + testRunId) if(err!=nil) {log.Fatal(err);} if(code!=0) {return models.RuntimeError} checkRunId,err := newRunFolder() copyFile("runs/"+testRunId+"/out","runs/"+checkRunId+"/out") err = runnerService.IsolatedRun(models.Languages[languageId].RunCmd,"../runs/"+checkRunId,Limits{Processes: 10,Core:core,Memory:BuildMemory,Time:BuildTime}); //FIXME add input/output/answer files options defer os.RemoveAll("runs/" + checkRunId) if(err!=nil) {log.Fatal(err);} switch code { case 0: return models.Accepted //FIXME add model constants case 1: return models.WrongAnswer case 2: return models.PresentationError case 3: return models.SystemFailDuringTesting } return models.SystemFailDuringTesting //bad checker result is a system fail go func() { runnerService.CoreFreed <- core }() } func (runnerService RunnerService) IsolatedRun(command []string, sharedFolder string, limits Limits) (int32,error) { args := []string{int32ToString(limits.Processes),int32ToString(limits.Core), int32ToString(limits.Memory), int32ToString(limits.Time), sharedFolder} args = append(args, command...) log.Println("running isolated process with command:",args) cmd := exec.Command("starter/starter", args...) err := cmd.Run(); if errors.Is(err,exec.ExitError) { return err.ProcessState.ExitCode,nil } else if(err!=nil) { return 0,err } return 0,nil }