github.com/bazelbuild/rules_webtesting@v0.2.0/go/wtl/service/cmd.go (about) 1 // Copyright 2016 Google Inc. 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 package service 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "os" 22 "os/exec" 23 "sync" 24 "syscall" 25 26 "github.com/bazelbuild/rules_webtesting/go/cmdhelper" 27 "github.com/bazelbuild/rules_webtesting/go/errors" 28 "github.com/bazelbuild/rules_webtesting/go/wtl/diagnostics" 29 ) 30 31 // Cmd is a service that starts an external executable. 32 type Cmd struct { 33 *Base 34 cmd *exec.Cmd 35 36 mu sync.RWMutex 37 stopMonitoring bool 38 39 done chan interface{} // this channel is closed when the process stops. 40 } 41 42 // NewCmd creates a new service for starting an external server on the host machine. 43 func NewCmd(name string, d diagnostics.Diagnostics, exe string, xvfb bool, env map[string]string, args ...string) (*Cmd, error) { 44 if xvfb { 45 args = append([]string{"-a", exe}, args...) 46 exe = "/usr/bin/xvfb-run" 47 } 48 cmd := exec.Command(exe, args...) 49 if env != nil { 50 cmd.Env = cmdhelper.BulkUpdateEnv(os.Environ(), env) 51 } 52 cmd.Stdout = os.Stdout 53 cmd.Stderr = os.Stderr 54 55 return &Cmd{ 56 Base: NewBase(name, d), 57 cmd: cmd, 58 done: make(chan interface{}), 59 }, nil 60 } 61 62 // Start starts the executable, waits for it to become healhy, and monitors it to ensure that it 63 // stays healthy. 64 func (c *Cmd) Start(ctx context.Context) error { 65 if err := c.Base.Start(ctx); err != nil { 66 return err 67 } 68 69 if err := c.cmd.Start(); err != nil { 70 return errors.New(c.Name(), err) 71 } 72 73 go c.Monitor() 74 return nil 75 } 76 77 // Stop stops the executable. 78 func (c *Cmd) Stop(ctx context.Context) error { 79 if err := c.Base.Stop(ctx); err != nil { 80 return err 81 } 82 c.StopMonitoring() 83 c.Kill() 84 return nil 85 } 86 87 // Kill kills the process. 88 func (c *Cmd) Kill() { 89 if c.cmd.Process == nil { 90 c.Warning(errors.New(c.Name(), "unable to kill; process is nil")) 91 return 92 } 93 if err := c.cmd.Process.Kill(); err != nil { 94 c.Warning(errors.New(c.Name(), fmt.Errorf("unable to kill: %v", err))) 95 } 96 } 97 98 // Wait waits for the command to exit or ctx to be done. If ctx is done 99 // before the command exits, then an error is returned. 100 func (c *Cmd) Wait(ctx context.Context) error { 101 select { 102 case <-c.done: 103 return nil 104 case <-ctx.Done(): 105 select { 106 case <-c.done: 107 return nil 108 default: 109 return errors.New(c.Name(), ctx.Err()) 110 } 111 } 112 } 113 114 // Monitor waits for cmd to exit, and when it does, logs an infrastructure failure 115 // if it exited abnormally. 116 func (c *Cmd) Monitor() { 117 err := c.cmd.Wait() 118 close(c.done) 119 120 c.mu.RLock() 121 defer c.mu.RUnlock() 122 if err == nil || c.stopMonitoring { 123 return 124 } 125 126 ee, ok := err.(*exec.ExitError) 127 if !ok { 128 return 129 } 130 signal := ee.Sys().(syscall.WaitStatus).Signal() 131 exitCode := ee.Sys().(syscall.WaitStatus).ExitStatus() 132 // KILL (0x9) and TERM (0xf) are normal when Forge is shutting down the 133 // test (e.g., in the event of a timeout). In some cases, the shell sets 134 // the exit code to 0x80 + the signal number. 135 if signal == syscall.SIGKILL || signal == syscall.SIGTERM || exitCode == 0x80|0x09 || exitCode == 0x80|0x0f { 136 return 137 } 138 c.Warning(errors.New(c.Name(), fmt.Errorf("exited prematurely with status: %v", err))) 139 } 140 141 // StdinPipe returns a pipe that will be connected to the command's standard input when the command starts. 142 func (c *Cmd) StdinPipe() (io.WriteCloser, error) { 143 pipe, err := c.cmd.StdinPipe() 144 if err != nil { 145 return nil, errors.New(c.Name(), err) 146 } 147 return pipe, nil 148 } 149 150 // StopMonitoring turns off reporting of infrastructure failures should this process exit. 151 func (c *Cmd) StopMonitoring() { 152 c.mu.Lock() 153 c.stopMonitoring = true 154 c.mu.Unlock() 155 } 156 157 // Healthy returns nil if c has been started and the process it started is still running. 158 func (c *Cmd) Healthy(ctx context.Context) error { 159 if err := c.Base.Healthy(ctx); err != nil { 160 return err 161 } 162 select { 163 case <-c.done: 164 return errors.NewPermanent(c.Name(), "executable has exited.") 165 default: 166 } 167 return nil 168 }