initial commit
This commit is contained in:
parent
2540834436
commit
c85143e88f
12 changed files with 578 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
.fleet
|
28
cmd/command.go
Normal file
28
cmd/command.go
Normal file
|
@ -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
|
||||
|
||||
}
|
38
cmd/node.go
Normal file
38
cmd/node.go
Normal file
|
@ -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
|
||||
|
||||
}
|
37
cmd/pip.go
Normal file
37
cmd/pip.go
Normal file
|
@ -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
|
||||
|
||||
}
|
38
cmd/python.go
Normal file
38
cmd/python.go
Normal file
|
@ -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
|
||||
|
||||
}
|
49
cmd/which.go
Normal file
49
cmd/which.go
Normal file
|
@ -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
|
||||
|
||||
}
|
18
events.go
Normal file
18
events.go
Normal file
|
@ -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
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module gitlab.com/omnibill/linux
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require golang.org/x/sys v0.26.0 // indirect
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -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=
|
36
interface.go
Normal file
36
interface.go
Normal file
|
@ -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")
|
||||
)
|
248
run.go
Normal file
248
run.go
Normal file
|
@ -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
|
||||
|
||||
}
|
75
utils.go
Normal file
75
utils.go
Normal file
|
@ -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
|
||||
|
||||
}
|
Loading…
Reference in a new issue