diff --git a/engine/resources/exec.go b/engine/resources/exec.go index f1bf7394..629524ab 100644 --- a/engine/resources/exec.go +++ b/engine/resources/exec.go @@ -120,6 +120,9 @@ type ExecRes struct { // WatchCwd is the Cwd for the WatchCmd. See the docs for Cwd. WatchCwd string `lang:"watchcwd" yaml:"watchcwd"` + // WatchFiles is a list of files that will be kept track of. + WatchFiles []string `lang:"watchfiles" yaml:"watchfiles"` + // WatchShell is the Shell for the WatchCmd. See the docs for Shell. WatchShell string `lang:"watchshell" yaml:"watchshell"` @@ -216,6 +219,12 @@ func (obj *ExecRes) Validate() error { return fmt.Errorf("the Args param can't be used when Cmd has args") } + for _, file := range obj.WatchFiles { + if !strings.HasPrefix(file, "/") { + return fmt.Errorf("the path (`%s`) in WatchFiles must be absolute", file) + } + } + if obj.Creates != "" && !strings.HasPrefix(obj.Creates, "/") { return fmt.Errorf("the Creates param must be an absolute path") } @@ -263,10 +272,13 @@ func (obj *ExecRes) Cleanup() error { // Watch is the primary listener for this resource and it outputs events. func (obj *ExecRes) Watch(ctx context.Context) error { - defer obj.wg.Wait() + wg := &sync.WaitGroup{} + defer wg.Wait() ioChan := make(chan *cmdOutput) rwChan := make(chan recwatch.Event) + filesChan := make(chan recwatch.Event) + var watchCmd *exec.Cmd if obj.WatchCmd != "" { var cmdName string @@ -306,6 +318,46 @@ func (obj *ExecRes) Watch(ctx context.Context) error { } } + for _, file := range obj.WatchFiles { + recurse := strings.HasSuffix(file, "/") // check if it's a file or dir + recWatcher, err := recwatch.NewRecWatcher(file, recurse) + if err != nil { + return err + } + defer recWatcher.Close() + + wg.Add(1) + go func() { + defer wg.Done() + for { + var files recwatch.Event + var ok bool + var shutdown bool + + select { + case files, ok = <-recWatcher.Events(): // receiving events + case <-ctx.Done(): // unblock + return + } + + if !ok { + err := fmt.Errorf("channel shutdown") + files = recwatch.Event{Error: err} + shutdown = true + } + + select { + case filesChan <- files: // send events + if shutdown { // optimization to free early + return + } + case <-ctx.Done(): + return + } + } + }() + } + if obj.Creates != "" { recWatcher, err := recwatch.NewRecWatcher(obj.Creates, false) if err != nil { @@ -369,6 +421,15 @@ func (obj *ExecRes) Watch(ctx context.Context) error { } send = true + case files, ok := <-filesChan: + if !ok { // channel shutdown + return fmt.Errorf("unexpected recwatch shutdown") + } + if err := files.Error; err != nil { + return errwrap.Wrapf(err, "unknown %s watcher error", obj) + } + send = true + case <-ctx.Done(): // closed by the engine to signal shutdown return nil } @@ -828,6 +889,9 @@ func (obj *ExecRes) Cmp(r engine.Res) error { if obj.WatchShell != res.WatchShell { return fmt.Errorf("the WatchShell differs") } + if err := engineUtil.StrListCmp(obj.WatchFiles, res.WatchFiles); err != nil { + return errwrap.Wrapf(err, "the WatchFiles differ") + } if obj.IfCmd != res.IfCmd { return fmt.Errorf("the IfCmd differs") diff --git a/examples/lang/exec1.mcl b/examples/lang/exec1.mcl new file mode 100644 index 00000000..c25ca58c --- /dev/null +++ b/examples/lang/exec1.mcl @@ -0,0 +1,6 @@ +exec "exec1" { + cmd => "echo hello world > /tmp/whatever", + shell => "/bin/bash", + creates => "/tmp/whatever", + watchfiles => ["/tmp/whatever", "/tmp/adir/",], +}