aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--helper/helper.go180
1 files changed, 180 insertions, 0 deletions
diff --git a/helper/helper.go b/helper/helper.go
index d9bcc7f4..6f529229 100644
--- a/helper/helper.go
+++ b/helper/helper.go
@@ -2,3 +2,183 @@
Package helper runs external helpers and manages their status and args FDs.
*/
package helper
+
+import (
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "sync"
+)
+
+var (
+ ErrStatusFault = errors.New("generic status pipe fault")
+ ErrStatusRead = errors.New("unexpected status response")
+)
+
+// Helper wraps *exec.Cmd and manages status and args fd.
+// Args is always 3 and status if set is always 4.
+type Helper struct {
+ lock sync.RWMutex
+ args io.WriterTo
+
+ statP [2]*os.File
+ argsP [2]*os.File
+
+ ready chan error
+
+ // ExtraFiles specifies additional open files to be inherited by the
+ // new process. It does not include standard input, standard output, or
+ // standard error. If non-nil, entry i becomes file descriptor 5+i.
+ ExtraFiles []*os.File
+
+ *exec.Cmd
+}
+
+func (h *Helper) StartNotify(ready chan error) error {
+ h.lock.Lock()
+ defer h.lock.Unlock()
+
+ // Check for doubled Start calls before we defer failure cleanup. If the prior
+ // call to Start succeeded, we don't want to spuriously close its pipes.
+ if h.Cmd.Process != nil {
+ return errors.New("exec: already started")
+ }
+
+ // create pipes
+ if pr, pw, err := os.Pipe(); err != nil {
+ return err
+ } else {
+ h.argsP[0], h.argsP[1] = pr, pw
+ }
+ // create status pipes if ready signal is requested
+ if ready != nil {
+ if pr, pw, err := os.Pipe(); err != nil {
+ return err
+ } else {
+ h.statP[0], h.statP[1] = pr, pw
+ }
+ }
+
+ // prepare extra files
+ el := len(h.ExtraFiles)
+ if ready != nil {
+ el += 2
+ } else {
+ el++
+ }
+ ef := make([]*os.File, 0, el)
+ ef = append(ef, h.argsP[0])
+ if ready != nil {
+ ef = append(ef, h.statP[1])
+ }
+ ef = append(ef, h.ExtraFiles...)
+
+ // prepare and start process
+ h.Cmd.ExtraFiles = ef
+ if err := h.Cmd.Start(); err != nil {
+ return err
+ }
+
+ statsP, argsP := h.statP[0], h.argsP[1]
+
+ // write arguments and close args pipe
+ if _, err := h.args.WriteTo(argsP); err != nil {
+ if err1 := h.Cmd.Process.Kill(); err1 != nil {
+ panic(err1)
+ }
+ return err
+ } else {
+ if err = argsP.Close(); err != nil {
+ if err1 := h.Cmd.Process.Kill(); err1 != nil {
+ panic(err1)
+ }
+ return err
+ }
+ }
+
+ if ready != nil {
+ h.ready = ready
+
+ // monitor stat pipe
+ go func() {
+ n, err := statsP.Read(make([]byte, 1))
+ switch n {
+ case -1:
+ if err1 := h.Cmd.Process.Kill(); err1 != nil {
+ panic(err1)
+ }
+ // ensure error is not nil
+ if err == nil {
+ err = ErrStatusFault
+ }
+ ready <- err
+ case 0:
+ // ensure error is not nil
+ if err == nil {
+ err = ErrStatusRead
+ }
+ ready <- err
+ case 1:
+ ready <- nil
+ default:
+ panic("unreachable") // unexpected read count
+ }
+ }()
+ }
+
+ return nil
+}
+
+func (h *Helper) Wait() error {
+ h.lock.RLock()
+ defer h.lock.RUnlock()
+
+ if h.Cmd.Process == nil {
+ return errors.New("exec: not started")
+ }
+ if h.Cmd.ProcessState != nil {
+ return errors.New("exec: Wait was already called")
+ }
+
+ // ensure pipe close
+ defer func() {
+ if err := h.argsP[0].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+ panic(err)
+ }
+ if err := h.argsP[1].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+ panic(err)
+ }
+
+ if h.ready != nil {
+ if err := h.statP[0].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+ panic(err)
+ }
+ if err := h.statP[1].Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+ panic(err)
+ }
+ }
+ }()
+
+ return h.Cmd.Wait()
+}
+
+func (h *Helper) Close() error {
+ if h.ready == nil {
+ panic("attempted to close helper with no status pipe")
+ }
+
+ return h.statP[0].Close()
+}
+
+func (h *Helper) Start() error {
+ return h.StartNotify(nil)
+}
+
+func New(wt io.WriterTo, name string, arg ...string) *Helper {
+ if wt == nil {
+ panic("attempted to create helper with nil argument writer")
+ }
+
+ return &Helper{args: wt, Cmd: exec.Command(name, arg...)}
+}