Documentation
¶
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DebugMode = false
Functions ¶
This section is empty.
Types ¶
type Commander ¶ added in v0.1.1
type Commander interface {
// Stringer provides a command via String(), e.g. the command "echo".
fmt.Stringer
// Writer accepts CLI output for parsing.
//
// Output from the CLI subprocess' stdout and stderr should be sent into
// Write. Commander can accumulate whatever good data and errors it desires.
//
// A commander should return an error from Write only on some sort of
// catastrophe, as the error will result in shutting down the CLI subprocess.
// An implementation may choose to allow certain unexpected CLI output and
// not return an error, instead merely counting and/or logging the error.
io.Writer
// Success returns true if the Commander decided that it succeeded
// in parsing output from the CLI. Doesn't necessarily imply that
// the command ran without error. This value can be consulted by whatever
// program is coordinating the ProcRunner and Commander instances.
Success() bool
// Reset resets the internal state of the parser (e.g. error counts)
// and sets Success to false. This allows the Commander instance to be
// used in another Run.
Reset()
}
Commander knows a CLI command, and knows how to parse the command's output.
type Parameters ¶
type Parameters struct {
// WorkingDir is the working directory of the CLI process.
WorkingDir string
// Path is the absolute or WorkingDir-relative path to the CLI's executable.
Path string
// Args has the arguments, flags and flag arguments for the CLI invocation.
Args []string
// ErrPrefix is added to the lines coming out of stdErr before combining
// them with lines from stdOut. Can be empty. This is just a way
// to help a Commander implementation more easily distinguish stdErr
// from stdOut.
// Example: "Err: "
ErrPrefix string
// ExitCommand is the command to send to gracefully exit the CLI.
// If empty it won't be sent. Regardless, the final thing sent to the
// CLI subprocess will be an EOF on its stdIn.
// Example: "quit"
ExitCommand string
// OutSentinel holds the command sent to the CLI after every command other
// than the ExitCommand. The OutSentinel knows how to scan output for a
// particular sentinel value.
//
// If the command string is empty, it's presumed that the ProcRunner will rely
// on the CLI to send a unique prompt to stdout, and the commander
// will parse output looking for that prompt.
//
// Even when prompts are available, if they are short and not unambiguously
// distinguishable from all possible command output, it's best to identify a
// sentinel command to run instead.
//
// Example: "echo pink elephants dance;"
// Look for: "pink elephants dance"
//
// Example: "version;"
// Look for: "v1.2.3"
//
// The sentinel can be custom, but it's simplest to use an instance
// of SimpleSentinelCommander, which can accommodate prompt detection.
OutSentinel Commander
// ErrSentinel is a command that intentionally triggers output on stderr,
// e.g. a misspelled command, a command with a non-existent flag - something
// that doesn't cause any real trouble. In non nil, this is issued after
// issuing command N, either before or after issuing the OutSentinel command.
// ErrSentinel is used to be sure that any errors generated in the course of
// running command N are swept up and accounted for before looking for errors
// from command N+1.
ErrSentinel Commander
// CommandTerminator, if not 0, is appended to the end of every command.
// This is merely a convenience for CLI's like mysql that want such things.
//
// Example: ';'
CommandTerminator byte
}
Parameters is a bag of parameters for ProcRunner.
func (*Parameters) Validate ¶
func (p *Parameters) Validate() error
Validate looks for trouble and sets defaults.
type ProcRunner ¶
type ProcRunner struct {
// contains filtered or unexported fields
}
ProcRunner manages an interactive command line interpreter (CLI) subprocess. See nearby example and tests for usage.
ProcRunner separates the problem of running a CLI from the problem of parsing the CLI's response to a particular command. The ProcRunner handles the former, and implementations of Commander handle the latter. ProcRunner knows nothing about the commands in a given CLI. Its job is to start the CLI, accept instances of Commander, watch stdOut and stdErr, and run a series of Commander instances.
So, one ProcRunner instance can be used to run any CLI (e.g. mysql, kubectl, mql, etc.), but the specific knowledge of a specific command and how to parse the output from that command must be expressed in an implementation of the Commander interface.
For the ProcRunner to know when a Commander completes, it looks for a particular string called the "sentinel value" in the CLI's output stream (and optionally its error stream). The sentinel value could simply be the value of the CLI prompt string. Or it could be the fixed, characteristic output of a particular "sentinel command", like "echo" or "version". The sentinel value is analogous to the code word "Over" in a radio transmission.
CLI's sometimes won't prompt to stdErr or stdOut if they detect that they are attached to a pipe on stdIn, so commands with a characteristic output are the only option for generating a sentinel. Also, some prompts might not be unambiguously distinguishable in several thousand lines of data, so it's best to use a sentinel command rather than rely on a prompt to signal command completion.
If the ProcRunner is prepared with a sentinel command, it will automatically issue the command inside the call to RunIt, immediately after issuing the command given to RunIt. During the call to RunIt, the runner will scan the CLI's output for the sentinel value, before sending output to the Commander for processing. When the sentinel value is found, the call to RunIt returns without error. If the sentinel is not found before the deadline, RunIt returns an error.
Example (BasicRun) ¶
runner, _ := NewProcRunner(&Parameters{
Path: cli2.TestCliPath,
Args: []string{
"--" + cli2.FlagDisablePrompt,
},
ExitCommand: cli2.CmdQuit,
OutSentinel: cli2.MakeOutSentinelCommander(),
})
commander := cmdrs.NewPrintingCommander("query limit 3", os.Stdout)
assertNoErr(runner.RunIt(commander, testingTimeout))
Output: Cempedak_|_Bamberga_|_4_|_00000000000000000000000000000001 Buddha's hand_|_Hermione_|_6_|_00000000000000000000000000000002 African cucumber_|_Ursula_|_6_|_00000000000000000000000000000003
Example (SubprocessError) ¶
runner, _ := NewProcRunner(&Parameters{
Path: cli2.TestCliPath,
Args: []string{
"--" + cli2.FlagDisablePrompt,
"--" + cli2.FlagRowToErrorOn, "4",
},
ExitCommand: cli2.CmdQuit,
OutSentinel: cli2.MakeOutSentinelCommander(),
})
commander := cmdrs.NewPrintingCommander("query limit 3", os.Stdout)
// Yields three lines.
assertNoErr(runner.RunIt(commander, testingTimeout))
// Query again, but ask for a row beyond the row that triggers a DB error.
// Because of the nature of output streams, there's no way to know
// when the error will show up in the combined output. It might come
// out first, last, or anywhere in the middle relative to lines from stdOut,
// so this test must not be fragile to the order.
commander.Reset()
commander.Command = "query limit 7"
// This will yield three "good lines", and one error line.
assertNoErr(runner.RunIt(commander, testingTimeout))
commander.Reset()
commander.Command = "query limit 2"
// Yields two lines.
assertNoErr(runner.RunIt(commander, testingTimeout))
// There should be nine (3 + 3 + 1 + 2) lines in the output.
Output: Cempedak_|_Bamberga_|_4_|_00000000000000000000000000000001 Buddha's hand_|_Hermione_|_6_|_00000000000000000000000000000002 African cucumber_|_Ursula_|_6_|_00000000000000000000000000000003 error! touching row 4 triggers this error Currant_|_Alauda_|_5_|_00000000000000000000000000000001 Banana_|_Egeria_|_5_|_00000000000000000000000000000002 Bilberry_|_Interamnia_|_2_|_00000000000000000000000000000003 Cherimoya_|_Palma_|_6_|_00000000000000000000000000000001 Abiu_|_Metis_|_3_|_00000000000000000000000000000002
func NewProcRunner ¶
func NewProcRunner(params *Parameters) (*ProcRunner, error)
NewProcRunner returns a new ProcRunner, or an error on bad parameters.
func (*ProcRunner) Close ¶
func (pr *ProcRunner) Close() (err error)
Close gracefully terminates the CLI, and shuts down all streams, reporting any errors that happen.
Close sends the CLI's ExitCommand (if not empty) and EOF, and returns the process' exit code in string form. If the exit code was 0, nil is returned.
TODO: kill a hung process, make it possible to transition from stateError to stateUninitialized.
func (*ProcRunner) RunIgnoringOutput ¶
func (pr *ProcRunner) RunIgnoringOutput(c string) error
RunIgnoringOutput runs the given command ignoring its output. A default timeout is used.
func (*ProcRunner) RunIt ¶
func (pr *ProcRunner) RunIt(cmdr Commander, timeOut time.Duration) error
RunIt runs the given Commander in the given duration.
RunIt blocks until the command completes, or the duration passes. After a call to RunIt returns, with or without an error, the Commander may be consulted for data it accumulated. If RunIt returned an error, the Commander might not have complete results.
RunIt returns an error from either the Commander or from ProcRunner's own internal infrastructure, e.g. a timeout. The Commander should _not_ return an error on some minor parsing trouble - instead it should note the error internally for later reporting to whatever owns it, and return no error to the ProcRunner. A Commander should only return an error to the ProcRunner in the rare case that it (the Commander) determines that the subprocess should no longer be used by itself or any other Commander.
If RunIt returns an error, then the ProcRunner should be abandoned. There's no general way to interrupt and "fix" a subprocess.