parent
b01002828e
commit
b3872ce052
@ -0,0 +1 @@
|
|||||||
|
tests
|
@ -1,4 +1,4 @@
|
|||||||
package cmdn
|
package ncmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
@ -0,0 +1,7 @@
|
|||||||
|
package ncmd
|
||||||
|
|
||||||
|
import "git.noahlan.cn/noahlan/ntool/nstr"
|
||||||
|
|
||||||
|
func decodeBytes(bs []byte) string {
|
||||||
|
return nstr.ToGBKStrSafe(bs)
|
||||||
|
}
|
@ -0,0 +1,239 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package ncmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.noahlan.cn/noahlan/ntool/ndef"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithOptions(o *Options) Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMarshaler(marshaler ndef.Marshaler) Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt.Marshaler = marshaler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBuffered(v bool) Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt.Buffered = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCombinedOutput() Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt.CombinedOutput = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStreaming() Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt.Streaming = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLineBufferSize(size uint) Option {
|
||||||
|
return func(opt *Options) {
|
||||||
|
opt.LineBufferSize = size
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
package cmdn
|
|
||||||
|
|
||||||
type ICommand interface {
|
|
||||||
// MessageID 消息ID
|
|
||||||
MessageID() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlainCommand 基于明文传输数据的命令
|
|
||||||
type PlainCommand struct {
|
|
||||||
MID string // 消息ID
|
|
||||||
ID string
|
|
||||||
Cmd string
|
|
||||||
Args []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PlainCommand) MessageID() string {
|
|
||||||
return c.MID
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package cmdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WithSerializer(serializer ndef.Serializer) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.Marshaler = &ndef.MarshalerWrapper{Marshaler: serializer}
|
|
||||||
opt.Unmarshaler = &ndef.UnmarshalerWrapper{Unmarshaler: serializer}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithMarshaler(marshaler ndef.Marshaler) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.Marshaler = marshaler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithUnmarshaler(unmarshaler ndef.Unmarshaler) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.Unmarshaler = unmarshaler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithStartupDecidedFunc(startupDecidedFunc LineFunc) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.StartupDecidedFunc = startupDecidedFunc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithEndLineDecidedFunc(endLineDecidedFunc LineFunc) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.EndLineDecidedFunc = endLineDecidedFunc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithReadIDFunc(readIDFunc ReadIDFunc) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.ReadIDFunc = readIDFunc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTimeout(timeout time.Duration) Option {
|
|
||||||
return func(opt *Options) {
|
|
||||||
opt.Timeout = timeout
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package cmdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IResponse interface {
|
|
||||||
// MessageID 消息ID
|
|
||||||
MessageID() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlainResp struct {
|
|
||||||
ID string
|
|
||||||
Command string
|
|
||||||
Result string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlainResp) MessageID() string {
|
|
||||||
return r.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PlainResp) GetResult() (string, error) {
|
|
||||||
reg, _ := regexp.Compile(`\t`)
|
|
||||||
result := strings.TrimSpace(reg.ReplaceAllString(r.Result, " "))
|
|
||||||
|
|
||||||
res := strings.Fields(result)
|
|
||||||
l := len(res)
|
|
||||||
if l > 0 {
|
|
||||||
if res[0] == "=" {
|
|
||||||
return strings.TrimSpace(strings.Join(res[1:], " ")), nil
|
|
||||||
} else if res[0] == "?" {
|
|
||||||
return "", errors.New(strings.Join(res[1:], " "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New(fmt.Sprintf("错误(未知应答): %s", r.Err))
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package cmdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"git.noahlan.cn/noahlan/ntool/ndef"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PlainSerializer struct {
|
|
||||||
sysType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlainSerializer() ndef.Serializer {
|
|
||||||
return &PlainSerializer{
|
|
||||||
sysType: runtime.GOOS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlainSerializer) Marshal(v any) ([]byte, error) {
|
|
||||||
ret, ok := v.(*PlainCommand)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New(fmt.Sprintf("参数类型必须为 %T", PlainCommand{}))
|
|
||||||
}
|
|
||||||
// ret arg0 arg1 arg2 ...
|
|
||||||
// cmd arg0 arg1 arg2 ...
|
|
||||||
sb := strings.Builder{}
|
|
||||||
if ret.ID != "" {
|
|
||||||
sb.WriteString(ret.ID)
|
|
||||||
sb.WriteString(" ")
|
|
||||||
}
|
|
||||||
sb.WriteString(ret.Cmd)
|
|
||||||
sb.WriteString(" ")
|
|
||||||
sb.WriteString(strings.Join(ret.Args, " "))
|
|
||||||
return []byte(sb.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlainSerializer) Unmarshal(data []byte, v any) error {
|
|
||||||
t, ok := v.(*PlainResp)
|
|
||||||
if !ok {
|
|
||||||
return errors.New(fmt.Sprintf("参数类型必须为 %T", PlainResp{}))
|
|
||||||
}
|
|
||||||
t.ID = ""
|
|
||||||
//t.Command
|
|
||||||
t.Result = string(data)
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue