cmd

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 22, 2017 License: MIT Imports: 7 Imported by: 262

README

go-cmd/Cmd

Go Report Card Build Status GoDoc

This package is a small but very useful wrapper around os/exec.Cmd that makes it dead simple and safe to run commands in asynchronous, highly concurrent, real-time applications. Here's the look and feel:

import "github.com/go-cmd/cmd"

// Start a long-running process, capture stdout and stderr
c := cmd.NewCmd("find", "/", "--name" "needle")
statusChan := c.Start()

// Print last line of stdout every 2s
go func() {
  for range time.Ticker(2 * time.Second).C {
    status := c.Status()
    n := len(status.Stdout)
    fmt.Println(status.Stdout[n - 1])
  }
}()

// Stop command after 1 hour
go func() {
  <-time.After(1 * time.Hour)
  c.Stop()
}()

// Check if command is done
switch {
case finalStatus := <-statusChan:
  // yes!
default:
  // no, still running
}

// Block waiting for command to exit, be stopped, or be killed
finalStatus := <-statusChan

That's it, only three methods: Start, Stop, and Status. Although free, here are the selling points of go-cmd/Cmd:

  1. Channel-based fire and forget
  2. Real-time stdout and stderr
  3. Real-time status
  4. Complete and consolidated return
  5. Proper process termination
  6. 100% test coverage, no race conditions
Channel-based fire and forget

As the example above shows, starting a command immediately returns a channel to which the final status is sent when the command exits for any reason. So by default commands run asynchronously, but running synchronously is possible and easy, too:

// Run foo and block waiting for it to exit
c := cmd.NewCmd("foo")
s := <-c.Start()

To achieve similar with Go built-in Cmd requires everything this package already does.

Real-time stdout and stderr

It's common to want to read stdout or stderr while the command is running. The common approach is to call StdoutPipe and read from the provided io.ReadCloser. This works but it causes a race condition (that go test -race detects) and the docs say not to do it: "it is incorrect to call Wait before all reads from the pipe have completed".

The proper solution is to set the io.Writer of Stdout. To be thread-safe and non-racey, this requires further work to write while possibly N-many goroutines read. go-cmd/Cmd has already done this work.

Real-time status

Similar to real-time stdout and stderr, it's nice to see, for example, elapsed runtime. This package allows that: Status can be called any time by any goroutine, and it returns this struct:

type Status struct {
    Cmd      string
    PID      int
    Complete bool
    Exit     int
    Error    error
    Runtime  float64 // seconds
    Stdout   []string
    Stderr   []string
}
Complete and consolidated return

Speaking of that struct above, Go built-in Cmd does not put all the return information in one place, which is fine because Go is awesome! But to save some time, go-cmd/Cmd uses the Status struct above to convey all information about the command. Even when the command finishes, calling Status returns the final status, the same final status sent to the status channel returned by the call to Start.

Proper process termination

It's been said that everyone love's a good mystery. Then here's one: process group ID. If you know, then wow, congratulations! If not, don't feel bad. It took me hours one Saturday evening to solve this mystery. Let's just say that Go built-in Wait can still block even after the command is killed. But not go-cmd/Cmd. You can rely on Stop in this package.

100% test coverage, no race conditions

Enough said.


Acknowledgements

Brian Ip wrote the original code to get the exit status. Strangely, Go doesn't just provide this, it requires magic like exiterr.Sys().(syscall.WaitStatus) and more.

Documentation

Overview

Package cmd provides higher-level wrappers around os/exec.Cmd. All operations are thread-safe and designed to be used asynchronously by multiple goroutines.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cmd

type Cmd struct {
	Name string
	Args []string
	// --
	*sync.Mutex
	// contains filtered or unexported fields
}

Cmd represents an external command, similar to the Go built-in os/exec.Cmd. A Cmd cannot be reused after calling Start.

func NewCmd

func NewCmd(name string, args ...string) *Cmd

NewCmd creates a new Cmd for the given command name and arguments. The command is not started until Start is called.

func (*Cmd) Start

func (c *Cmd) Start() <-chan Status

Start starts the command and immediately returns a channel that the caller can use to receive the final Status of the command when it ends. The caller can start the command and wait like,

status := <-c.Start() // blocking

or start the command asynchronously and be notified later when it ends,

statusChan := c.Start() // non-blocking
// do other stuff...
status := <-statusChan // blocking

Either way, exactly one Status is sent on the channel when the command ends. The channel is not closed. Any error is set to Status.Error. Start is idempotent; it always returns the same channel.

func (*Cmd) Status

func (c *Cmd) Status() Status

Status returns the Status of the command at any time. It is safe to call concurrently by multiple goroutines.

func (*Cmd) Stop

func (c *Cmd) Stop() error

Stop stops the command by sending its process group a SIGTERM signal. Stop is idempotent. An error should only be returned in the rare case that Stop is called immediately after the command ends but before Start can update its internal state.

type Status

type Status struct {
	Cmd      string
	PID      int
	Complete bool    // false if stopped or signaled
	Exit     int     // exit code of process
	Error    error   // Go error
	StartTs  int64   // Unix ts (nanoseconds)
	StopTs   int64   // Unix ts (nanoseconds)
	Runtime  float64 // seconds
	Stdout   []string
	Stderr   []string
}

Status represents the status of a Cmd. It is valid during the entire lifecycle of the command. If StartTs > 0 (or PID > 0), the command has started. If StopTs > 0, the command has stopped. After the command has stopped, Exit = 0 is usually enough to indicate success, but complete success is indicated by:

Exit     = 0
Error    = nil
Complete = true

If Complete is false, the command was stopped or timed out. Error is a Go error related to starting or running the command.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL