1// Copyright 2019 The SwiftShader Authors. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15//go:build darwin || linux 16// +build darwin linux 17 18package shell 19 20import ( 21 "bytes" 22 "fmt" 23 "log" 24 "os" 25 "os/exec" 26 "os/signal" 27 "strconv" 28 "syscall" 29 "time" 30) 31 32func init() { 33 // As we are going to be running a number of tests concurrently, we need to 34 // limit the amount of virtual memory each test uses, otherwise memory 35 // hungry tests can bring the whole system down into a swapping apocalypse. 36 // 37 // Linux has the setrlimit() function to limit a process (and child's) 38 // virtual memory usage - but we cannot call this from the regres process 39 // as this process may need more memory than the limit allows. 40 // 41 // Unfortunately golang has no native support for setting rlimits for child 42 // processes (https://github.com/golang/go/issues/6603), so we instead wrap 43 // the exec to the test executable with another child regres process using a 44 // special --exec mode: 45 // 46 // [regres] -> [regres --exec <test-exe N args...>] -> [test-exe] 47 // ^^^^ 48 // (calls rlimit() with memory limit of N bytes) 49 50 if len(os.Args) > 3 && os.Args[1] == "--exec" { 51 exe := os.Args[2] 52 limit, err := strconv.ParseUint(os.Args[3], 10, 64) 53 if err != nil { 54 log.Fatalf("Expected memory limit as 3rd argument. %v\n", err) 55 } 56 if limit > 0 { 57 if err := syscall.Setrlimit(syscall.RLIMIT_AS, &syscall.Rlimit{Cur: limit, Max: limit}); err != nil { 58 log.Fatalln(fmt.Errorf("Setrlimit: %w", err)) 59 } 60 } 61 cmd := exec.Command(exe, os.Args[4:]...) 62 cmd.Stdin = os.Stdin 63 cmd.Stdout = os.Stdout 64 cmd.Stderr = os.Stderr 65 if err := cmd.Start(); err != nil { 66 os.Stderr.WriteString(err.Error()) 67 os.Exit(1) 68 } 69 // Forward signals to the child process 70 c := make(chan os.Signal, 1) 71 signal.Notify(c, os.Interrupt) 72 go func() { 73 for sig := range c { 74 cmd.Process.Signal(sig) 75 } 76 }() 77 cmd.Wait() 78 close(c) 79 os.Exit(cmd.ProcessState.ExitCode()) 80 } 81} 82 83// Exec runs the executable exe with the given arguments, in the working 84// directory wd, with the custom environment flags. 85// If the process does not finish within timeout a errTimeout will be returned. 86func Exec(timeout time.Duration, exe, wd string, env []string, toStdin string, args ...string) ([]byte, error) { 87 stdin := &bytes.Buffer{} 88 stdin.WriteString(toStdin) 89 90 // Shell via regres: --exec N <exe> <args...> 91 // See main() for details. 92 args = append([]string{"--exec", exe, fmt.Sprintf("%v", MaxProcMemory)}, args...) 93 b := bytes.Buffer{} 94 c := exec.Command(os.Args[0], args...) 95 c.Dir = wd 96 c.Env = env 97 c.Stdin = stdin 98 c.Stdout = &b 99 c.Stderr = &b 100 101 if err := c.Start(); err != nil { 102 return nil, err 103 } 104 105 res := make(chan error) 106 go func() { res <- c.Wait() }() 107 108 select { 109 case <-time.NewTimer(timeout).C: 110 c.Process.Signal(syscall.SIGINT) 111 time.Sleep(time.Second * 3) 112 if c.ProcessState == nil || !c.ProcessState.Exited() { 113 log.Printf("Process %v still has not exited, killing\n", c.Process.Pid) 114 syscall.Kill(-c.Process.Pid, syscall.SIGKILL) 115 } 116 return b.Bytes(), ErrTimeout{exe, timeout} 117 case err := <-res: 118 return b.Bytes(), err 119 } 120} 121