// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

package resources

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"sort"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/purpleidea/mgmt/engine"
	"github.com/purpleidea/mgmt/engine/traits"
	engineUtil "github.com/purpleidea/mgmt/engine/util"
	"github.com/purpleidea/mgmt/util/errwrap"
	"github.com/purpleidea/mgmt/util/recwatch"
)

func init() {
	engine.RegisterResource("exec", func() engine.Res { return &ExecRes{} })
}

// ExecRes is an exec resource for running commands.
type ExecRes struct {
	traits.Base // add the base methods without re-implementation
	traits.Edgeable
	traits.Sendable

	init *engine.Init

	// Cmd is the command to run. If this is not specified, we use the name.
	// Remember that if you're not using `Shell` (the default) then adding
	// single quotes around args make them part of the actual values. IOW,
	// if your command is: "touch '/tmp/foo'", then (1) it probably won't be
	// able to find the "touch" command (use /usr/bin/touch instead) and (2)
	// the file won't be in the /tmp/ directory, it will be an oddly named
	// file that contains two single quotes, and it will likely error since
	// the dir path doesn't exist. In general, it's best to use the `Args`
	// field instead of including them here.
	// XXX: if not using shell, don't allow args here, force them to args!
	Cmd string `lang:"cmd" yaml:"cmd"`

	// Args is a list of args to pass to Cmd. This can be used *instead* of
	// passing the full command and args as a single string to Cmd. It can
	// only be used when a Shell is *not* specified. The advantage of this
	// is that you don't have to worry about escape characters.
	Args []string `lang:"args" yaml:"args"`

	// Cwd is the dir to run the command in. If empty, then this will use
	// the working directory of the calling process. (This process is mgmt,
	// not the process being run here.) Keep in mind that if you're running
	// this command as a user that does not have perms to the current
	// directory, you may wish to set this to `/` to avoid hitting an error
	// such as: `could not change directory to "/root": Permission denied`.
	Cwd string `lang:"cwd" yaml:"cwd"`

	// Shell is the (optional) shell to use to run the cmd. If you specify
	// this, then you can't use the Args parameter.
	Shell string `lang:"shell" yaml:"shell"`

	// Timeout is the number of seconds to wait before sending a Kill to the
	// running command. If the Kill is received before the process exits,
	// then this be treated as an error.
	Timeout uint64 `lang:"timeout" yaml:"timeout"`

	// Env allows the user to specify environment variables for script
	// execution. These are taken using a map of format of VAR_NAME -> value.
	Env map[string]string `lang:"env" yaml:"env"`

	// WatchCmd is the command to run to detect event changes. Each line of
	// output from this command is treated as an event.
	WatchCmd string `lang:"watchcmd" yaml:"watchcmd"`

	// WatchCwd is the Cwd for the WatchCmd. See the docs for Cwd.
	WatchCwd string `lang:"watchcwd" yaml:"watchcwd"`

	// WatchShell is the Shell for the WatchCmd. See the docs for Shell.
	WatchShell string `lang:"watchshell" yaml:"watchshell"`

	// IfCmd is the command that runs to guard against running the Cmd. If
	// this command succeeds, then Cmd *will* be run. If this command
	// returns a non-zero result, then the Cmd will not be run. Any error
	// scenario or timeout will cause the resource to error.
	IfCmd string `lang:"ifcmd" yaml:"ifcmd"`

	// IfCwd is the Cwd for the IfCmd. See the docs for Cwd.
	IfCwd string `lang:"ifcwd" yaml:"ifcwd"`

	// IfShell is the Shell for the IfCmd. See the docs for Shell.
	IfShell string `lang:"ifshell" yaml:"ifshell"`

	// Creates is the absolute file path to check for before running the
	// main cmd. If this path exists, then the cmd will not run. More
	// precisely we attempt to `stat` the file, so it must succeed for a
	// skip. This also adds a watch on this path which re-checks things when
	// it changes.
	Creates string `lang:"creates" yaml:"creates"`

	// DoneCmd is the command that runs after the regular Cmd runs
	// successfully. This is a useful pattern to avoid the shelling out to
	// bash simply to do `$cmd && echo done > /tmp/donefile`. If this
	// command errors, it behaves as if the normal Cmd had errored.
	DoneCmd string `lang:"donecmd" yaml:"donecmd"`

	// DoneCwd is the Cwd for the DoneCmd. See the docs for Cwd.
	DoneCwd string `lang:"donecwd" yaml:"donecwd"`

	// DoneShell is the Shell for the DoneCmd. See the docs for Shell.
	DoneShell string `lang:"doneshell" yaml:"doneshell"`

	// User is the (optional) user to use to execute the command. It is used
	// for any command being run.
	User string `lang:"user" yaml:"user"`

	// Group is the (optional) group to use to execute the command. It is
	// used for any command being run.
	Group string `lang:"group" yaml:"group"`

	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!

	interruptChan chan struct{}
	wg            *sync.WaitGroup
}

