gaper/runner.go

165 lines
3.5 KiB
Go

package gaper
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"syscall"
"time"
)
// OSWindows is used to check if current OS is a Windows
const OSWindows = "windows"
// os errors
var errFinished = errors.New("os: process already finished")
// Runner is a interface for the run process
type Runner interface {
Run() (*exec.Cmd, error)
Kill() error
Errors() chan error
Exited() bool
IsRunning() bool
ExitStatus(err error) int
}
type runner struct {
bin string
args []string
writerStdout io.Writer
writerStderr io.Writer
command *exec.Cmd
starttime time.Time
errors chan error
end chan bool // used internally by Kill to wait a process die
}
// NewRunner creates a new runner
func NewRunner(wStdout io.Writer, wStderr io.Writer, bin string, args []string) Runner {
return &runner{
bin: bin,
args: args,
writerStdout: wStdout,
writerStderr: wStderr,
starttime: time.Now(),
errors: make(chan error),
end: make(chan bool),
}
}
// Run executes the project binary
func (r *runner) Run() (*exec.Cmd, error) {
logger.Info("Starting program")
if r.command != nil && !r.Exited() {
return r.command, nil
}
if err := r.runBin(); err != nil {
return nil, fmt.Errorf("error running: %v", err)
}
return r.command, nil
}
// Kill the current process running for the Golang project
func (r *runner) Kill() error { // nolint gocyclo
if r.command == nil || r.command.Process == nil {
return nil
}
done := make(chan error)
go func() {
<-r.end
close(done)
}()
// Trying a "soft" kill first
if runtime.GOOS == OSWindows {
if err := r.command.Process.Kill(); err != nil {
return err
}
} else if err := r.command.Process.Signal(os.Interrupt); err != nil {
return err
}
// Wait for our process to die before we return or hard kill after 3 sec
select {
case <-time.After(3 * time.Second):
if err := r.command.Process.Kill(); err != nil {
errMsg := err.Error()
// ignore error if the processed has been killed already
if errMsg != errFinished.Error() && errMsg != os.ErrInvalid.Error() {
return fmt.Errorf("failed to kill: %v", err)
}
}
case <-done:
}
r.command = nil
return nil
}
// Exited checks if the process has exited
func (r *runner) Exited() bool {
return r.command != nil && r.command.ProcessState != nil && r.command.ProcessState.Exited()
}
// IsRunning returns if the process is running
func (r *runner) IsRunning() bool {
return r.command != nil && r.command.Process != nil && r.command.Process.Pid > 0
}
// Errors get errors occurred during the build
func (r *runner) Errors() chan error {
return r.errors
}
// ExitStatus resolves the exit status
func (r *runner) ExitStatus(err error) int {
var exitStatus int
if exiterr, ok := err.(*exec.ExitError); ok {
if status, oks := exiterr.Sys().(syscall.WaitStatus); oks {
exitStatus = status.ExitStatus()
}
}
return exitStatus
}
func (r *runner) runBin() error {
r.command = exec.Command(r.bin, r.args...) // nolint gas
stdout, err := r.command.StdoutPipe()
if err != nil {
return err
}
stderr, err := r.command.StderrPipe()
if err != nil {
return err
}
// TODO: handle or log errors
go io.Copy(r.writerStdout, stdout) // nolint errcheck
go io.Copy(r.writerStderr, stderr) // nolint errcheck
err = r.command.Start()
if err != nil {
return err
}
r.starttime = time.Now()
// wait for exit errors
go func() {
r.errors <- r.command.Wait()
r.end <- true
}()
return nil
}