environment/exec: Seperate Exec and Wait

This commit is contained in:
Tom Wiesing 2022-12-14 08:53:45 +01:00
parent 2a308ee03c
commit a590d93e76
No known key found for this signature in database
14 changed files with 90 additions and 117 deletions

View file

@ -45,7 +45,7 @@ type Environment interface {
DialContext(context context.Context, network, address string) (net.Conn, error)
Executable() (string, error)
Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) int
Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) func() int
LookPathAbs(name string) (string, error)
}

View file

@ -14,6 +14,11 @@ import (
// This typically hints that the executable cannot be found, but may have other causes.
const ExecCommandError = 127
// ExecCommandErrorFunc always returns ExecCommandError.
func ExecCommandErrorFunc() int {
return ExecCommandError
}
// DefaultFilePerm is the default mode to use for files
const DefaultFilePerm fs.FileMode = 0666
@ -66,5 +71,5 @@ func ReadFile(env Environment, path string) ([]byte, error) {
// MustExec is like Exec, except that it returns true if the command exited successfully, and else false.
func MustExec(ctx context.Context, env Environment, io stream.IOStream, workdir string, exe string, argv ...string) bool {
return env.Exec(ctx, io, workdir, exe, argv...) == 0
return env.Exec(ctx, io, workdir, exe, argv...)() == 0
}

View file

@ -4,15 +4,18 @@ import (
"context"
"os/exec"
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
"github.com/rs/zerolog"
"github.com/tkw1536/goprogram/stream"
)
// Exec executes a system command with the specified input/output streams, working directory, and arguments.
//
// If the command executes, it's exit code will be returned.
// If the command can not be executed, returns [ExecCommandError].
func (*Native) Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) int {
// The command is started immediatly.
// The returned function is guaranteed to be non-nil and returns an exit code.
//
// If the command executes, the returns the exit code as soon as the process executes.
// If the command can not be executed, the returned function is [ExecCommandErrorFunc] and returns [ExecCommandError].
func (*Native) Exec(ctx context.Context, io stream.IOStream, workdir string, exe string, argv ...string) func() int {
// setup the command
cmd := exec.Command(exe, argv...)
cmd.Dir = workdir
@ -20,40 +23,53 @@ func (*Native) Exec(ctx context.Context, io stream.IOStream, workdir string, exe
cmd.Stdout = io.Stdout
cmd.Stderr = io.Stderr
// run the process in a cancelable fashion
err, cErr := cancel.WithContext(ctx, func(cancelable func()) error {
// start the process
err := cmd.Start()
if err != nil {
return err
}
// allow it to be cancellable
cancelable()
// and wait for the rest of the process
return cmd.Wait()
}, func() {
if cmd.Process != nil {
cmd.Process.Kill()
}
})
if err == nil {
err = cErr
// context is already cancelled, don't run it!
if err := ctx.Err(); err != nil {
return ExecCommandErrorFunc
}
// non-zero exit
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode()
}
// unknown error
// start the command, but if something happens, return nil
err := cmd.Start()
zerolog.Ctx(ctx).Debug().Str("exe", exe).Strs("argv", argv).Err(err).Msg("exec.Command.Start")
if err != nil {
return ExecCommandError
return ExecCommandErrorFunc
}
// everything is fine!
return 0
waitdone := make(chan struct{}) // closed once Wait() below returns
alldone := make(chan struct{}) // closed once the kill goroutine exits
go func() {
defer close(alldone)
select {
case <-ctx.Done():
err := cmd.Process.Kill()
zerolog.Ctx(ctx).Debug().Str("exe", exe).Strs("argv", argv).Err(err).Msg("exec.Command.Kill")
case <-waitdone:
}
}()
// create a new command
return func() int {
defer func() {
// wait for the goroutine to exit
close(waitdone)
<-alldone
}()
err := cmd.Wait()
zerolog.Ctx(ctx).Debug().Str("exe", exe).Strs("argv", argv).Err(err).Msg("exec.Command.Wait")
// non-zero exit
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode()
}
if err != nil {
return ExecCommandError
}
return 0
}
}
func (n *Native) LookPathAbs(file string) (string, error) {