engine: Fix up some send/recv corner cases

Initially I wasn't 100% clear or decided on the send/recv semantics.
After some experimenting, I think this is much closer to what we want.
Nothing should break or regress here, this only enables more
possibilities.
This commit is contained in:
James Shubin
2025-05-05 23:53:37 -04:00
parent ae1d9b94d4
commit 774d408e13
17 changed files with 412 additions and 32 deletions

View File

@@ -38,6 +38,7 @@ import (
"os"
"os/exec"
"os/user"
"path"
"sort"
"strings"
"sync"
@@ -161,10 +162,28 @@ type ExecRes struct {
// used for any command being run.
Group string `lang:"group" yaml:"group"`
// SendOutput is a value which can be sent for the Send/Recv Output
// field if no value is available in the cache. This is used in very
// specialized scenarios (particularly prototyping and unclean
// environments) and should not be used routinely. It should be used
// only in situations where we didn't produce our own sending values,
// and there are none in the cache, and instead are relying on a runtime
// mechanism to help us out. This can commonly occur if you wish to make
// incremental progress when locally testing some code using Send/Recv,
// but you are combining it with --tmp-prefix for other reasons.
SendOutput *string `lang:"send_output" yaml:"send_output"`
// SendStdout is like SendOutput but for stdout alone. See those docs.
SendStdout *string `lang:"send_stdout" yaml:"send_stdout"`
// SendStderr is like SendOutput but for stderr alone. See those docs.
SendStderr *string `lang:"send_stderr" yaml:"send_stderr"`
output *string // all cmd output, read only, do not set!
stdout *string // the cmd stdout, read only, do not set!
stderr *string // the cmd stderr, read only, do not set!
dir string // the path to local storage
interruptChan chan struct{}
wg *sync.WaitGroup
}
@@ -225,6 +244,12 @@ func (obj *ExecRes) Validate() error {
func (obj *ExecRes) Init(init *engine.Init) error {
obj.init = init // save for later
dir, err := obj.init.VarDir("")
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir in Init()")
}
obj.dir = dir
obj.interruptChan = make(chan struct{})
obj.wg = &sync.WaitGroup{}
@@ -364,6 +389,10 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
// check and this will run. It is still guarded by the IfCmd, but it can
// have a chance to execute, and all without the check of obj.Refresh()!
if err := obj.checkApplyReadCache(); err != nil {
return false, err
}
if obj.IfCmd != "" { // if there is no onlyif check, we should just run
var cmdName string
var cmdArgs []string
@@ -423,6 +452,13 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
obj.init.Logf("ifcmd out:")
obj.init.Logf("%s", s)
}
//if err := obj.checkApplyWriteCache(); err != nil {
// return false, err
//}
obj.safety()
if err := obj.send(); err != nil {
return false, err
}
return true, nil // don't run
}
if s := out.String(); s == "" {
@@ -436,12 +472,26 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if obj.Creates != "" { // gate the extra syscall
if _, err := os.Stat(obj.Creates); err == nil {
obj.init.Logf("creates file exists, skipping cmd")
//if err := obj.checkApplyWriteCache(); err != nil {
// return false, err
//}
obj.safety()
if err := obj.send(); err != nil {
return false, err
}
return true, nil // don't run
}
}
// state is not okay, no work done, exit, but without error
if !apply {
//if err := obj.checkApplyWriteCache(); err != nil {
// return false, err
//}
//obj.safety()
if err := obj.send(); err != nil {
return false, err
}
return false, nil
}
@@ -654,11 +704,10 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
}
}
if err := obj.init.Send(&ExecSends{
Output: obj.output,
Stdout: obj.stdout,
Stderr: obj.stderr,
}); err != nil {
if err := obj.checkApplyWriteCache(); err != nil {
return false, err
}
if err := obj.send(); err != nil {
return false, err
}
@@ -670,6 +719,77 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
return false, nil // success
}
// send is a helper to avoid duplication of the same send operation.
func (obj *ExecRes) send() error {
return obj.init.Send(&ExecSends{
Output: obj.output,
Stdout: obj.stdout,
Stderr: obj.stderr,
})
}
// safety is a helper function that populates the cached "send" values if they
// are empty. It must only be called right before actually sending any values,
// and right before CheckApply returns. It should be used only in situations
// where we didn't produce our own sending values, and there are none in the
// cache, and instead are relying on a runtime mechanism to help us out. This
// mechanism is useful as a backstop for when we're running in unclean
// scenarios.
func (obj *ExecRes) safety() {
if x := obj.SendOutput; x != nil && obj.output == nil {
s := *x // copy
obj.output = &s
}
if x := obj.SendStdout; x != nil && obj.stdout == nil {
s := *x // copy
obj.stdout = &s
}
if x := obj.SendStderr; x != nil && obj.stderr == nil {
s := *x // copy
obj.stderr = &s
}
}
// checkApplyReadCache is a helper to do all our reading from the cache.
func (obj *ExecRes) checkApplyReadCache() error {
output, err := engineUtil.ReadData(path.Join(obj.dir, "output"))
if err != nil {
return err
}
obj.output = output
stdout, err := engineUtil.ReadData(path.Join(obj.dir, "stdout"))
if err != nil {
return err
}
obj.stdout = stdout
stderr, err := engineUtil.ReadData(path.Join(obj.dir, "stderr"))
if err != nil {
return err
}
obj.stderr = stderr
return nil
}
// checkApplyWriteCache is a helper to do all our writing into the cache.
func (obj *ExecRes) checkApplyWriteCache() error {
if _, err := engineUtil.WriteData(path.Join(obj.dir, "output"), obj.output); err != nil {
return err
}
if _, err := engineUtil.WriteData(path.Join(obj.dir, "stdout"), obj.stdout); err != nil {
return err
}
if _, err := engineUtil.WriteData(path.Join(obj.dir, "stderr"), obj.stderr); err != nil {
return err
}
return nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *ExecRes) Cmp(r engine.Res) error {
// we can only compare ExecRes to others of the same resource kind
@@ -740,6 +860,16 @@ func (obj *ExecRes) Cmp(r engine.Res) error {
return fmt.Errorf("the Group differs")
}
if err := engineUtil.StrPtrCmp(obj.SendOutput, res.SendOutput); err != nil {
return errwrap.Wrapf(err, "the SendOutput differs")
}
if err := engineUtil.StrPtrCmp(obj.SendStdout, res.SendStdout); err != nil {
return errwrap.Wrapf(err, "the SendStdout differs")
}
if err := engineUtil.StrPtrCmp(obj.SendStderr, res.SendStderr); err != nil {
return errwrap.Wrapf(err, "the SendStderr differs")
}
return nil
}