mirror of https://github.com/maxcnunes/gaper.git
165 lines
3.5 KiB
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
|
|
}
|