From abbf89a5589f2c92f786bb45c5cd613a318a9e24 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 23 Feb 2018 17:40:32 +0100 Subject: Add PtsMode support to eows lib Signed-off-by: Sebastien Douheret --- golib/eows/eows-in.go | 3 + golib/eows/eows-out.go | 26 +++++--- golib/eows/eows.go | 169 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 150 insertions(+), 48 deletions(-) diff --git a/golib/eows/eows-in.go b/golib/eows/eows-in.go index 1ecd2a1..89ca891 100644 --- a/golib/eows/eows-in.go +++ b/golib/eows/eows-in.go @@ -7,6 +7,7 @@ import ( "time" ) +// DoneChan Channel used to propagate status+error on command exit type DoneChan struct { status int err error @@ -43,6 +44,8 @@ func (e *ExecOverWS) cmdPumpStdin(inw *os.File) { s := sts.Sys().(syscall.WaitStatus) status = s.ExitStatus() } + e.procExited = true + done <- DoneChan{status, err} }() diff --git a/golib/eows/eows-out.go b/golib/eows/eows-out.go index b70e70c..2d9bdb0 100644 --- a/golib/eows/eows-out.go +++ b/golib/eows/eows-out.go @@ -16,7 +16,7 @@ func scanChars(data []byte, atEOF bool) (advance int, token []byte, err error) { } // _pumper is in charge to collect -func (e *ExecOverWS) _pumper(sc *bufio.Scanner, fctOut func(s string)) { +func (e *ExecOverWS) _pumper(sc *bufio.Scanner, fctCB func(s string)) { // Select split function (default sc.ScanLines) if e.OutSplit == SplitChar || e.OutSplit == SplitLineTime || e.OutSplit == SplitTime { @@ -31,20 +31,26 @@ func (e *ExecOverWS) _pumper(sc *bufio.Scanner, fctOut func(s string)) { buf += sc.Text() if time.Since(t0).Nanoseconds() > e.LineTimeSpan || (e.OutSplit == SplitLineTime && strings.Contains(buf, "\n")) { - fctOut(buf) + fctCB(buf) buf = "" t0 = time.Now() } + if e.procExited { + break + } } // Send remaining characters if len(buf) > 0 { - e.OutputCB(e, "", buf) + fctCB(buf) } } else { for sc.Scan() { - e.OutputCB(e, sc.Text(), "") + fctCB(sc.Text()) + if e.procExited { + break + } } } @@ -58,10 +64,12 @@ func (e *ExecOverWS) cmdPumpStdout(r io.Reader, done chan struct{}) { sc := bufio.NewScanner(r) - e._pumper(sc, func(bufOut string) { - e.OutputCB(e, bufOut, "") + e._pumper(sc, func(b string) { + e.OutputCB(e, b, "") }) + e.logDebug("STDOUT pump exit") + if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { e.logError("stdout scan: %v", sc.Err()) } @@ -77,10 +85,12 @@ func (e *ExecOverWS) cmdPumpStderr(r io.Reader) { sc := bufio.NewScanner(r) - e._pumper(sc, func(bufErr string) { - e.OutputCB(e, "", bufErr) + e._pumper(sc, func(b string) { + e.OutputCB(e, "", b) }) + e.logDebug("STDERR pump exit") + if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { e.logError("stderr scan: %v", sc.Err()) } diff --git a/golib/eows/eows.go b/golib/eows/eows.go index 192be3d..283d673 100644 --- a/golib/eows/eows.go +++ b/golib/eows/eows.go @@ -4,11 +4,15 @@ package eows import ( "fmt" "os" + "os/exec" "strings" + "syscall" "time" + "unsafe" "github.com/Sirupsen/logrus" "github.com/googollee/go-socket.io" + "github.com/kr/pty" ) // OnInputCB is the function callback used to receive data @@ -56,9 +60,14 @@ type ExecOverWS struct { UserData *map[string]interface{} // user data passed to callbacks OutSplit SplitType // split method to tokenize stdout/stderr LineTimeSpan int64 // time span (only used with SplitTime or SplitLineTime) + PtsMode bool // Allocate a pseudo-terminal (allow to execute screen-based program) // Private fields - proc *os.Process + + proc *os.Process + command *exec.Cmd + ptmx *os.File + procExited bool } var cmdIDMap = make(map[string]*ExecOverWS) @@ -75,6 +84,7 @@ func New(cmd string, args []string, so *socketio.Socket, soID, cmdID string) *Ex CmdExecTimeout: -1, // default no timeout OutSplit: SplitLineTime, // default split by line with time LineTimeSpan: 500 * time.Millisecond.Nanoseconds(), + PtsMode: false, } cmdIDMap[cmdID] = e @@ -102,53 +112,82 @@ func (e *ExecOverWS) Start() error { e.CmdExecTimeout = 365 * 24 * 60 * 60 } - // Create pipes - outr, outw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stdout error: " + err.Error()) - goto exitErr - } + e.procExited = false - errr, errw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stderr error: " + err.Error()) - goto exitErr - } + if e.PtsMode { - inr, inw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stdin error: " + err.Error()) - goto exitErr - } + e.command = exec.Command(bashArgs[0], bashArgs[1:]...) + e.command.Env = append(os.Environ(), e.Env...) + e.ptmx, err = pty.Start(e.command) + if err != nil { + err = fmt.Errorf("Process start error: " + err.Error()) + goto exitErr + } + e.proc = e.command.Process - e.proc, err = os.StartProcess("/bin/bash", bashArgs, &os.ProcAttr{ - Files: []*os.File{inr, outw, errw}, - Env: append(os.Environ(), e.Env...), - }) - if err != nil { - err = fmt.Errorf("Process start error: " + err.Error()) - goto exitErr + // Turn off terminal echo + e.terminalEcho(e.ptmx, false) + + } else { + + // Create pipes + outr, outw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stdout error: " + err.Error()) + goto exitErr + } + + errr, errw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stderr error: " + err.Error()) + goto exitErr + } + + inr, inw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stdin error: " + err.Error()) + goto exitErr + } + + e.proc, err = os.StartProcess(bashArgs[0], bashArgs, &os.ProcAttr{ + Files: []*os.File{inr, outw, errw}, + Env: append(os.Environ(), e.Env...), + }) + if err != nil { + err = fmt.Errorf("Process start error: " + err.Error()) + goto exitErr + } } go func() { - defer outr.Close() - defer outw.Close() - defer errr.Close() - defer errw.Close() - defer inr.Close() - defer inw.Close() - stdoutDone := make(chan struct{}) - go e.cmdPumpStdout(outr, stdoutDone) - go e.cmdPumpStderr(errr) - // Blocking function that poll input or wait for end of process - e.cmdPumpStdin(inw) + if e.PtsMode { + // Make sure to close the pty at the end. + defer e.ptmx.Close() + + // Handle both stdout mixed with stderr + go e.cmdPumpStdout(e.ptmx, stdoutDone) + + // Blocking function that poll input or wait for end of process + e.cmdPumpStdin(e.ptmx) - // Some commands will exit when stdin is closed. - inw.Close() + } else { + // Make sure to close all pipes + defer outr.Close() + defer outw.Close() + defer errr.Close() + defer errw.Close() + defer inr.Close() + defer inw.Close() - defer outr.Close() + // Handle stdout + stderr + go e.cmdPumpStdout(outr, stdoutDone) + go e.cmdPumpStderr(errr) + + // Blocking function that poll input or wait for end of process + e.cmdPumpStdin(inw) + } if status, err := e.proc.Wait(); err == nil { // Other commands need a bonk on the head. @@ -181,14 +220,64 @@ exitErr: return err } +// TerminalSetSize Set terminal size +func (e *ExecOverWS) TerminalSetSize(rows, cols uint16) error { + if !e.PtsMode || e.ptmx == nil { + return fmt.Errorf("PtsMode not set") + } + w, err := pty.GetsizeFull(e.ptmx) + if err != nil { + return err + } + return e.TerminalSetSizePos(rows, cols, w.X, w.Y) +} + +// TerminalSetSizePos Set terminal size and position +func (e *ExecOverWS) TerminalSetSizePos(rows, cols, x, y uint16) error { + if !e.PtsMode || e.ptmx == nil { + return fmt.Errorf("PtsMode not set") + } + winSz := pty.Winsize{Rows: rows, Cols: cols, X: x, Y: y} + return pty.Setsize(e.ptmx, &winSz) +} + +/** + * Private functions + **/ + +// terminalEcho Enable or disable echoing terminal input. +// This is useful specifically for when users enter passwords. +func (e *ExecOverWS) terminalEcho(ff *os.File, show bool) { + var termios = &syscall.Termios{} + + fd := ff.Fd() + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, + syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 { + return + } + + if show { + termios.Lflag |= syscall.ECHO + } else { + termios.Lflag &^= syscall.ECHO + } + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, + uintptr(syscall.TCSETS), + uintptr(unsafe.Pointer(termios))); err != 0 { + return + } +} + func (e *ExecOverWS) logDebug(format string, a ...interface{}) { if e.Log != nil { - e.Log.Debugf(format, a) + e.Log.Debugf(format, a...) } } func (e *ExecOverWS) logError(format string, a ...interface{}) { if e.Log != nil { - e.Log.Errorf(format, a) + e.Log.Errorf(format, a...) } } -- cgit 1.2.3-korg