// Default returns some sensible defaults for this resource.
func (obj *ExecRes) Default() engine.Res {
	return &ExecRes{}
}

// getCmd returns the actual command to run. When Cmd is not specified, we use
// the Name.
func (obj *ExecRes) getCmd() string {
	if obj.Cmd != "" {
		return obj.Cmd
	}
	return obj.Name()
}

// Validate if the params passed in are valid data.
func (obj *ExecRes) Validate() error {
	if obj.getCmd() == "" { // this is the only thing that is really required
		return fmt.Errorf("the Cmd can't be empty")
	}

	split := strings.Fields(obj.getCmd())
	if len(obj.Args) > 0 && obj.Shell != "" {
		return fmt.Errorf("the Args param can't be used with a Shell")
	}
	if len(obj.Args) > 0 && len(split) > 1 {
		return fmt.Errorf("the Args param can't be used when Cmd has args")
	}

	if obj.Creates != "" && !strings.HasPrefix(obj.Creates, "/") {
		return fmt.Errorf("the Creates param must be an absolute path")
	}

	// check that, if an user or a group is set, we're running as root
	if obj.User != "" || obj.Group != "" {
		currentUser, err := user.Current()
		if err != nil {
			return errwrap.Wrapf(err, "error looking up current user")
		}
		if currentUser.Uid != "0" {
			return fmt.Errorf("running as root is required if you want to use exec with a different user/group")
		}
	}

	// check that environment variables' format is valid
	for key := range obj.Env {
		if err := isNameValid(key); err != nil {
			return errwrap.Wrapf(err, "invalid variable name")
		}
	}
	return nil
}

// Init runs some startup code for this resource.
func (obj *ExecRes) Init(init *engine.Init) error {
	obj.init = init // save for later

	obj.interruptChan = make(chan struct{})
	obj.wg = &sync.WaitGroup{}

	return nil
}

// Cleanup is run by the engine to clean up after the resource is done.
func (obj *ExecRes) Cleanup() error {
	return nil
}

