route/vendor/github.com/otm/gluash/shellcommand.go

475 lines
8.8 KiB
Go
Raw Normal View History

package gluash
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"sync"
"syscall"
"github.com/yuin/gopher-lua"
)
const luaShTypeName = "sh"
type shellCommand struct {
path string
args []string
command *exec.Cmd
stdout io.ReadCloser
stderr io.ReadCloser
stdin io.ReadCloser
waitCalled bool
stdoutClosed bool
stderrClosed bool
}
func newShellCommand(path string, args ...string) (*shellCommand, error) {
cmd := &shellCommand{
path: path,
}
err := cmd.Command(path, args...)
if err != nil {
return nil, err
}
return cmd, nil
}
func (s *shellCommand) Command(path string, args ...string) error {
s.command = exec.Command(path, args...)
stdout, err := s.command.StdoutPipe()
if err != nil {
return err
}
stderr, err := s.command.StderrPipe()
if err != nil {
return err
}
s.stdout = stdout
s.stderr = stderr
return nil
}
func (s *shellCommand) Start(wait bool) error {
if !wait {
return s.command.Start()
}
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
io.Copy(stdout, s.stdout)
wg.Done()
}()
go func() {
io.Copy(stderr, s.stderr)
wg.Done()
}()
err := s.command.Start()
if err != nil {
return err
}
if debug {
fmt.Println("Waiting for process")
}
if err = s.command.Wait(); err != nil && !isExitError(err) {
return err
}
wg.Wait()
if abort {
status, ok := s.command.ProcessState.Sys().(syscall.WaitStatus)
if ok && status.ExitStatus() != 0 {
return fmt.Errorf(
"exit -- status %v\nSTDOUT:\n%v\nSTDERR:\n%v}",
status.ExitStatus(),
stdout,
stderr,
)
}
}
s.stdout = ioutil.NopCloser(stdout)
s.stderr = ioutil.NopCloser(stderr)
return nil
}
func (s *shellCommand) UserData(L *lua.LState) *lua.LUserData {
ud := L.NewUserData()
ud.Value = s
L.SetMetatable(ud, L.GetTypeMetatable(luaShTypeName))
return ud
}
func (s *shellCommand) CloseStdout() {
//s.stdout.Close()
s.stdoutClosed = true
}
func (s *shellCommand) CloseStderr() {
//s.stderr.Close()
s.stderrClosed = true
}
func (s *shellCommand) Close(std string) {
switch std {
case "stderr":
s.CloseStderr()
case "stdout":
s.CloseStdout()
default:
log.Fatalf("unable to close stream: %v", std)
}
}
func (s *shellCommand) IsClosed(std string) bool {
switch std {
case "stderr":
return s.stderrClosed
case "stdout":
return s.stdoutClosed
default:
log.Fatalf("Unknown option: %v", std)
}
return false
}
// shIndex checks if it is a predefined method or if it should be interprited as
// shell command.
func shIndex(L *lua.LState) int {
index := L.CheckString(2)
switch index {
case "cmd":
L.Push(L.NewFunction(shPathCmd(L)))
return 1
case "print":
L.Push(L.NewFunction(shPrint))
return 1
case "ok":
L.Push(L.NewFunction(shOk))
return 1
case "lines":
L.Push(L.NewFunction(shLines))
return 1
case "success":
L.Push(L.NewFunction(shSuccess))
return 1
case "exitcode":
L.Push(L.NewFunction(shExitCode))
return 1
case "stdout", "stderr", "combinedOutput":
L.Push(L.NewFunction(shOutput(index)))
return 1
default:
return shCmd(L)
}
}
func shCmd(L *lua.LState) int {
shellCmd := checkShellCmd(L)
index := L.CheckString(2)
cmd := &shellCommand{
path: index,
stdin: shellCmd.stdout,
}
L.Push(cmd.UserData(L))
return 1
}
func shPathCmd(L *lua.LState) lua.LGFunction {
shellCmd := checkShellCmd(L)
return func(L *lua.LState) int {
path := L.CheckString(2)
args := checkStrings(L, 3)
cmd := &shellCommand{
path: path,
}
err := cmd.Command(path, args...)
checkError(L, err)
cmd.command.Stdin = shellCmd.stdout
err = cmd.Start(true)
checkError(L, err)
L.Push(cmd.UserData(L))
return 1
}
}
// shCall executes the shell command and returns it self
func shCall(L *lua.LState) int {
ud := L.CheckUserData(1)
args := checkStrings(L, 2)
shellCmd := checkShellCmd(L)
err := shellCmd.Command(shellCmd.path, args...)
checkError(L, err)
if shellCmd.stdin != nil {
shellCmd.command.Stdin = shellCmd.stdin
}
err = shellCmd.Start(shouldWait)
checkError(L, err)
L.Push(ud)
return 1
}
func shOutput(std string) lua.LGFunction {
return func(L *lua.LState) int {
shellCmd := checkShellCmd(L)
file := L.OptString(2, "")
if shellCmd.waitCalled {
L.RaiseError("Do not call `ok` or `success` before print")
}
stream := io.MultiReader(shellCmd.stdout, shellCmd.stderr)
if std == "stderr" {
stream = shellCmd.stderr
}
if std == "stdout" {
stream = shellCmd.stdout
}
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(stream)
if err != nil {
L.RaiseError("Unable to read from `%v` several times", std)
}
if std == "stderr" || std == "stdout" {
shellCmd.Close(std)
} else {
shellCmd.CloseStderr()
shellCmd.CloseStdout()
}
if file != "" {
err := ioutil.WriteFile(file, buf.Bytes(), 0644)
checkError(L, err)
}
out := buf.String()
L.Push(lua.LString(out))
return 1
}
}
func shOk(L *lua.LState) int {
ud := L.CheckUserData(1)
shellCmd := checkShellCmd(L)
exitcode, err := wait(L, shellCmd)
checkError(L, err)
if exitcode != 0 {
buf := new(bytes.Buffer)
buf.ReadFrom(shellCmd.stdout)
so := buf.String()
buf.Reset()
buf.ReadFrom(shellCmd.stderr)
se := buf.String()
L.RaiseError(
"exit status %v\nSTDOUT:\n%v\nSTDERR:\n%v",
exitcode,
so,
se,
)
//L.RaiseError("exit status %v", exitcode)
}
L.Push(ud)
return 1
}
func shSuccess(L *lua.LState) int {
shellCmd := checkShellCmd(L)
errorCode, err := wait(L, shellCmd)
checkError(L, err)
L.Push(lua.LBool(errorCode == 0))
return 1
}
func shExitCode(L *lua.LState) int {
shellCmd := checkShellCmd(L)
exitcode, err := wait(L, shellCmd)
checkError(L, err)
L.Push(lua.LNumber(exitcode))
return 1
}
func shLines(L *lua.LState) int {
shellCmd := checkShellCmd(L)
std := L.OptString(2, "stdout")
if !(std == "stdout" || std == "stderr") {
L.RaiseError("lines: illigal file handle `%v`", std)
}
if shellCmd.IsClosed(std) {
L.RaiseError("Unable to read from `%v` several times", std)
}
stream := shellCmd.stdout
if std == "stderr" {
stream = shellCmd.stderr
}
scanner := bufio.NewScanner(stream)
scanner.Split(bufio.ScanLines)
iterator := func(L *lua.LState) int {
if scanner.Scan() {
L.Push(lua.LString(scanner.Text()))
return 1
}
shellCmd.Close(std)
checkError(L, scanner.Err())
return 0
}
L.Push(L.NewFunction(iterator))
return 1
}
func shPrint(L *lua.LState) int {
ud := L.CheckUserData(1)
shellCmd := checkShellCmd(L)
if shellCmd.stderrClosed || shellCmd.stdoutClosed {
L.RaiseError("Unable to read from `%v` several times", "stdout/stderr")
}
if shellCmd.waitCalled {
L.RaiseError("Do not call `ok` or `success` before print")
}
combo := io.MultiReader(shellCmd.stdout, shellCmd.stderr)
_, err := io.Copy(os.Stdout, combo)
//fmt.Println("A")
checkError(L, err)
//fmt.Println("B")
_, err = wait(L, shellCmd)
if err != nil && !isExitError(err) {
L.RaiseError("Error while waiting for command to finish: %v", err)
}
shellCmd.CloseStderr()
shellCmd.CloseStdout()
L.Push(ud)
return 1
}
// check if it is a shellCmd userdata as the first parmeter
func checkShellCmd(L *lua.LState) *shellCommand {
ud := L.CheckUserData(1)
shellCmd, ok := ud.Value.(*shellCommand)
if !ok {
L.Error(lua.LString("Expected the user data should be a shell command"), 0)
return nil
}
return shellCmd
}
// converts all input parameters to strings from the n:th element
func checkStrings(L *lua.LState, n int) []string {
params := L.GetTop()
if n > params {
return []string{}
}
args := make([]string, 0, (params - n + 1))
for i := n; i <= params; i++ {
p := L.Get(i)
if p.Type() == lua.LTUserData {
continue
}
L.CheckTypes(i, lua.LTString, lua.LTNumber)
args = append(args, p.String())
}
return args
}
func checkError(L *lua.LState, err error) {
if err != nil {
L.RaiseError("%v", err)
}
}
func isExitError(err error) bool {
_, ok := err.(*exec.ExitError)
return ok
}
func wait(L *lua.LState, shellCmd *shellCommand) (exitcode int, err error) {
defer func() {
if abort && (err != nil || exitcode != 0) {
L.RaiseError("exit status %v, err: %v", exitcode, err)
}
}()
if shellCmd.command.ProcessState == nil {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
io.Copy(stdout, shellCmd.stdout)
wg.Done()
}()
go func() {
io.Copy(stderr, shellCmd.stderr)
wg.Done()
}()
err := shellCmd.command.Wait()
shellCmd.stdout = ioutil.NopCloser(stdout)
shellCmd.stderr = ioutil.NopCloser(stderr)
if err != nil && !isExitError(err) {
return 0, err
}
}
if shellCmd.command.ProcessState.Success() {
return 0, nil
}
if status, ok := shellCmd.command.ProcessState.Sys().(syscall.WaitStatus); ok {
return status.ExitStatus(), nil
}
err = fmt.Errorf("`%v`: error retreiving exit code", shellCmd.command.Args)
return 0, err
}