diff --git a/helper/export_test.go b/helper/export_test.go new file mode 100644 index 0000000..6bf18ce --- /dev/null +++ b/helper/export_test.go @@ -0,0 +1,18 @@ +package helper + +import ( + "os" + "os/exec" + "testing" +) + +// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub +func ReplaceExecCommand(t *testing.T) { + t.Cleanup(func() { + execCommand = exec.Command + }) + + execCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command(os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...) + } +} diff --git a/helper/helper.go b/helper/helper.go index 5488ec5..1470092 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -16,6 +16,11 @@ var ( ErrStatusRead = errors.New("unexpected status response") ) +const ( + FortifyHelper = "FORTIFY_HELPER" + FortifyStatus = "FORTIFY_STATUS" +) + // Helper wraps *exec.Cmd and manages status and args fd. // Args is always 3 and status if set is always 4. type Helper struct { @@ -52,15 +57,20 @@ func (h *Helper) StartNotify(ready chan error) error { h.argsP[0], h.argsP[1] = pr, pw } // create status pipes if ready signal is requested + var sv string if ready != nil { if pr, pw, err := os.Pipe(); err != nil { return err } else { h.statP[0], h.statP[1] = pr, pw } + + sv = FortifyStatus + "=1" + } else { + sv = FortifyStatus + "=0" } - // prepare extra files + // prepare extra files from caller el := len(h.ExtraFiles) if ready != nil { el += 2 @@ -76,6 +86,7 @@ func (h *Helper) StartNotify(ready chan error) error { // prepare and start process h.Cmd.ExtraFiles = ef + h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1", sv) if err := h.Cmd.Start(); err != nil { return err } @@ -175,10 +186,12 @@ func (h *Helper) Start() error { return h.StartNotify(nil) } +var execCommand = exec.Command + func New(wt io.WriterTo, name string, arg ...string) *Helper { if wt == nil { panic("attempted to create helper with invalid argument writer") } - return &Helper{args: wt, Cmd: exec.Command(name, arg...)} + return &Helper{args: wt, Cmd: execCommand(name, arg...)} } diff --git a/helper/stub_test.go b/helper/stub_test.go new file mode 100644 index 0000000..9f85ac6 --- /dev/null +++ b/helper/stub_test.go @@ -0,0 +1,75 @@ +package helper_test + +import ( + "io" + "os" + "strconv" + "syscall" + "testing" + + "git.ophivana.moe/cat/fortify/helper" +) + +func TestHelperChildStub(t *testing.T) { + // this test mocks the helper process + if os.Getenv(helper.FortifyHelper) != "1" { + return + } + // simulate args pipe behaviour + func() { + f := os.NewFile(3, "|0") + if f == nil { + panic("attempted to start helper without args pipe") + } + + if _, err := io.Copy(os.Stdout, f); err != nil { + panic("cannot read args: " + err.Error()) + } + }() + + var wait chan struct{} + + // simulate status pipe behaviour + if os.Getenv(helper.FortifyStatus) == "1" { + wait = make(chan struct{}) + go func() { + f := os.NewFile(4, "|1") + if f == nil { + panic("attempted to start with status reporting without status pipe") + } + + if _, err := f.Write([]byte{'x'}); err != nil { + panic("cannot write to status pipe: " + err.Error()) + } + + // wait for status pipe close + var epoll int + if fd, err := syscall.EpollCreate1(0); err != nil { + panic("cannot open epoll fd: " + err.Error()) + } else { + defer func() { + if err = syscall.Close(fd); err != nil { + panic("cannot close epoll fd: " + err.Error()) + } + }() + epoll = fd + } + if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(f.Fd()), &syscall.EpollEvent{}); err != nil { + panic("cannot add status pipe to epoll: " + err.Error()) + } + events := make([]syscall.EpollEvent, 1) + if _, err := syscall.EpollWait(epoll, events, -1); err != nil { + panic("cannot poll status pipe: " + err.Error()) + } + if events[0].Events != syscall.EPOLLERR { + panic(strconv.Itoa(int(events[0].Events))) + + } + close(wait) + }() + } + + if wait != nil { + <-wait + } +}