// Watch is the primary listener for this resource and it outputs events.
func (obj *ExecRes) Watch(ctx context.Context) error {
	defer obj.wg.Wait()

	ioChan := make(chan *cmdOutput)
	rwChan := make(chan recwatch.Event)
	var watchCmd *exec.Cmd
	if obj.WatchCmd != "" {
		var cmdName string
		var cmdArgs []string
		if obj.WatchShell == "" {
			// call without a shell
			// FIXME: are there still whitespace splitting issues?
			split := strings.Fields(obj.WatchCmd)
			cmdName = split[0]
			//d, _ := os.Getwd() // TODO: how does this ever error ?
			//cmdName = path.Join(d, cmdName)
			cmdArgs = split[1:]
		} else {
			cmdName = obj.WatchShell // usually bash, or sh
			cmdArgs = []string{"-c", obj.WatchCmd}
		}

		innerCtx, cancel := context.WithCancel(context.Background())
		defer cancel()
		cmd := exec.CommandContext(innerCtx, cmdName, cmdArgs...)
		cmd.Dir = obj.WatchCwd // run program in pwd if ""
		// ignore signals sent to parent process (we're in our own group)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Setpgid: true,
			Pgid:    0,
		}
		watchCmd = cmd // store for errors

		// if we have a user and group, use them
		var err error
		if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
			return errwrap.Wrapf(err, "error while setting credential")
		}

		if ioChan, err = obj.cmdOutputRunner(innerCtx, cmd); err != nil {
			return errwrap.Wrapf(err, "error starting WatchCmd")
		}
	}

	if obj.Creates != "" {
		recWatcher, err := recwatch.NewRecWatcher(obj.Creates, false)
		if err != nil {
			return err
		}
		defer recWatcher.Close()
		rwChan = recWatcher.Events()
	}

	obj.init.Running() // when started, notify engine that we're running

	var send = false // send event?
	for {
		select {
		case data, ok := <-ioChan:
			if !ok { // EOF
				// FIXME: add an "if watch command ends/crashes"
				// restart or generate error option
				return fmt.Errorf("reached EOF")
			}
			if err := data.err; err != nil {
				// error reading input or cmd failure
				exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
				if !ok {
					// command failed in some bad way
					return errwrap.Wrapf(err, "watchcmd failed in some bad way")
				}
				pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
				wStatus, ok := pStateSys.(syscall.WaitStatus)
				if !ok {
					return errwrap.Wrapf(err, "could not get exit status of watchcmd")
				}
				exitStatus := wStatus.ExitStatus()
				if exitStatus == 0 {
					// i'm not sure if this could happen
					return errwrap.Wrapf(err, "unexpected watchcmd exit status of zero")
				}

				obj.init.Logf("watchcmd: %s", strings.Join(watchCmd.Args, " "))
				obj.init.Logf("watchcmd exited with: %d", exitStatus)
				return errwrap.Wrapf(err, "watchcmd errored")
			}

			// each time we get a line of output, we loop!
			if s := data.text; s == "" {
				obj.init.Logf("watch out empty!")
			} else {
				obj.init.Logf("watch out:")
				obj.init.Logf("%s", s)
			}
			if data.text != "" {
				send = true
			}

		case event, ok := <-rwChan:
			if !ok { // channel shutdown
				return fmt.Errorf("unexpected recwatch shutdown")
			}
			if err := event.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
		}

		// do all our event sending all together to avoid duplicate msgs
		if send {
			send = false
			obj.init.Event() // notify engine of an event (this can block)
		}
	}
}

