193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
|
package shell
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"time"
|
||
|
|
||
|
"github.com/go-cmd/cmd"
|
||
|
"go.wit.com/log"
|
||
|
)
|
||
|
|
||
|
// this is a simplified interaction with the excellent
|
||
|
// go-cmd/cmd package to work 'shell' like.
|
||
|
|
||
|
// in all cases here, STDERR -> STDOUT
|
||
|
// If you want the output from whatever you run
|
||
|
// to be captured like it appears when you see it
|
||
|
// on the command line, this is what this tries to do
|
||
|
|
||
|
/*
|
||
|
if r := shell.Run([]{"ping", "-c", "3", "localhost"}); r.Error == nil {
|
||
|
if r.Exit == 0 {
|
||
|
log.Println("ran ok")
|
||
|
} else {
|
||
|
log.Println("ran")
|
||
|
}
|
||
|
// all stdout/stderr captured in r.Stdout
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
// shortcut, sends a blank value for pwd
|
||
|
// which means the exec Dir is not set
|
||
|
// echos output (otherwise use RunQuiet)
|
||
|
func Run(args []string) cmd.Status {
|
||
|
return PathRun("", args)
|
||
|
}
|
||
|
|
||
|
// exec the cmd at a filepath. this does not change the working directory
|
||
|
// sets the exec dir if it's not an empty string
|
||
|
// combines stdout and stderr
|
||
|
// echo's output (otherwise use PathRunQuiet()
|
||
|
// this is basically the exact example from the go-cmd/cmd devs
|
||
|
// where the have rocked out a proper smart read on both filehandles
|
||
|
// https://dave.cheney.net/2013/04/30/curious-channels
|
||
|
func PathRun(path string, args []string) cmd.Status {
|
||
|
var save []string // combined stdout & stderr
|
||
|
var arg0 string
|
||
|
var argx []string
|
||
|
// Check if the slice has at least one element (the command name)
|
||
|
if len(args) == 0 {
|
||
|
var s cmd.Status
|
||
|
s.Error = errors.New("Error: Command slice is empty.")
|
||
|
return s
|
||
|
}
|
||
|
if len(args) == 1 {
|
||
|
// Pass the first element as the command, and the rest as variadic arguments
|
||
|
arg0 = args[0]
|
||
|
} else {
|
||
|
arg0 = args[0]
|
||
|
argx = args[1:]
|
||
|
}
|
||
|
|
||
|
// Disable output buffering, enable streaming
|
||
|
cmdOptions := cmd.Options{
|
||
|
Buffered: false,
|
||
|
Streaming: true,
|
||
|
}
|
||
|
|
||
|
// Create Cmd with options
|
||
|
envCmd := cmd.NewCmdOptions(cmdOptions, arg0, argx...)
|
||
|
if path != "" {
|
||
|
// set the path for exec
|
||
|
envCmd.Dir = path
|
||
|
}
|
||
|
|
||
|
// Print STDOUT and STDERR lines streaming from Cmd
|
||
|
doneChan := make(chan struct{})
|
||
|
go func() {
|
||
|
defer close(doneChan)
|
||
|
// Done when both channels have been closed
|
||
|
// https://dave.cheney.net/2013/04/30/curious-channels
|
||
|
for envCmd.Stdout != nil || envCmd.Stderr != nil {
|
||
|
select {
|
||
|
case line, open := <-envCmd.Stdout:
|
||
|
if !open {
|
||
|
envCmd.Stdout = nil
|
||
|
continue
|
||
|
}
|
||
|
save = append(save, line)
|
||
|
fmt.Println(line)
|
||
|
case line, open := <-envCmd.Stderr:
|
||
|
if !open {
|
||
|
envCmd.Stderr = nil
|
||
|
continue
|
||
|
}
|
||
|
save = append(save, line)
|
||
|
fmt.Println(line)
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Run and wait for Cmd to return, discard Status
|
||
|
<-envCmd.Start()
|
||
|
|
||
|
// Wait for goroutine to print everything
|
||
|
<-doneChan
|
||
|
|
||
|
s := envCmd.Status()
|
||
|
s.Stdout = save
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// absolutely doesn't echo anything
|
||
|
func PathRunQuiet(pwd string, args []string) cmd.Status {
|
||
|
// Check if the slice has at least one element (the command name)
|
||
|
if len(args) == 0 {
|
||
|
var s cmd.Status
|
||
|
s.Error = errors.New("Error: Command slice is empty.")
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// Start a long-running process, capture stdout and stderr
|
||
|
a, b := RemoveFirstElement(args)
|
||
|
findCmd := cmd.NewCmd(a, b...)
|
||
|
if pwd != "" {
|
||
|
findCmd.Dir = pwd
|
||
|
}
|
||
|
statusChan := findCmd.Start() // non-blocking
|
||
|
|
||
|
ticker := time.NewTicker(2 * time.Second)
|
||
|
|
||
|
// this is interesting, maybe useful, but wierd, but neat. interesting even
|
||
|
// Print last line of stdout every 2s
|
||
|
go func() {
|
||
|
for range ticker.C {
|
||
|
status := findCmd.Status()
|
||
|
n := len(status.Stdout)
|
||
|
if n != 0 {
|
||
|
fmt.Println("todo:removethisecho", status.Stdout[n-1])
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Stop command after 1 hour
|
||
|
go func() {
|
||
|
<-time.After(1 * time.Hour)
|
||
|
findCmd.Stop()
|
||
|
}()
|
||
|
|
||
|
// Check if command is done
|
||
|
select {
|
||
|
case finalStatus := <-statusChan:
|
||
|
log.Info("finalStatus =", finalStatus.Exit, finalStatus.Error)
|
||
|
return finalStatus
|
||
|
// done
|
||
|
default:
|
||
|
// no, still running
|
||
|
}
|
||
|
|
||
|
// Block waiting for command to exit, be stopped, or be killed
|
||
|
finalStatus := <-statusChan
|
||
|
return finalStatus
|
||
|
}
|
||
|
|
||
|
func blah(cmd []string) {
|
||
|
r := Run(cmd)
|
||
|
log.Info("cmd =", r.Cmd)
|
||
|
log.Info("complete =", r.Complete)
|
||
|
log.Info("exit =", r.Exit)
|
||
|
log.Info("err =", r.Error)
|
||
|
log.Info("len(stdout+stderr) =", len(r.Stdout))
|
||
|
}
|
||
|
|
||
|
// run these to see confirm the sytem behaves as expected
|
||
|
func RunTest() {
|
||
|
blah([]string{"ping", "-c", "3", "localhost"})
|
||
|
blah([]string{"exit", "0"})
|
||
|
blah([]string{"exit", "-1"})
|
||
|
blah([]string{"true"})
|
||
|
blah([]string{"false"})
|
||
|
blah([]string{"grep", "root", "/etc/", "/proc/cmdline", "/usr/bin/chmod"})
|
||
|
blah([]string{"grep", "root", "/proc/cmdline"})
|
||
|
fmt.Sprint("blahdone")
|
||
|
}
|
||
|
|
||
|
// this is stuff from a long time ago that there must be a replacement for
|
||
|
func RemoveFirstElement(slice []string) (string, []string) {
|
||
|
if len(slice) == 0 {
|
||
|
return "", slice // Return the original slice if it's empty
|
||
|
}
|
||
|
return slice[0], slice[1:] // Return the slice without the first element
|
||
|
}
|