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  }