github.com/influx6/npkg@v0.8.8/nexec/nexec.go (about)

     1  package nexec
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"runtime"
    12  	"time"
    13  )
    14  
    15  // nerror ...
    16  var (
    17  	ErrCommandFailed = errors.New("Command failed to execute succcesfully")
    18  )
    19  
    20  type Log interface {
    21  }
    22  
    23  // CommanderOption defines a function type that aguments a commander's field.
    24  type CommanderOption func(*Commander)
    25  
    26  // Command sets the command for the Commander.
    27  func Command(format string, m ...interface{}) CommanderOption {
    28  	return func(cm *Commander) {
    29  		cm.Command = fmt.Sprintf(format, m...)
    30  	}
    31  }
    32  
    33  // SubCommands sets the subcommands for the Commander exec call.
    34  // If subcommands are set then the Binary, Flag and Command are ignored
    35  // and the values of the subcommand is used.
    36  func SubCommands(p ...string) CommanderOption {
    37  	return func(cm *Commander) {
    38  		cm.SubCommands = p
    39  	}
    40  }
    41  
    42  // Dir sets the Directory for the Commander exec call.
    43  func Dir(p string) CommanderOption {
    44  	return func(cm *Commander) {
    45  		cm.Dir = p
    46  	}
    47  }
    48  
    49  // Binary sets the binary command for the Commander.
    50  func Binary(bin string, flag string) CommanderOption {
    51  	return func(cm *Commander) {
    52  		cm.Binary = bin
    53  		cm.Flag = flag
    54  	}
    55  }
    56  
    57  // Timeout sets the commander to run in synchronouse mode.
    58  func Timeout(d time.Duration) CommanderOption {
    59  	return func(cm *Commander) {
    60  		cm.Timeout = d
    61  	}
    62  }
    63  
    64  // Sync sets the commander to run in synchronouse mode.
    65  func Sync() CommanderOption {
    66  	return SetAsync(false)
    67  }
    68  
    69  // Async sets the commander to run in asynchronouse mode.
    70  func Async() CommanderOption {
    71  	return SetAsync(true)
    72  }
    73  
    74  // SetAsync sets the command for the Commander.
    75  func SetAsync(b bool) CommanderOption {
    76  	return func(cm *Commander) {
    77  		cm.Async = b
    78  	}
    79  }
    80  
    81  // Input sets the input reader for the Commander.
    82  func Input(in io.Reader) CommanderOption {
    83  	return func(cm *Commander) {
    84  		cm.In = in
    85  	}
    86  }
    87  
    88  // Output sets the output writer for the Commander.
    89  func Output(out io.Writer) CommanderOption {
    90  	return func(cm *Commander) {
    91  		cm.Out = out
    92  	}
    93  }
    94  
    95  // Err sets the error writer for the Commander.
    96  func Err(err io.Writer) CommanderOption {
    97  	return func(cm *Commander) {
    98  		cm.Err = err
    99  	}
   100  }
   101  
   102  // Envs sets the map of environment for the Commander.
   103  func Envs(envs map[string]string) CommanderOption {
   104  	return func(cm *Commander) {
   105  		cm.Envs = envs
   106  	}
   107  }
   108  
   109  // Apply takes the giving series of CommandOption returning a function that always applies them to passed in commanders.
   110  func Apply(ops ...CommanderOption) CommanderOption {
   111  	return func(cm *Commander) {
   112  		for _, op := range ops {
   113  			op(cm)
   114  		}
   115  	}
   116  }
   117  
   118  // ApplyImmediate applies the options immediately to the Commander.
   119  func ApplyImmediate(cm *Commander, ops ...CommanderOption) *Commander {
   120  	for _, op := range ops {
   121  		op(cm)
   122  	}
   123  
   124  	return cm
   125  }
   126  
   127  // Commander runs provided command within a /bin/sh -c "{COMMAND}", returning
   128  // response associatedly. It also attaches if provided stdin, stdout and stderr readers/writers.
   129  // Commander allows you to set the binary to use and flag, where each defaults to /bin/sh for binary
   130  // and -c for flag respectively.
   131  type Commander struct {
   132  	Async       bool
   133  	Command     string
   134  	SubCommands []string
   135  	Timeout     time.Duration
   136  	Dir         string
   137  	Binary      string
   138  	Flag        string
   139  	Envs        map[string]string
   140  	In          io.Reader
   141  	Out         io.Writer
   142  	Err         io.Writer
   143  }
   144  
   145  // New returns a new Commander instance.
   146  func New(ops ...CommanderOption) *Commander {
   147  	cm := new(Commander)
   148  
   149  	for _, op := range ops {
   150  		op(cm)
   151  	}
   152  
   153  	return cm
   154  }
   155  
   156  // Exec executes giving command associated within the command with os/exec.
   157  func (c *Commander) Exec(ctx context.Context) (int, error) {
   158  	if c.Binary == "" {
   159  		if runtime.GOOS != "windows" {
   160  			c.Binary = "/bin/sh"
   161  			if c.Flag == "" {
   162  				c.Flag = "-c"
   163  			}
   164  		}
   165  
   166  		if runtime.GOOS == "windows" {
   167  			c.Binary = "cmd"
   168  			if c.Flag == "" {
   169  				c.Flag = "/C"
   170  			}
   171  		}
   172  	}
   173  
   174  	var cancel func()
   175  	if c.Timeout > 0 {
   176  		ctx, cancel = context.WithTimeout(ctx, c.Timeout)
   177  	}
   178  
   179  	if cancel != nil {
   180  		defer cancel()
   181  	}
   182  
   183  	var execCommand []string
   184  
   185  	switch {
   186  	case c.Command == "" && len(c.SubCommands) != 0:
   187  		execCommand = c.SubCommands
   188  	case c.Command != "" && len(c.SubCommands) == 0 && c.Binary != "":
   189  		execCommand = append(execCommand, c.Binary, c.Flag, c.Command)
   190  	case c.Command != "" && len(c.SubCommands) != 0 && c.Binary != "":
   191  		execCommand = append(append(execCommand, c.Binary, c.Flag, c.Command), c.SubCommands...)
   192  	case c.Command != "" && len(c.SubCommands) == 0 && c.Binary == "":
   193  		execCommand = append(execCommand, c.Command)
   194  	case c.Command != "" && len(c.SubCommands) != 0:
   195  		execCommand = append(append(execCommand, c.Command), c.SubCommands...)
   196  	default:
   197  		return -1, errors.New("commands with/without subcommands must be specified")
   198  	}
   199  
   200  	var errCopy bytes.Buffer
   201  	var multiErr io.Writer
   202  
   203  	if c.Err != nil {
   204  		multiErr = io.MultiWriter(&errCopy, c.Err)
   205  	} else {
   206  		multiErr = &errCopy
   207  	}
   208  
   209  	cmder := exec.Command(execCommand[0], execCommand[1:]...)
   210  	cmder.Dir = c.Dir
   211  	cmder.Stderr = multiErr
   212  	cmder.Stdin = c.In
   213  	cmder.Stdout = c.Out
   214  	cmder.Env = os.Environ()
   215  
   216  	if c.Envs != nil {
   217  		for name, val := range c.Envs {
   218  			cmder.Env = append(cmder.Env, fmt.Sprintf("%s=%s", name, val))
   219  		}
   220  	}
   221  
   222  	if !c.Async {
   223  		err := cmder.Run()
   224  		return getExitStatus(err), err
   225  	}
   226  
   227  	if err := cmder.Start(); err != nil {
   228  		return getExitStatus(err), err
   229  	}
   230  
   231  	go func() {
   232  		<-ctx.Done()
   233  		if cmder.Process == nil {
   234  			return
   235  		}
   236  
   237  		cmder.Process.Kill()
   238  	}()
   239  
   240  	if err := cmder.Wait(); err != nil {
   241  		return getExitStatus(err), err
   242  	}
   243  
   244  	if cmder.ProcessState == nil {
   245  		return 0, nil
   246  	}
   247  
   248  	if !cmder.ProcessState.Success() {
   249  		return -1, ErrCommandFailed
   250  	}
   251  
   252  	return 0, nil
   253  }
   254  
   255  type exitStatus interface {
   256  	ExitStatus() int
   257  }
   258  
   259  func getExitStatus(err error) int {
   260  	if err == nil {
   261  		return 0
   262  	}
   263  	if e, ok := err.(exitStatus); ok {
   264  		return e.ExitStatus()
   265  	}
   266  	if e, ok := err.(*exec.ExitError); ok {
   267  		if ex, ok := e.Sys().(exitStatus); ok {
   268  			return ex.ExitStatus()
   269  		}
   270  	}
   271  	return 1
   272  }