package main 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: below the runtime's thirty-second call limit. const 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 { ctx, cancel := context.WithTimeout(ctx, CallTimeout) 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(CallTimeout/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 "" }