From 750686fd1ffbc8ed4f9192ae07079e3bb0958e8c Mon Sep 17 00:00:00 2001 From: Shane C Date: Sat, 6 Jul 2024 11:48:45 -0400 Subject: [PATCH] add ability for live output, add events system --- linux/events.go | 10 ++++ linux/interface.go | 10 +++- linux/run.go | 134 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 linux/events.go diff --git a/linux/events.go b/linux/events.go new file mode 100644 index 0000000..df2c37c --- /dev/null +++ b/linux/events.go @@ -0,0 +1,10 @@ +package linux + +const ( + EventOutput = iota + EventStderr +) + +type EventOutputData struct { + Output string +} diff --git a/linux/interface.go b/linux/interface.go index d958111..b0267d0 100644 --- a/linux/interface.go +++ b/linux/interface.go @@ -2,10 +2,17 @@ package linux import ( "errors" + "io" + "sync" ) type LinuxCommand struct { - Options CommandOptions + Options CommandOptions + handlers map[int]interface{} + wg sync.WaitGroup + stdout io.ReadCloser + stderr io.ReadCloser + stdin io.WriteCloser } type CommandOptions struct { @@ -24,4 +31,5 @@ var ( ErrRunningCmd = errors.New("error running command") ErrCommandNotFound = errors.New("error command not found") ErrCommandNotExecutable = errors.New("error command not executable") + ErrInvalidHandler = errors.New("invalid handler") ) diff --git a/linux/run.go b/linux/run.go index b02cdef..68dc1b7 100644 --- a/linux/run.go +++ b/linux/run.go @@ -1,10 +1,15 @@ package linux import ( + "bufio" "errors" "fmt" + "golang.org/x/sys/unix" "os" "os/exec" + "os/signal" + "strings" + "syscall" ) func NewCommand(options CommandOptions) (*LinuxCommand, error) { @@ -22,34 +27,68 @@ func NewCommand(options CommandOptions) (*LinuxCommand, error) { } return &LinuxCommand{ - Options: options, + Options: options, + handlers: make(map[int]interface{}), }, nil } +func (cmd *LinuxCommand) AddHandler(handler interface{}) error { + + switch h := handler.(type) { + case func(data EventOutputData) error: + cmd.handlers[EventOutput] = h + break + default: + return ErrInvalidHandler + } + + return nil + +} + func (cmd *LinuxCommand) Run() error { - var sourceCommand string + var sourceCommand strings.Builder for _, value := range cmd.Options.Sources { - sourceCommand += fmt.Sprintf("source %s && ", value) + sourceCommand.WriteString(fmt.Sprintf("source %s && ", value)) } - command := exec.Command(cmd.Options.Shell, "-c", sourceCommand+cmd.Options.Command) + var commandOptions strings.Builder + commandOptions.WriteString(" ") + for index, arg := range cmd.Options.Args { + if len(cmd.Options.Args)-1 == index { + commandOptions.WriteString(fmt.Sprintf("%s", arg)) + } else { + commandOptions.WriteString(fmt.Sprintf("%s ", arg)) + } + } + + command := exec.Command(cmd.Options.Shell, "-c", sourceCommand.String()+cmd.Options.Command+commandOptions.String()) command.Dir = cmd.Options.Cwd - command.Args = append(command.Args, cmd.Options.Args...) - // Loop through env to format and add them to the command. - for key, value := range cmd.Options.Env { - command.Env = append(command.Env, fmt.Sprintf("%s=%s", key, value)) - } + var signalChannel chan os.Signal - isCommandExecutable, err := cmd.isCommandExecutable(cmd.Options.Command) - if err != nil { - return err - } + if len(cmd.handlers) != 0 { + command.SysProcAttr = &unix.SysProcAttr{Setsid: true} + signalChannel = make(chan os.Signal, 1) + signal.Notify(signalChannel, unix.SIGINT, unix.SIGTERM) - if !isCommandExecutable { - return ErrCommandNotExecutable + var err error + cmd.stdout, err = command.StdoutPipe() + if err != nil { + return err + } + + cmd.stdin, err = command.StdinPipe() + if err != nil { + return err + } + + cmd.stderr, err = command.StderrPipe() + if err != nil { + return err + } } if err := command.Start(); err != nil { @@ -65,6 +104,67 @@ func (cmd *LinuxCommand) Run() error { } } + cmd.wg.Add(3) + + go func() { + defer cmd.wg.Done() + scanner := bufio.NewScanner(cmd.stderr) + for scanner.Scan() { + line := scanner.Text() + if h, ok := cmd.handlers[EventOutput]; ok { + if err := h.(func(data EventOutputData) error)(EventOutputData{ + Output: line, + }); err != nil { + return + } + } + } + }() + + go func() { + defer cmd.wg.Done() + + scanner := bufio.NewScanner(cmd.stdout) + for scanner.Scan() { + line := scanner.Text() + if h, ok := cmd.handlers[EventOutput]; ok { + if err := h.(func(data EventOutputData) error)(EventOutputData{ + Output: line, + }); err != nil { + return + } + } + } + }() + + go func() { + defer cmd.wg.Done() + + select { + case _, ok := <-signalChannel: + if !ok { + return + } + if err := unix.Kill(-command.Process.Pid, syscall.SIGINT); err != nil { + return + } + } + }() + + // Loop through env to format and add them to the command. + for key, value := range cmd.Options.Env { + command.Env = append(command.Env, fmt.Sprintf("%s=%s", key, value)) + } + + isCommandExecutable, err := cmd.isCommandExecutable(cmd.Options.Command) + if err != nil { + return err + } + + if !isCommandExecutable { + return ErrCommandNotExecutable + } + if err := command.Wait(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { @@ -78,6 +178,10 @@ func (cmd *LinuxCommand) Run() error { } } + close(signalChannel) + signal.Stop(signalChannel) + cmd.wg.Wait() + return nil }