Files
mesh-sdk/go/forgekey/runner.go
T

78 lines
2.3 KiB
Go

package forgekey
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
// Ran is what a command did: its output, its exit status, and why it never ran to an answer.
type Ran struct {
Stdout string
Stderr string
Status int
// Err is "ENOENT" when the program is not installed, or says it was ended for taking too long.
Err string
}
// Runner runs one command with extra environment words, so every tool can be tested without ssh.
type Runner func(ctx context.Context, env []string, name string, args ...string) Ran
// CallTimeout bounds one command of ExecRunner: below the runtime's thirty-second call limit. A variable so a
// test can lower it.
var CallTimeout = 20 * time.Second
// ExecRunner runs a command on this machine, with no terminal and nothing on its stdin, bounded by
// CallTimeout.
func ExecRunner(ctx context.Context, env []string, name string, args ...string) Ran {
return ExecRunnerFor(CallTimeout)(ctx, env, name, args...)
}
// ExecRunnerFor is ExecRunner with its own bound (issue 1497): a build module's long step passes 0, so only the
// step's own context ends a command, and a fetch may outlast a tool call's thirty seconds.
func ExecRunnerFor(bound time.Duration) Runner {
return func(ctx context.Context, env []string, name string, args ...string) Ran {
return execRun(ctx, bound, env, name, args...)
}
}
func execRun(ctx context.Context, bound time.Duration, env []string, name string, args ...string) Ran {
if bound > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, bound)
defer cancel()
}
cmd := exec.CommandContext(ctx, name, args...)
cmd.Env = append(os.Environ(), env...)
var out, errb bytes.Buffer
cmd.Stdout, cmd.Stderr = &out, &errb
err := cmd.Run()
r := Ran{Stdout: out.String(), Stderr: errb.String()}
var exitErr *exec.ExitError
switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded):
r.Status, r.Err = 124, fmt.Sprintf("no answer within %d s", int(bound/time.Second))
case errors.Is(err, exec.ErrNotFound):
r.Status, r.Err = 127, "ENOENT"
case errors.As(err, &exitErr):
r.Status = exitErr.ExitCode()
case err != nil:
r.Status, r.Err = 1, err.Error()
}
return r
}
func firstLine(s string) string {
for _, l := range strings.Split(s, "\n") {
if l = strings.TrimSpace(l); l != "" {
return l
}
}
return ""
}