// CheckApply checks the resource state and applies the resource if the bool
// input is true. It returns error info and if the state check passed or not.
// TODO: expand the IfCmd to be a list of commands
func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
	// If we receive a refresh signal, then the engine skips the IsStateOK()
	// 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 obj.IfCmd != "" { // if there is no onlyif check, we should just run
		var cmdName string
		var cmdArgs []string
		if obj.IfShell == "" {
			// call without a shell
			// FIXME: are there still whitespace splitting issues?
			split := strings.Fields(obj.IfCmd)
			cmdName = split[0]
			//d, _ := os.Getwd() // TODO: how does this ever error ?
			//cmdName = path.Join(d, cmdName)
			cmdArgs = split[1:]
		} else {
			cmdName = obj.IfShell // usually bash, or sh
			cmdArgs = []string{"-c", obj.IfCmd}
		}
		cmd := exec.Command(cmdName, cmdArgs...)
		cmd.Dir = obj.IfCwd // run program in pwd if ""
		// ignore signals sent to parent process (we're in our own group)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Setpgid: true,
			Pgid:    0,
		}

		// if we have an user and group, use them
		var err error
		if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
			return false, errwrap.Wrapf(err, "error while setting credential")
		}

		var out splitWriter
		out.Init()
		cmd.Stdout = out.Stdout
		cmd.Stderr = out.Stderr

		if err := cmd.Run(); err != nil {
			exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
			if !ok {
				// command failed in some bad way
				return false, errwrap.Wrapf(err, "ifcmd failed in some bad way")
			}
			pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
			wStatus, ok := pStateSys.(syscall.WaitStatus)
			if !ok {
				return false, errwrap.Wrapf(err, "could not get exit status of ifcmd")
			}
			exitStatus := wStatus.ExitStatus()
			if exitStatus == 0 {
				// i'm not sure if this could happen
				return false, errwrap.Wrapf(err, "unexpected ifcmd exit status of zero")
			}

			obj.init.Logf("ifcmd: %s", strings.Join(cmd.Args, " "))
			obj.init.Logf("ifcmd exited with: %d, skipping cmd", exitStatus)
			if s := out.String(); s == "" {
				obj.init.Logf("ifcmd out empty!")
			} else {
				obj.init.Logf("ifcmd out:")
				obj.init.Logf("%s", s)
			}
			return true, nil // don't run
		}
		if s := out.String(); s == "" {
			obj.init.Logf("ifcmd out empty!")
		} else {
			obj.init.Logf("ifcmd out:")
			obj.init.Logf("%s", s)
		}
	}

	if obj.Creates != "" { // gate the extra syscall
		if _, err := os.Stat(obj.Creates); err == nil {
			obj.init.Logf("creates file exists, skipping cmd")
			return true, nil // don't run
		}
	}

	// state is not okay, no work done, exit, but without error
	if !apply {
		return false, nil
	}

	// apply portion
	obj.init.Logf("Apply")
	var cmdName string
	var cmdArgs []string
	if obj.Shell == "" {
		// call without a shell
		// FIXME: are there still whitespace splitting issues?
		// TODO: we could make the split character user selectable...!
		split := strings.Fields(obj.getCmd())
		cmdName = split[0]
		//d, _ := os.Getwd() // TODO: how does this ever error ?
		//cmdName = path.Join(d, cmdName)
		cmdArgs = split[1:]
		if len(obj.Args) > 0 {
			if len(split) != 1 { // should not happen
				return false, fmt.Errorf("validation error")
			}
			cmdArgs = obj.Args
		}
	} else {
		cmdName = obj.Shell // usually bash, or sh
		cmdArgs = []string{"-c", obj.getCmd()}
	}

	wg := &sync.WaitGroup{}
	defer wg.Wait() // this must be above the defer cancel() call
	var innerCtx context.Context
	var cancel context.CancelFunc
	if obj.Timeout > 0 { // cmd.Process.Kill() is called on timeout
		innerCtx, cancel = context.WithTimeout(ctx, time.Duration(obj.Timeout)*time.Second)
	} else { // zero timeout means no timer
		innerCtx, cancel = context.WithCancel(ctx)
	}
	defer cancel()
	cmd := exec.CommandContext(innerCtx, cmdName, cmdArgs...)
	cmd.Dir = obj.Cwd // run program in pwd if ""

	envKeys := []string{}
	for key := range obj.Env {
		envKeys = append(envKeys, key)
	}
	sort.Strings(envKeys)
	cmdEnv := []string{}
	for _, k := range envKeys {
		cmdEnv = append(cmdEnv, k+"="+obj.Env[k])
	}
	cmd.Env = cmdEnv

	// ignore signals sent to parent process (we're in our own group)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
		Pgid:    0,
	}

	// if we have a user and group, use them
	var err error
	if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
		return false, errwrap.Wrapf(err, "error while setting credential")
	}

	var out splitWriter
	out.Init()
	// from the docs: "If Stdout and Stderr are the same writer, at most one
	// goroutine at a time will call Write." so we trick it here!
	cmd.Stdout = out.Stdout
	cmd.Stderr = out.Stderr

	obj.init.Logf("cmd: %s", strings.Join(cmd.Args, " "))
	if err := cmd.Start(); err != nil {
		return false, errwrap.Wrapf(err, "error starting cmd")
	}

	wg.Add(1)
	go func() {
		defer wg.Done()
		select {
		case <-obj.interruptChan:
			cancel()
		case <-innerCtx.Done():
			// let this exit
		}
	}()

	err = cmd.Wait() // we can unblock this with the timeout

	// save in memory for send/recv
	// we use pointers to strings to indicate if used or not
	if out.Stdout.Activity || out.Stderr.Activity {
		str := out.String()
		obj.output = &str
	}
	if out.Stdout.Activity {
		str := out.Stdout.String()
		obj.stdout = &str
	}
	if out.Stderr.Activity {
		str := out.Stderr.String()
		obj.stderr = &str
	}

	// process the err result from cmd, we process non-zero exits here too!
	exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
	if err != nil && ok {
		pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
		wStatus, ok := pStateSys.(syscall.WaitStatus)
		if !ok {
			return false, errwrap.Wrapf(err, "error running cmd")
		}
		exitStatus := wStatus.ExitStatus()
		if !wStatus.Signaled() { // not a timeout or cancel (no signal)
			// most commands error in this way
			if s := out.String(); s == "" {
				obj.init.Logf("exit status %d", exitStatus)
			} else {
				obj.init.Logf("cmd error: %s", s)
			}

			return false, errwrap.Wrapf(err, "cmd error") // exit status will be in the error
		}
		sig := wStatus.Signal()

		// we get this on timeout, because ctx calls cmd.Process.Kill()
		if sig == syscall.SIGKILL {
			return false, errwrap.Wrapf(err, "cmd timeout, exit status: %d", exitStatus)
		}

		return false, errwrap.Wrapf(err, "unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)

	} else if err != nil {
		return false, errwrap.Wrapf(err, "general cmd error")
	}

	// TODO: if we printed the stdout while the command is running, this
	// would be nice, but it would require terminal log output that doesn't
	// interleave all the parallel parts which would mix it all up...
	if s := out.String(); s == "" {
		obj.init.Logf("cmd out empty!")
	} else {
		obj.init.Logf("cmd out:")
		obj.init.Logf("%s", s)
	}

	if obj.DoneCmd != "" {
		var cmdName string
		var cmdArgs []string
		if obj.DoneShell == "" {
			// call without a shell
			// FIXME: are there still whitespace splitting issues?
			split := strings.Fields(obj.DoneCmd)
			cmdName = split[0]
			//d, _ := os.Getwd() // TODO: how does this ever error ?
			//cmdName = path.Join(d, cmdName)
			cmdArgs = split[1:]
		} else {
			cmdName = obj.DoneShell // usually bash, or sh
			cmdArgs = []string{"-c", obj.DoneCmd}
		}
		cmd := exec.Command(cmdName, cmdArgs...)
		cmd.Dir = obj.DoneCwd // run program in pwd if ""
		// ignore signals sent to parent process (we're in our own group)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Setpgid: true,
			Pgid:    0,
		}

		// if we have an user and group, use them
		var err error
		if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
			return false, errwrap.Wrapf(err, "error while setting credential")
		}

		var out splitWriter
		out.Init()
		cmd.Stdout = out.Stdout
		cmd.Stderr = out.Stderr

		if err := cmd.Run(); err != nil {
			exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
			if !ok {
				// command failed in some bad way
				return false, errwrap.Wrapf(err, "donecmd failed in some bad way")
			}
			pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
			wStatus, ok := pStateSys.(syscall.WaitStatus)
			if !ok {
				return false, errwrap.Wrapf(err, "could not get exit status of donecmd")
			}
			exitStatus := wStatus.ExitStatus()
			if exitStatus == 0 {
				// i'm not sure if this could happen
				return false, errwrap.Wrapf(err, "unexpected donecmd exit status of zero")
			}

			obj.init.Logf("donecmd: %s", strings.Join(cmd.Args, " "))
			if s := out.String(); s == "" {
				obj.init.Logf("donecmd exit status %d", exitStatus)
			} else {
				obj.init.Logf("donecmd error: %s", s)
			}
			return false, errwrap.Wrapf(err, "cmd error") // exit status will be in the error
		}
		if s := out.String(); s == "" {
			obj.init.Logf("donecmd out empty!")
		} else {
			obj.init.Logf("donecmd out:")
			obj.init.Logf("%s", s)
		}
	}

	if err := obj.init.Send(&ExecSends{
		Output: obj.output,
		Stdout: obj.stdout,
		Stderr: obj.stderr,
	}); err != nil {
		return false, err
	}

	// The state tracking is for exec resources that can't "detect" their
	// state, and assume it's invalid when the Watch() function triggers.
	// If we apply state successfully, we should reset it here so that we
	// know that we have applied since the state was set not ok by event!
	// This now happens automatically after the engine runs CheckApply().
	return false, nil // success
}

