github.com/golazy/golazy@v0.0.7-0.20221012133820-968fe65a0b65/lazydev/runner/runner.go (about)

     1  // Package runner run a restart a program on signals
     2  package runner
     3  
     4  import (
     5  	"bytes"
     6  	"errors"
     7  	"os/exec"
     8  	"strings"
     9  	"syscall"
    10  	"time"
    11  )
    12  
    13  // Options holds the runner options
    14  type Options struct {
    15  	KillWaitPeriod time.Duration // Time wait for a proces to die before callking kill
    16  	ReadyString    []string      // ReadyString is the string that Runner looks for in the command output to send EventReady
    17  }
    18  
    19  // DefaultRunnerOptions are used when no RunnerOptions are passed
    20  var DefaultRunnerOptions = &Options{
    21  	KillWaitPeriod: time.Second,
    22  	ReadyString:    []string{"Listening", "Started", "Ready"},
    23  }
    24  
    25  // EventStart is fired whenever Start is called
    26  type EventStart struct {
    27  	// Err display any error that happen during start
    28  	// If Err is nil you can expect an EventStarted event
    29  	Err error
    30  }
    31  
    32  // EventStop is fired whenever Stop is called
    33  type EventStop struct {
    34  	// Err display any error that happen during stop
    35  	Err error
    36  }
    37  
    38  // EventSignal is fired whenever
    39  type EventSignal struct {
    40  	// Err display any error that happen during stop
    41  	Err error
    42  }
    43  
    44  // EventReady is fired whenever the command outputs the string Listening
    45  type EventReady struct {
    46  	Data string // Data contains the block that triggered the EventReady
    47  }
    48  
    49  // EventRestart is fired whenever Restart is called
    50  type EventRestart struct {
    51  	// Err display any error that happen during stop
    52  	// If Err is nil you can expect an EventStarted event
    53  	Err error
    54  }
    55  
    56  // EventStopped is fired whenever the process exits
    57  type EventStopped struct {
    58  	Output   []string // Holds the command output up to MaxOutputSize
    59  	ExitCode int      // ExitCode holds the exit code
    60  	RunTime  time.Duration
    61  }
    62  
    63  // EventStarted is fired whenever the subprocess is started
    64  type EventStarted struct {
    65  	Command string
    66  }
    67  
    68  // Runner is an command runner that produces events on start/stop and restart
    69  type Runner struct {
    70  	Events     <-chan (interface{}) // Events will be fired here. The channel is not expected to be closed.
    71  	options    Options
    72  	cmd        *exec.Cmd
    73  	e          chan (interface{})
    74  	startCmd   chan (chan (error))
    75  	restartCmd chan (chan (error))
    76  	stopCmd    chan (chan (error))
    77  	closeCmd   chan (chan (error))
    78  	signalCmd  chan (struct {
    79  		Signal syscall.Signal
    80  		errC   chan (error)
    81  	})
    82  	closed bool
    83  }
    84  
    85  // Close stop all the internal goroutines
    86  // After Close is called the runner can't be used anymore
    87  func (r *Runner) Close() error {
    88  	if r.closed {
    89  		return nil
    90  	}
    91  	errC := make(chan (error))
    92  	r.closeCmd <- errC
    93  	return <-errC
    94  }
    95  
    96  // Start starts the command
    97  // If the command is already running it returns ErrRunning
    98  func (r *Runner) Start() error {
    99  	if r.closed {
   100  		return ErrRunnerClosed
   101  	}
   102  	errC := make(chan (error))
   103  	r.startCmd <- errC
   104  	return <-errC
   105  }
   106  
   107  // Restart restart the process by calling Stop and then Restart. If the process is not runing it will be the same as calling Start
   108  func (r *Runner) Restart() error {
   109  	if r.closed {
   110  		return ErrRunnerClosed
   111  	}
   112  	errC := make(chan (error))
   113  	r.restartCmd <- errC
   114  	return <-errC
   115  }
   116  
   117  // Stop stops the process.
   118  // It will send an interrupt signal to the process.
   119  // If after KillWaitPeriod the process is still alive, it will send a kill signal
   120  func (r *Runner) Stop() error {
   121  	if r.closed {
   122  		return ErrRunnerClosed
   123  	}
   124  	errC := make(chan (error))
   125  	r.stopCmd <- errC
   126  	return <-errC
   127  }
   128  
   129  // Signal sends a signal to the process.
   130  // If the process is not running it returns ErrNotRunning
   131  func (r *Runner) Signal(s syscall.Signal) error {
   132  	if r.closed {
   133  		return ErrRunnerClosed
   134  	}
   135  	errC := make(chan (error))
   136  	r.signalCmd <- struct {
   137  		Signal syscall.Signal
   138  		errC   chan error
   139  	}{s, errC}
   140  	return <-errC
   141  }
   142  
   143  // New creates a new runner for the given command
   144  // if options is nil, New will use DefaultRunnerOptions
   145  func New(cmd *exec.Cmd, options *Options) *Runner {
   146  
   147  	e := make(chan (interface{}), 1024)
   148  	if options == nil {
   149  		options = DefaultRunnerOptions
   150  	}
   151  	r := &Runner{
   152  		Events:     e,
   153  		options:    *options,
   154  		cmd:        cmd,
   155  		e:          e,
   156  		startCmd:   make(chan (chan (error))),
   157  		restartCmd: make(chan (chan (error))),
   158  		stopCmd:    make(chan (chan (error))),
   159  		closeCmd:   make(chan (chan (error))),
   160  		closed:     false,
   161  	}
   162  	go r.loop()
   163  	return r
   164  }
   165  
   166  var (
   167  	// ErrRunning is the return error in the Start method
   168  	ErrRunning = errors.New("Program is already running")
   169  	// ErrCantKill is returned by Restart and Stop in case the process can't be killed
   170  	ErrCantKill = errors.New("Process is still alive after sending the kill signal")
   171  	// ErrNotRunning is retuned by the Stop and Signal command when the program is not running
   172  	ErrNotRunning = errors.New("Process is not running")
   173  	// ErrRunnerClosed is returned by any method when the runner is closed
   174  	ErrRunnerClosed = errors.New("Runner is closed")
   175  )
   176  
   177  func (r *Runner) loop() {
   178  	running := false
   179  	var done chan (int) = nil
   180  	var io chan ([]byte) = nil
   181  	var readyEventSent bool
   182  	var output []string
   183  	var startTime time.Time
   184  	var cmd *exec.Cmd
   185  
   186  	signal := func(sig syscall.Signal) error {
   187  		if cmd == nil || cmd.Process == nil {
   188  			return ErrNotRunning
   189  		}
   190  		syscall.Kill(-cmd.Process.Pid, sig)
   191  		err := cmd.Process.Signal(sig)
   192  		return err
   193  
   194  	}
   195  
   196  	start := func() error {
   197  		startTime = time.Now()
   198  		io = make(chan ([]byte))
   199  		cmd = &exec.Cmd{
   200  			Path:        r.cmd.Path,
   201  			Args:        r.cmd.Args,
   202  			Env:         r.cmd.Env,
   203  			Dir:         r.cmd.Dir,
   204  			Stdin:       r.cmd.Stdin,
   205  			Stdout:      channelWriter(io),
   206  			Stderr:      channelWriter(io),
   207  			ExtraFiles:  r.cmd.ExtraFiles,
   208  			SysProcAttr: &syscall.SysProcAttr{Setpgid: true},
   209  		}
   210  
   211  		output = make([]string, 0, 1024)
   212  		readyEventSent = false
   213  
   214  		if err := cmd.Start(); err != nil {
   215  			return err
   216  		}
   217  		running = true
   218  		r.e <- EventStarted{
   219  			Command: strings.Join(append([]string{cmd.Path}, cmd.Args...), " "),
   220  		}
   221  
   222  		// Wait for it to stop
   223  		done = make(chan (int))
   224  		go func() {
   225  			err := cmd.Wait()
   226  			if exitError, ok := err.(*exec.ExitError); ok {
   227  				done <- exitError.ExitCode()
   228  				return
   229  			}
   230  			done <- cmd.ProcessState.ExitCode()
   231  		}()
   232  
   233  		return nil
   234  	}
   235  
   236  	checkReadyEvent := func(data []byte) {
   237  		if !readyEventSent {
   238  			for _, readyString := range r.options.ReadyString {
   239  				if bytes.Contains(data, []byte(readyString)) {
   240  					r.e <- EventReady{string(data)}
   241  					readyEventSent = true
   242  				}
   243  			}
   244  		}
   245  	}
   246  
   247  	buf := []byte{}
   248  	processIO := func(data []byte) {
   249  		if r.cmd.Stdout != nil {
   250  			r.cmd.Stdout.Write(data)
   251  		}
   252  		buf := append(buf, data...)
   253  		if len(buf) == 0 {
   254  			return
   255  		}
   256  		checkReadyEvent(buf)
   257  		lines := strings.Split(string(buf), "\n")
   258  		output = append(output, lines[0:len(lines)-1]...)
   259  		buf = []byte(lines[len(lines)-1])
   260  	}
   261  
   262  	handleExit := func(statusCode int) {
   263  		if len(buf) != 0 {
   264  			output = append(output, string(buf))
   265  		}
   266  		running = false
   267  		done = nil
   268  		r.e <- EventStopped{
   269  			Output:   output,
   270  			ExitCode: statusCode,
   271  			RunTime:  time.Since(startTime),
   272  		}
   273  	}
   274  
   275  	stop := func() error {
   276  		signal(syscall.SIGINT)
   277  
   278  		wait := time.After(time.Duration(r.options.KillWaitPeriod))
   279  		var kill <-chan (time.Time) = nil
   280  
   281  		for running {
   282  			select {
   283  			case exitCode := <-done:
   284  				handleExit(exitCode)
   285  				return nil
   286  			case data := <-io:
   287  				processIO(data)
   288  			case <-wait:
   289  				signal(syscall.SIGKILL)
   290  				kill = time.After(time.Duration(r.options.KillWaitPeriod))
   291  			case <-kill:
   292  				return ErrCantKill
   293  			}
   294  		}
   295  		return nil
   296  	}
   297  
   298  	for {
   299  		select {
   300  		case errC := <-r.startCmd:
   301  			if running {
   302  				errC <- ErrRunning
   303  				r.e <- EventStart{Err: ErrRunning}
   304  				continue
   305  			}
   306  			err := start()
   307  			r.e <- EventStart{Err: err}
   308  			errC <- err
   309  
   310  		case errC := <-r.restartCmd:
   311  			if running {
   312  				err := stop()
   313  				if err != nil {
   314  					r.e <- EventRestart{Err: err}
   315  					errC <- err
   316  					continue
   317  				}
   318  			}
   319  			err := start()
   320  			r.e <- EventRestart{Err: err}
   321  			errC <- err
   322  		case errC := <-r.stopCmd:
   323  			if !running {
   324  				errC <- ErrNotRunning
   325  				r.e <- EventStop{Err: ErrNotRunning}
   326  				continue
   327  			}
   328  			err := stop()
   329  			r.e <- EventStop{Err: err}
   330  			errC <- err
   331  
   332  		case args := <-r.signalCmd:
   333  			args.errC <- signal(args.Signal)
   334  		case errC := <-r.closeCmd:
   335  			if running {
   336  				errC <- stop()
   337  			}
   338  			errC <- nil
   339  			return
   340  		case exitCode := <-done:
   341  			handleExit(exitCode)
   342  		case data := <-io:
   343  			processIO(data)
   344  		}
   345  	}
   346  }
   347  
   348  type channelWriter chan ([]byte)
   349  
   350  func (cw channelWriter) Write(data []byte) (int, error) {
   351  	cw <- data
   352  	return len(data), nil
   353  }