github.com/m3db/m3@v1.5.0/src/m3em/os/exec/process_monitor.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package exec
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"os"
    27  	oexec "os/exec"
    28  	"path"
    29  	"path/filepath"
    30  	"sync"
    31  
    32  	xerrors "github.com/m3db/m3/src/x/errors"
    33  )
    34  
    35  var (
    36  	defaultStdoutSuffix = "out"
    37  	defaultStderrSuffix = "err"
    38  )
    39  
    40  var (
    41  	errUnableToStartClosed  = fmt.Errorf("unable to start: process monitor Closed()")
    42  	errUnableToStartRunning = fmt.Errorf("unable to start: process already running")
    43  	errUnableToStopStoped   = fmt.Errorf("unable to stop: process not running")
    44  )
    45  
    46  func (m EnvMap) toSlice() []string {
    47  	if m == nil || len(m) == 0 {
    48  		return nil
    49  	}
    50  	envVars := os.Environ()
    51  	for k, v := range m {
    52  		envVars = append(envVars, fmt.Sprintf("%s=%s", k, v))
    53  	}
    54  	return envVars
    55  }
    56  
    57  type processListener struct {
    58  	completeFn func()
    59  	errFn      func(error)
    60  }
    61  
    62  // NewProcessListener returns a new ProcessListener
    63  func NewProcessListener(
    64  	completeFn func(),
    65  	errFn func(error),
    66  ) ProcessListener {
    67  	return &processListener{
    68  		completeFn: completeFn,
    69  		errFn:      errFn,
    70  	}
    71  }
    72  
    73  func (pl *processListener) OnComplete() {
    74  	if fn := pl.completeFn; fn != nil {
    75  		fn()
    76  	}
    77  }
    78  
    79  func (pl *processListener) OnError(err error) {
    80  	if fn := pl.errFn; fn != nil {
    81  		fn(err)
    82  	}
    83  }
    84  
    85  type startFn func()
    86  
    87  type processMonitor struct {
    88  	sync.Mutex
    89  	cmd        *oexec.Cmd
    90  	cancelFunc context.CancelFunc
    91  	stdoutPath string
    92  	stderrPath string
    93  	stdoutFd   *os.File
    94  	stderrFd   *os.File
    95  	startFn    startFn
    96  	listener   ProcessListener
    97  	err        error
    98  	running    bool
    99  	done       bool
   100  }
   101  
   102  // NewProcessMonitor creates a new ProcessMonitor
   103  func NewProcessMonitor(cmd Cmd, pl ProcessListener) (ProcessMonitor, error) {
   104  	if err := cmd.Validate(); err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	fileInfo, err := os.Stat(cmd.OutputDir)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("specified directory does not exist: %v", err)
   111  	}
   112  
   113  	if !fileInfo.IsDir() {
   114  		return nil, fmt.Errorf("specified path is not a directory: %v", cmd.OutputDir)
   115  	}
   116  
   117  	base := filepath.Base(cmd.Path)
   118  	stdoutPath := outputPath(cmd.OutputDir, base, defaultStdoutSuffix)
   119  	stdoutWriter, err := newWriter(stdoutPath)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("unable to open stdout writer: %v", err)
   122  	}
   123  
   124  	stderrPath := outputPath(cmd.OutputDir, base, defaultStderrSuffix)
   125  	stderrWriter, err := newWriter(stderrPath)
   126  	if err != nil {
   127  		stdoutWriter.Close()
   128  		return nil, fmt.Errorf("unable to open stderr writer: %v", err)
   129  	}
   130  
   131  	ctx, cancel := context.WithCancel(context.Background())
   132  	ocmd := oexec.CommandContext(ctx, cmd.Path)
   133  	ocmd.Args = cmd.Args
   134  	ocmd.Dir = cmd.OutputDir
   135  	ocmd.Stderr = stderrWriter
   136  	ocmd.Stdout = stdoutWriter
   137  	ocmd.Env = cmd.Env.toSlice()
   138  
   139  	pm := &processMonitor{
   140  		listener:   pl,
   141  		stdoutFd:   stdoutWriter,
   142  		stderrFd:   stderrWriter,
   143  		stdoutPath: stdoutPath,
   144  		stderrPath: stderrPath,
   145  		cancelFunc: cancel,
   146  		cmd:        ocmd,
   147  	}
   148  	pm.startFn = pm.startAsync
   149  
   150  	return pm, nil
   151  }
   152  
   153  func (pm *processMonitor) notifyListener(err error) {
   154  	if pm.listener == nil {
   155  		return
   156  	}
   157  	if err == nil {
   158  		pm.listener.OnComplete()
   159  	} else {
   160  		pm.listener.OnError(err)
   161  	}
   162  }
   163  
   164  func (pm *processMonitor) Start() error {
   165  	pm.Lock()
   166  	if pm.done {
   167  		pm.Unlock()
   168  		return errUnableToStartClosed
   169  	}
   170  
   171  	if pm.err != nil {
   172  		pm.Unlock()
   173  		return pm.err
   174  	}
   175  
   176  	if pm.running {
   177  		pm.Unlock()
   178  		return errUnableToStartRunning
   179  	}
   180  	pm.Unlock()
   181  
   182  	pm.startFn()
   183  	return nil
   184  }
   185  
   186  func (pm *processMonitor) startAsync() {
   187  	go pm.startSync()
   188  }
   189  
   190  func (pm *processMonitor) startSync() {
   191  	pm.Lock()
   192  	pm.running = true
   193  	pm.Unlock()
   194  
   195  	err := pm.cmd.Run()
   196  	pm.Lock()
   197  	defer pm.Unlock()
   198  
   199  	// only notify when Stop() has not been called explicitly
   200  	if done := pm.done; !done {
   201  		pm.notifyListener(err)
   202  		pm.err = err
   203  	}
   204  	pm.running = false
   205  }
   206  
   207  func (pm *processMonitor) Stop() error {
   208  	pm.Lock()
   209  	defer pm.Unlock()
   210  	pm.done = true
   211  	return pm.stopWithLock()
   212  }
   213  
   214  func (pm *processMonitor) stopWithLock() error {
   215  	if pm.err != nil {
   216  		return pm.err
   217  	}
   218  
   219  	if !pm.running {
   220  		return errUnableToStopStoped
   221  	}
   222  
   223  	pm.cancelFunc()
   224  	return nil
   225  }
   226  
   227  func (pm *processMonitor) Running() bool {
   228  	pm.Lock()
   229  	defer pm.Unlock()
   230  	return pm.running
   231  }
   232  
   233  func (pm *processMonitor) Err() error {
   234  	pm.Lock()
   235  	defer pm.Unlock()
   236  	return pm.err
   237  }
   238  
   239  func (pm *processMonitor) Close() error {
   240  	pm.Lock()
   241  	defer pm.Unlock()
   242  	if pm.done {
   243  		return nil
   244  	}
   245  
   246  	var multiErr xerrors.MultiError
   247  	if pm.running {
   248  		multiErr = multiErr.Add(pm.stopWithLock())
   249  	}
   250  
   251  	if pm.stderrFd != nil {
   252  		multiErr = multiErr.Add(pm.stderrFd.Close())
   253  		pm.stderrFd = nil
   254  	}
   255  	if pm.stdoutFd != nil {
   256  		multiErr = multiErr.Add(pm.stdoutFd.Close())
   257  		pm.stdoutFd = nil
   258  	}
   259  	pm.done = true
   260  	return multiErr.FinalError()
   261  }
   262  
   263  func (pm *processMonitor) StdoutPath() string {
   264  	return pm.stdoutPath
   265  }
   266  
   267  func (pm *processMonitor) StderrPath() string {
   268  	return pm.stderrPath
   269  }
   270  
   271  func outputPath(outputDir string, prefix string, suffix string) string {
   272  	return path.Join(outputDir, fmt.Sprintf("%s.%s", prefix, suffix))
   273  }
   274  
   275  func newWriter(outputPath string) (*os.File, error) {
   276  	fd, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	return fd, nil
   281  }