// 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
	res, ok := r.(*ExecRes)
	if !ok {
		return fmt.Errorf("not a %s", obj.Kind())
	}

	if obj.Cmd != res.Cmd {
		return fmt.Errorf("the Cmd differs")
	}
	if len(obj.Args) != len(res.Args) {
		return fmt.Errorf("the Args differ")
	}
	for i, a := range obj.Args {
		if a != res.Args[i] {
			return fmt.Errorf("the Args differ at index: %d", i)
		}
	}
	if obj.Cwd != res.Cwd {
		return fmt.Errorf("the Cwd differs")
	}
	if obj.Shell != res.Shell {
		return fmt.Errorf("the Shell differs")
	}
	if obj.Timeout != res.Timeout {
		return fmt.Errorf("the Timeout differs")
	}

	if obj.WatchCmd != res.WatchCmd {
		return fmt.Errorf("the WatchCmd differs")
	}
	if obj.WatchCwd != res.WatchCwd {
		return fmt.Errorf("the WatchCwd differs")
	}
	if obj.WatchShell != res.WatchShell {
		return fmt.Errorf("the WatchShell differs")
	}

	if obj.IfCmd != res.IfCmd {
		return fmt.Errorf("the IfCmd differs")
	}
	if obj.IfCwd != res.IfCwd {
		return fmt.Errorf("the IfCwd differs")
	}
	if obj.IfShell != res.IfShell {
		return fmt.Errorf("the IfShell differs")
	}

	if obj.Creates != res.Creates {
		return fmt.Errorf("the Creates differs")
	}

	if obj.DoneCmd != res.DoneCmd {
		return fmt.Errorf("the DoneCmd differs")
	}
	if obj.DoneCwd != res.DoneCwd {
		return fmt.Errorf("the DoneCwd differs")
	}
	if obj.DoneShell != res.DoneShell {
		return fmt.Errorf("the DoneShell differs")
	}

	if obj.User != res.User {
		return fmt.Errorf("the User differs")
	}
	if obj.Group != res.Group {
		return fmt.Errorf("the Group differs")
	}

	return nil
}

