You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
8.0 KiB
Go
240 lines
8.0 KiB
Go
11 months ago
|
package ncmd
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// os/exec.Cmd.StdoutPipe is usually used incorrectly. The docs are clear:
|
||
|
// "it is incorrect to call Wait before all reads from the pipe have completed."
|
||
|
// Therefore, we can't read from the pipe in another goroutine because it
|
||
|
// causes a race condition: we'll read in one goroutine and the original
|
||
|
// goroutine that calls Wait will write on close which is what Wait does.
|
||
|
// The proper solution is using an io.Writer for cmd.Stdout. I couldn't find
|
||
|
// an io.Writer that's also safe for concurrent reads (as lines in a []string
|
||
|
// no less), so I created one:
|
||
|
|
||
|
// OutputBuffer represents command output that is saved, line by line, in an
|
||
|
// unbounded buffer. It is safe for multiple goroutines to read while the command
|
||
|
// is running and after it has finished. If output is small (a few megabytes)
|
||
|
// and not read frequently, an output buffer is a good solution.
|
||
|
//
|
||
|
// A Cmd in this package uses an OutputBuffer for both STDOUT and STDERR by
|
||
|
// default when created by calling NewCmd. To use OutputBuffer directly with
|
||
|
// a Go standard library os/exec.Command:
|
||
|
//
|
||
|
// import "os/exec"
|
||
|
// import "github.com/go-cmd/cmd"
|
||
|
// runnableCmd := exec.Command(...)
|
||
|
// stdout := cmd.NewOutputBuffer()
|
||
|
// runnableCmd.Stdout = stdout
|
||
|
//
|
||
|
// While runnableCmd is running, call stdout.Lines() to read all output
|
||
|
// currently written.
|
||
|
type OutputBuffer struct {
|
||
|
buf *bytes.Buffer
|
||
|
lines []string
|
||
|
*sync.Mutex
|
||
|
}
|
||
|
|
||
|
// NewOutputBuffer creates a new output buffer. The buffer is unbounded and safe
|
||
|
// for multiple goroutines to read while the command is running by calling Lines.
|
||
|
func NewOutputBuffer() *OutputBuffer {
|
||
|
out := &OutputBuffer{
|
||
|
buf: &bytes.Buffer{},
|
||
|
lines: []string{},
|
||
|
Mutex: &sync.Mutex{},
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// Write makes OutputBuffer implement the io.Writer interface. Do not call
|
||
|
// this function directly.
|
||
|
func (rw *OutputBuffer) Write(p []byte) (n int, err error) {
|
||
|
rw.Lock()
|
||
|
n, err = rw.buf.Write(p) // and bytes.Buffer implements io.Writer
|
||
|
rw.Unlock()
|
||
|
return // implicit
|
||
|
}
|
||
|
|
||
|
// Lines returns lines of output written by the Cmd. It is safe to call while
|
||
|
// the Cmd is running and after it has finished. Subsequent calls returns more
|
||
|
// lines, if more lines were written. "\r\n" are stripped from the lines.
|
||
|
func (rw *OutputBuffer) Lines() []string {
|
||
|
rw.Lock()
|
||
|
// Scanners are io.Readers which effectively destroy the buffer by reading
|
||
|
// to EOF. So once we scan the buf to lines, the buf is empty again.
|
||
|
s := bufio.NewScanner(rw.buf)
|
||
|
for s.Scan() {
|
||
|
rw.lines = append(rw.lines, decodeBytes(s.Bytes()))
|
||
|
}
|
||
|
rw.Unlock()
|
||
|
return rw.lines
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
// DEFAULT_LINE_BUFFER_SIZE is the default size of the OutputStream line buffer.
|
||
|
// The default value is usually sufficient, but if ErrLineBufferOverflow errors
|
||
|
// occur, try increasing the size by calling OutputBuffer.SetLineBufferSize.
|
||
|
DEFAULT_LINE_BUFFER_SIZE = 16384
|
||
|
|
||
|
// DEFAULT_STREAM_CHAN_SIZE is the default string channel size for a Cmd when
|
||
|
// Options.Streaming is true. The string channel size can have a minor
|
||
|
// performance impact if too small by causing OutputStream.Write to block
|
||
|
// excessively.
|
||
|
DEFAULT_STREAM_CHAN_SIZE = 1000
|
||
|
)
|
||
|
|
||
|
// ErrLineBufferOverflow is returned by OutputStream.Write when the internal
|
||
|
// line buffer is filled before a newline character is written to terminate a
|
||
|
// line. Increasing the line buffer size by calling OutputStream.SetLineBufferSize
|
||
|
// can help prevent this error.
|
||
|
type ErrLineBufferOverflow struct {
|
||
|
Line string // Unterminated line that caused the error
|
||
|
BufferSize int // Internal line buffer size
|
||
|
BufferFree int // Free bytes in line buffer
|
||
|
}
|
||
|
|
||
|
func (e ErrLineBufferOverflow) Error() string {
|
||
|
return fmt.Sprintf("line does not contain newline and is %d bytes too long to buffer (buffer size: %d)",
|
||
|
len(e.Line)-e.BufferSize, e.BufferSize)
|
||
|
}
|
||
|
|
||
|
// OutputStream represents real time, line by line output from a running Cmd.
|
||
|
// Lines are terminated by a single newline preceded by an optional carriage
|
||
|
// return. Both newline and carriage return are stripped from the line when
|
||
|
// sent to a caller-provided channel.
|
||
|
//
|
||
|
// The caller must begin receiving before starting the Cmd. Write blocks on the
|
||
|
// channel; the caller must always read the channel. The channel is closed when
|
||
|
// the Cmd exits and all output has been sent.
|
||
|
//
|
||
|
// A Cmd in this package uses an OutputStream for both STDOUT and STDERR when
|
||
|
// created by calling NewCmdOptions and Options.Streaming is true. To use
|
||
|
// OutputStream directly with a Go standard library os/exec.Command:
|
||
|
//
|
||
|
// import "os/exec"
|
||
|
// import "github.com/go-cmd/cmd"
|
||
|
//
|
||
|
// stdoutChan := make(chan string, 100)
|
||
|
// go func() {
|
||
|
// for line := range stdoutChan {
|
||
|
// // Do something with the line
|
||
|
// }
|
||
|
// }()
|
||
|
//
|
||
|
// runnableCmd := exec.Command(...)
|
||
|
// stdout := cmd.NewOutputStream(stdoutChan)
|
||
|
// runnableCmd.Stdout = stdout
|
||
|
//
|
||
|
// While runnableCmd is running, lines are sent to the channel as soon as they
|
||
|
// are written and newline-terminated by the command.
|
||
|
type OutputStream struct {
|
||
|
streamChan chan string
|
||
|
bufSize int
|
||
|
buf []byte
|
||
|
lastChar int
|
||
|
}
|
||
|
|
||
|
// NewOutputStream creates a new streaming output on the given channel. The
|
||
|
// caller must begin receiving on the channel before the command is started.
|
||
|
// The OutputStream never closes the channel.
|
||
|
func NewOutputStream(streamChan chan string) *OutputStream {
|
||
|
out := &OutputStream{
|
||
|
streamChan: streamChan,
|
||
|
// --
|
||
|
bufSize: DEFAULT_LINE_BUFFER_SIZE,
|
||
|
buf: make([]byte, DEFAULT_LINE_BUFFER_SIZE),
|
||
|
lastChar: 0,
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// Write makes OutputStream implement the io.Writer interface. Do not call
|
||
|
// this function directly.
|
||
|
func (rw *OutputStream) Write(p []byte) (n int, err error) {
|
||
|
n = len(p) // end of buffer
|
||
|
firstChar := 0
|
||
|
|
||
|
for {
|
||
|
// Find next newline in stream buffer. nextLine starts at 0, but buff
|
||
|
// can contain multiple lines, like "foo\nbar". So in that case nextLine
|
||
|
// will be 0 ("foo\nbar\n") then 4 ("bar\n") on next iteration. And i
|
||
|
// will be 3 and 7, respectively. So lines are [0:3] are [4:7].
|
||
|
newlineOffset := bytes.IndexByte(p[firstChar:], '\n')
|
||
|
if newlineOffset < 0 {
|
||
|
break // no newline in stream, next line incomplete
|
||
|
}
|
||
|
|
||
|
// End of line offset is start (nextLine) + newline offset. Like bufio.Scanner,
|
||
|
// we allow \r\n but strip the \r too by decrementing the offset for that byte.
|
||
|
lastChar := firstChar + newlineOffset // "line\n"
|
||
|
if newlineOffset > 0 && p[newlineOffset-1] == '\r' {
|
||
|
lastChar -= 1 // "line\r\n"
|
||
|
}
|
||
|
|
||
|
// Send the line, prepend line buffer if set
|
||
|
var line string
|
||
|
if rw.lastChar > 0 {
|
||
|
line = decodeBytes(rw.buf[0:rw.lastChar])
|
||
|
//line = string(rw.buf[0:rw.lastChar])
|
||
|
rw.lastChar = 0 // reset buffer
|
||
|
}
|
||
|
line += decodeBytes(p[firstChar:lastChar])
|
||
|
//line += string(p[firstChar:lastChar])
|
||
|
rw.streamChan <- line // blocks if chan full
|
||
|
|
||
|
// Next line offset is the first byte (+1) after the newline (i)
|
||
|
firstChar += newlineOffset + 1
|
||
|
}
|
||
|
|
||
|
if firstChar < n {
|
||
|
remain := len(p[firstChar:])
|
||
|
bufFree := len(rw.buf[rw.lastChar:])
|
||
|
if remain > bufFree {
|
||
|
var line string
|
||
|
if rw.lastChar > 0 {
|
||
|
line = string(rw.buf[0:rw.lastChar])
|
||
|
}
|
||
|
line += string(p[firstChar:])
|
||
|
err = ErrLineBufferOverflow{
|
||
|
Line: line,
|
||
|
BufferSize: rw.bufSize,
|
||
|
BufferFree: bufFree,
|
||
|
}
|
||
|
n = firstChar
|
||
|
return // implicit
|
||
|
}
|
||
|
copy(rw.buf[rw.lastChar:], p[firstChar:])
|
||
|
rw.lastChar += remain
|
||
|
}
|
||
|
|
||
|
return // implicit
|
||
|
}
|
||
|
|
||
|
// Lines returns the channel to which lines are sent. This is the same channel
|
||
|
// passed to NewOutputStream.
|
||
|
func (rw *OutputStream) Lines() <-chan string {
|
||
|
return rw.streamChan
|
||
|
}
|
||
|
|
||
|
// SetLineBufferSize sets the internal line buffer size. The default is DEFAULT_LINE_BUFFER_SIZE.
|
||
|
// This function must be called immediately after NewOutputStream, and it is not
|
||
|
// safe to call by multiple goroutines.
|
||
|
//
|
||
|
// Increasing the line buffer size can help reduce ErrLineBufferOverflow errors.
|
||
|
func (rw *OutputStream) SetLineBufferSize(n int) {
|
||
|
rw.bufSize = n
|
||
|
rw.buf = make([]byte, rw.bufSize)
|
||
|
}
|
||
|
|
||
|
// Flush empties the buffer of its last line.
|
||
|
func (rw *OutputStream) Flush() {
|
||
|
if rw.lastChar > 0 {
|
||
|
line := string(rw.buf[0:rw.lastChar])
|
||
|
rw.streamChan <- line
|
||
|
}
|
||
|
}
|