From c85143e88fd8f6fb7e3d831a03316589caaad6f3 Mon Sep 17 00:00:00 2001 From: Shane C Date: Sun, 3 Nov 2024 11:10:34 -0500 Subject: [PATCH] initial commit --- .gitignore | 4 + cmd/command.go | 28 ++++++ cmd/node.go | 38 ++++++++ cmd/pip.go | 37 ++++++++ cmd/python.go | 38 ++++++++ cmd/which.go | 49 ++++++++++ events.go | 18 ++++ go.mod | 5 + go.sum | 2 + interface.go | 36 +++++++ run.go | 248 +++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 75 +++++++++++++++ 12 files changed, 578 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/command.go create mode 100644 cmd/node.go create mode 100644 cmd/pip.go create mode 100644 cmd/python.go create mode 100644 cmd/which.go create mode 100644 events.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 interface.go create mode 100644 run.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12acf63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# IDE +.vscode +.idea +.fleet diff --git a/cmd/command.go b/cmd/command.go new file mode 100644 index 0000000..24c2920 --- /dev/null +++ b/cmd/command.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +func Command(cmd string) (string, error) { + + command := exec.Command("command", "-v", cmd) + + outputBytes, err := command.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 1 { + return "", ErrNotFound + } else { + return "", fmt.Errorf("command error: %w", err) + } + } + } + + return strings.Trim(string(outputBytes), "\n"), nil + +} diff --git a/cmd/node.go b/cmd/node.go new file mode 100644 index 0000000..32bc6f2 --- /dev/null +++ b/cmd/node.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +var NodeNotFound = errors.New("nodejs not found") + +func Node(options BasicOptions, args ...string) (output string, err error) { + + if _, err := Which("node", BasicOptions{ + Env: options.Env, + Sources: options.Sources, + Cwd: options.Cwd, + }); err != nil { + if errors.Is(err, ErrNotFound) { + return "", NodeNotFound + } else { + return "", err + } + } + + command := exec.Command("node", args...) + + outputBytes, err := command.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return "", fmt.Errorf("command error: %w", err) + } + } + + return strings.Trim(string(outputBytes), "\n"), nil + +} diff --git a/cmd/pip.go b/cmd/pip.go new file mode 100644 index 0000000..974b3ee --- /dev/null +++ b/cmd/pip.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +func Pip(options BasicOptions, args ...string) (output string, err error) { + + if _, err := Which("python3", BasicOptions{ + Env: options.Env, + Sources: options.Sources, + Cwd: options.Cwd, + }); err != nil { + if errors.Is(err, ErrNotFound) { + return "", PythonNotFound + } else { + return "", err + } + } + + command := exec.Command("python3", "-m", "pip") + command.Args = append(command.Args, args...) + + outputBytes, err := command.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return "", fmt.Errorf("command error: %w", err) + } + } + + return strings.Trim(string(outputBytes), "\n"), nil + +} diff --git a/cmd/python.go b/cmd/python.go new file mode 100644 index 0000000..2945854 --- /dev/null +++ b/cmd/python.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +var PythonNotFound = errors.New("python not found") + +func Python(options BasicOptions, args ...string) (output string, err error) { + + if _, err := Which("python3", BasicOptions{ + Env: options.Env, + Sources: options.Sources, + Cwd: options.Cwd, + }); err != nil { + if errors.Is(err, ErrNotFound) { + return "", PythonNotFound + } else { + return "", err + } + } + + command := exec.Command("python3", args...) + + outputBytes, err := command.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return "", fmt.Errorf("command error: %w", err) + } + } + + return strings.Trim(string(outputBytes), "\n"), nil + +} diff --git a/cmd/which.go b/cmd/which.go new file mode 100644 index 0000000..168abdd --- /dev/null +++ b/cmd/which.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +var ErrNotFound = errors.New("which: command not found") + +type BasicOptions struct { + Env map[string]string + Sources []string + Cwd string +} + +func Which(cmd string, options BasicOptions) (dir string, err error) { + + var sourceCommand strings.Builder + for _, value := range options.Sources { + sourceCommand.WriteString(fmt.Sprintf("source %s && ", value)) + } + + command := exec.Command("which", cmd) + + if options.Cwd != "" { + command.Dir = options.Cwd + } + + for k, v := range options.Env { + command.Env = append(command.Env, fmt.Sprintf("%s=%s", k, v)) + } + + outputBytes, err := command.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 1 { + return "", ErrNotFound + } else { + return "", fmt.Errorf("command error: %w", err) + } + } + } + + return strings.Trim(string(outputBytes), "\n"), nil + +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..76fe279 --- /dev/null +++ b/events.go @@ -0,0 +1,18 @@ +package linux + +const ( + EventOutput = iota + EventExit +) + +type EventOutputData struct { + Output string + CmdOptions CommandOptions +} + +type EventExitData struct { + HasSucceeded bool + ExitCode int + CmdOptions CommandOptions + Error string +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d2987f0 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitlab.com/omnibill/linux + +go 1.23.2 + +require golang.org/x/sys v0.26.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fe521ef --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..2e7950c --- /dev/null +++ b/interface.go @@ -0,0 +1,36 @@ +package linux + +import ( + "errors" + "io" + "sync" +) + +type LinuxCommand struct { + Options CommandOptions + handlers map[int]interface{} + wg sync.WaitGroup + stdout io.ReadCloser + stderr io.ReadCloser + stdin io.WriteCloser +} + +type CommandOptions struct { + Env map[string]string + Sources []string + Command string + Args []string + CustomErrors map[int8]error + Cwd string + Shell string +} + +// Errors +var ( + ErrFetchingCwd = errors.New("error fetching cwd") + 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") + ErrRunningEvt = errors.New("error running event") +) diff --git a/run.go b/run.go new file mode 100644 index 0000000..c11c461 --- /dev/null +++ b/run.go @@ -0,0 +1,248 @@ +package linux + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "golang.org/x/sys/unix" + "io" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" +) + +func NewCommand(options CommandOptions) (*LinuxCommand, error) { + + if len(options.Shell) == 0 { + options.Shell = "/bin/bash" + } + + if len(options.Cwd) == 0 { + cwd, err := os.Getwd() + if err != nil { + return nil, ErrFetchingCwd + } + options.Cwd = cwd + } + + return &LinuxCommand{ + 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 + case func(data EventExitData) error: + cmd.handlers[EventExit] = h + break + default: + return ErrInvalidHandler + } + + return nil + +} + +func (cmd *LinuxCommand) Run() error { + + //isCommandExecutable, err := cmd.isCommandExecutable(cmd.Options.Command) + //if err != nil { + // return err + //} + // + //if !isCommandExecutable { + // return ErrCommandNotExecutable + //} + + var sourceCommand strings.Builder + for _, value := range cmd.Options.Sources { + sourceCommand.WriteString(fmt.Sprintf("source %s && ", value)) + } + + 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.SysProcAttr = &unix.SysProcAttr{Setsid: true} + command.Dir = cmd.Options.Cwd + + for key, value := range cmd.Options.Env { + command.Env = append(command.Env, fmt.Sprintf("%s=%s", key, value)) + } + + var signalChannel chan os.Signal + signalChannel = make(chan os.Signal, 1) + signal.Notify(signalChannel, unix.SIGINT, unix.SIGTERM) + + if len(cmd.handlers) != 0 { + 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 { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 127 { + return ErrCommandNotFound + } else if _, ok := cmd.Options.CustomErrors[int8(exitErr.ExitCode())]; ok { + return cmd.Options.CustomErrors[int8(exitErr.ExitCode())] + } else { + return fmt.Errorf("%s: %w", ErrRunningCmd.Error(), err) + } + } + } + + if len(cmd.handlers) != 0 { + cmd.wg.Add(2) + + 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, + CmdOptions: cmd.Options, + }); 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, + CmdOptions: cmd.Options, + }); err != nil { + return + } + } + } + }() + } + + cmd.wg.Add(1) + + 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 + } + } + }() + + var exitInfo *EventExitData + + if _, ok := cmd.handlers[EventExit]; ok { + exitInfo = &EventExitData{ + HasSucceeded: true, + CmdOptions: cmd.Options, + } + } + + if err := command.Wait(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.String() != "signal: interrupt" { + if exitErr.ExitCode() == 127 { + return ErrCommandNotFound + } else if _, ok := cmd.Options.CustomErrors[int8(exitErr.ExitCode())]; ok { + if h, ok := cmd.handlers[EventExit]; ok { + if exitInfo == nil { + return fmt.Errorf("%s: %w", ErrRunningCmd.Error(), err) + } + exitInfo.HasSucceeded = false + exitInfo.ExitCode = exitErr.ExitCode() + var stdoutData bytes.Buffer + if _, err := io.Copy(&stdoutData, cmd.stdout); err != nil { + return err + } + exitInfo.Error = stdoutData.String() + err := h.(func(data EventExitData) error)(*exitInfo) + if err != nil { + return fmt.Errorf("%s: %w", ErrRunningEvt.Error(), err) + } + } + return cmd.Options.CustomErrors[int8(exitErr.ExitCode())] + } else { + if h, ok := cmd.handlers[EventExit]; ok { + if exitInfo == nil { + return fmt.Errorf("%s: %w", ErrRunningEvt.Error(), err) + } + exitInfo.HasSucceeded = false + exitInfo.ExitCode = exitErr.ExitCode() + var stdoutData bytes.Buffer + if _, err := io.Copy(&stdoutData, cmd.stdout); err != nil { + return err + } + exitInfo.Error = stdoutData.String() + err := h.(func(data EventExitData) error)(*exitInfo) + if err != nil { + return fmt.Errorf("%s: %w", ErrRunningEvt.Error(), err) + } + } + return fmt.Errorf("%s: %w", ErrRunningCmd.Error(), err) + } + } + } + + if h, ok := cmd.handlers[EventExit]; ok { + if exitInfo == nil { + return nil + } + exitInfo.ExitCode = 0 + err := h.(func(data EventExitData) error)(*exitInfo) + if err != nil { + return fmt.Errorf("%s: %w", ErrRunningEvt.Error(), err) + } + } + + close(signalChannel) + signal.Stop(signalChannel) + cmd.wg.Wait() + + return nil + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..26279da --- /dev/null +++ b/utils.go @@ -0,0 +1,75 @@ +package linux + +import ( + "errors" + "fmt" + "gitlab.com/omnibill/linux/cmd" + "golang.org/x/sys/unix" + "io/fs" + "os" + "os/exec" +) + +func (c *LinuxCommand) isCommandExecutable(command string) (bool, error) { + + whichOut, err := cmd.Which(command, cmd.BasicOptions{ + Env: c.Options.Env, + Sources: c.Options.Sources, + Cwd: c.Options.Cwd, + }) + if err != nil { + if errors.Is(err, cmd.ErrNotFound) { + if _, err := os.Stat(command); errors.Is(err, fs.ErrNotExist) { + return false, err + } + } else { + return false, err + } + } + + if len(whichOut) == 0 { + return false, nil + } + + if err := unix.Access(whichOut, unix.X_OK); err != nil { + if err == unix.EACCES { + return false, nil + } else { + fmt.Println(err) + return false, err + } + } + + return true, nil + +} + +func (c *LinuxCommand) doesCommandExist(command string) (bool, error) { + + shellCmd := exec.Command(c.Options.Shell, "-c", fmt.Sprintf("command -v %s", command)) + + if err := shellCmd.Start(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 1 { + return false, nil + } else { + return false, ErrRunningCmd + } + } + } + + if err := shellCmd.Wait(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 1 { + return false, nil + } else { + return false, ErrRunningCmd + } + } + } + + return true, nil + +}