// Interrupt is called to ask the execution of this resource to end early.
func (obj *ExecRes) Interrupt() error {
	close(obj.interruptChan)
	return nil
}

// ExecUID is the UID struct for ExecRes.
type ExecUID struct {
	engine.BaseUID
	Cmd      string
	WatchCmd string
	IfCmd    string
	DoneCmd  string
	// TODO: add more elements here
}

// ExecResAutoEdges holds the state of the auto edge generator.
type ExecResAutoEdges struct {
	edges   []engine.ResUID
	pointer int
}

// Next returns the next automatic edge.
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
	if len(obj.edges) == 0 {
		return nil
	}
	value := obj.edges[obj.pointer]
	obj.pointer++
	return []engine.ResUID{value}
}

// Test gets results of the earlier Next() call, & returns if we should
// continue!
func (obj *ExecResAutoEdges) Test(input []bool) bool {
	if len(obj.edges) <= obj.pointer {
		return false
	}
	if len(input) != 1 { // in case we get given bad data
		panic(fmt.Sprintf("Expecting a single value!"))
	}
	return true // keep going
}

// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
	var data []engine.ResUID
	var reversed = true

	for _, x := range obj.cmdFiles() {
		data = append(data, &PkgFileUID{
			BaseUID: engine.BaseUID{
				Name:     obj.Name(),
				Kind:     obj.Kind(),
				Reversed: &reversed,
			},
			path: x, // what matters
		})
		data = append(data, &FileUID{
			BaseUID: engine.BaseUID{
				Name:     obj.Name(),
				Kind:     obj.Kind(),
				Reversed: &reversed,
			},
			path: x,
		})
	}
	if obj.User != "" {
		data = append(data, &UserUID{
			BaseUID: engine.BaseUID{
				Name:     obj.Name(),
				Kind:     obj.Kind(),
				Reversed: &reversed,
			},
			name: obj.User,
		})
	}
	if obj.Group != "" {
		data = append(data, &GroupUID{
			BaseUID: engine.BaseUID{
				Name:     obj.Name(),
				Kind:     obj.Kind(),
				Reversed: &reversed,
			},
			name: obj.Group,
		})
	}

	return &ExecResAutoEdges{
		edges:   data,
		pointer: 0,
	}, nil
}

// UIDs includes all params to make a unique identification of this object. Most
// resources only return one, although some resources can return multiple.
func (obj *ExecRes) UIDs() []engine.ResUID {
	x := &ExecUID{
		BaseUID:  engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
		Cmd:      obj.getCmd(),
		WatchCmd: obj.WatchCmd,
		IfCmd:    obj.IfCmd,
		DoneCmd:  obj.DoneCmd,
		// TODO: add more params here
	}
	return []engine.ResUID{x}
}

// ExecSends is the struct of data which is sent after a successful Apply.
type ExecSends struct {
	// Output is the combined stdout and stderr of the command.
	Output *string `lang:"output"`
	// Stdout is the stdout of the command.
	Stdout *string `lang:"stdout"`
	// Stderr is the stderr of the command.
	Stderr *string `lang:"stderr"`
}

// Sends represents the default struct of values we can send using Send/Recv.
func (obj *ExecRes) Sends() interface{} {
	return &ExecSends{
		Output: nil,
		Stdout: nil,
		Stderr: nil,
	}
}

// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
	type rawRes ExecRes // indirection to avoid infinite recursion

	def := obj.Default()      // get the default
	res, ok := def.(*ExecRes) // put in the right format
	if !ok {
		return fmt.Errorf("could not convert to ExecRes")
	}
	raw := rawRes(*res) // convert; the defaults go here

	if err := unmarshal(&raw); err != nil {
		return err
	}

	*obj = ExecRes(raw) // restore from indirection with type conversion!
	return nil
}

// getCredential returns the correct *syscall.Credential if an User and Group
// are set.
func (obj *ExecRes) getCredential() (*syscall.Credential, error) {
	var uid, gid int
	var err error
	var currentUser *user.User
	if currentUser, err = user.Current(); err != nil {
		return nil, errwrap.Wrapf(err, "error looking up current user")
	}
	if currentUser.Uid != "0" {
		// since we're not root, we've got nothing to do
		return nil, nil
	}

	if obj.Group != "" {
		gid, err = engineUtil.GetGID(obj.Group)
		if err != nil {
			return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group)
		}
	}

	if obj.User != "" {
		uid, err = engineUtil.GetUID(obj.User)
		if err != nil {
			return nil, errwrap.Wrapf(err, "error looking up uid for %s", obj.User)
		}
	}

	return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, nil
}

// cmdFiles returns all the potential files/commands this command might need.
func (obj *ExecRes) cmdFiles() []string {
	var paths []string
	if obj.Shell != "" {
		paths = append(paths, obj.Shell)
	} else if sp := strings.Fields(obj.getCmd()); len(sp) > 0 {
		paths = append(paths, sp[0])
	}
	if obj.WatchShell != "" {
		paths = append(paths, obj.WatchShell)
	} else if sp := strings.Fields(obj.WatchCmd); len(sp) > 0 {
		paths = append(paths, sp[0])
	}
	if obj.IfShell != "" {
		paths = append(paths, obj.IfShell)
	} else if sp := strings.Fields(obj.IfCmd); len(sp) > 0 {
		paths = append(paths, sp[0])
	}
	if obj.DoneShell != "" {
		paths = append(paths, obj.DoneShell)
	} else if sp := strings.Fields(obj.DoneCmd); len(sp) > 0 {
		paths = append(paths, sp[0])
	}
	return paths
}

// cmdOutput is the output struct of the cmdOutputRunner channel output. You
// should always check the error first. If it's nil, then you can assume the
// text data is good to use.
type cmdOutput struct {
	text string
	err  error
}

// cmdOutputRunner wraps the Cmd in with a StdoutPipe scanner and reads for
// errors. It runs Start and Wait, and errors runtime things in the channel. If
// it can't start up the command, it will fail early. Once it's running, it will
// return the channel which can be used for the duration of the process.
// Cancelling the context merely unblocks the sending on the output channel, it
// does not Kill the cmd process. For that you must do it yourself elsewhere.
func (obj *ExecRes) cmdOutputRunner(ctx context.Context, cmd *exec.Cmd) (chan *cmdOutput, error) {
	stdoutReader, err := cmd.StdoutPipe()
	if err != nil {
		return nil, errwrap.Wrapf(err, "error creating StdoutPipe for Cmd")
	}
	stderrReader, err := cmd.StderrPipe()
	if err != nil {
		return nil, errwrap.Wrapf(err, "error creating StderrPipe for Cmd")
	}
	// XXX: Can io.MultiReader when one of these is still open? Is there an
	// issue or race here about calling cmd.Wait() if only one of them dies?
	cmdReader := io.MultiReader(stdoutReader, stderrReader)
	scanner := bufio.NewScanner(cmdReader)
	if err := cmd.Start(); err != nil {
		return nil, errwrap.Wrapf(err, "error starting Cmd")
	}

	ch := make(chan *cmdOutput)
	obj.wg.Add(1)
	go func() {
		defer obj.wg.Done()
		defer close(ch)
		for scanner.Scan() {
			select {
			case ch <- &cmdOutput{text: scanner.Text()}: // blocks here ?
			case <-ctx.Done():
				return
			}
		}

		// on EOF, scanner.Err() will be nil
		reterr := scanner.Err()
		reterr = errwrap.Append(reterr, cmd.Wait()) // always run Wait()
		// send any misc errors we encounter on the channel
		if reterr != nil {
			select {
			case ch <- &cmdOutput{err: reterr}:
			case <-ctx.Done():
				return
			}
		}
	}()
	return ch, nil
}

// splitWriter mimics what the ssh.CombinedOutput command does, but stores the
// the stdout and stderr separately. This is slightly tricky because we don't
// want the combined output to be interleaved incorrectly. It creates sub writer
// structs which share the same lock and a shared output buffer.
type splitWriter struct {
	Stdout *wrapWriter
	Stderr *wrapWriter

	stdout      bytes.Buffer // just the stdout
	stderr      bytes.Buffer // just the stderr
	output      bytes.Buffer // combined output
	mutex       *sync.Mutex
	initialized bool // is this initialized?
}

// Init initializes the splitWriter.
func (obj *splitWriter) Init() {
	if obj.initialized {
		panic("splitWriter is already initialized")
	}
	obj.mutex = &sync.Mutex{}
	obj.Stdout = &wrapWriter{
		Mutex:  obj.mutex,
		Buffer: &obj.stdout,
		Output: &obj.output,
	}
	obj.Stderr = &wrapWriter{
		Mutex:  obj.mutex,
		Buffer: &obj.stderr,
		Output: &obj.output,
	}
	obj.initialized = true
}

// String returns the contents of the combined output buffer.
func (obj *splitWriter) String() string {
	if !obj.initialized {
		panic("splitWriter is not initialized")
	}
	return obj.output.String()
}

// wrapWriter is a simple writer which is used internally by splitWriter.
type wrapWriter struct {
	Mutex    *sync.Mutex
	Buffer   *bytes.Buffer // stdout or stderr
	Output   *bytes.Buffer // combined output
	Activity bool          // did we get any writes?
}

// Write writes to both bytes buffers with a parent lock to mix output safely.
func (obj *wrapWriter) Write(p []byte) (int, error) {
	// TODO: can we move the lock to only guard around the Output.Write ?
	obj.Mutex.Lock()
	defer obj.Mutex.Unlock()
	obj.Activity = true
	i, err := obj.Buffer.Write(p) // first write
	if err != nil {
		return i, err
	}
	return obj.Output.Write(p) // shared write
}

// String returns the contents of the unshared buffer.
func (obj *wrapWriter) String() string {
	return obj.Buffer.String()
}

// isNameValid checks that environment variable name is valid.
func isNameValid(varName string) error {
	if varName == "" {
		return fmt.Errorf("variable name cannot be an empty string")
	}
	for i := range varName {
		c := varName[i]
		if i == 0 && '0' <= c && c <= '9' {
			return fmt.Errorf("variable name cannot begin with number")
		}
		if !(c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
			return fmt.Errorf("invalid character in variable name")
		}
	}
	return nil
}
