blob: d887192ea4a10e96f881682d0f095632906ad919 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
)
// program represents an external program by its executable path.
type program struct {
path string
}
// findProgram searches the directories in bin for the named program. It returns
// an error if the program cannot be found.
func findProgram(name string, bin []string) (program, error) {
path, err := findFile(name, bin)
return program{path}, err
}
// findFile searches the directories in dirs for the file with the given name.
// It returns the full path, or an error if the file cannot be found.
func findFile(name string, dirs []string) (string, error) {
for _, dir := range dirs {
path := filepath.Join(dir, name)
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("cannot find %s (searched in %s)", name, strings.Join(dirs, ", "))
}
/// runResult stores the result of running a program.
type runResult struct {
// True if the exit code is zero, false otherwise.
success bool
// Standard output.
stdout string
// Standard error.
stderr string
}
// normalErrorExitCode is the exit code indicating that a program failed in a
// normal or expected way. For example, fidlc exits with this code when it finds
// a syntax error. We conservatively assume this is the only nonzero exit code
// occurring under normal conditions, and that all others indicate an abnormal
// error, e.g. a segfault. This ensures that we never expose abnormal failures
// to the user. Instead, we log them and return 500 Internal Server Error.
//
// We choose the value 1 because this is used for general errors:
// https://fuchsia.dev/fuchsia-src/concepts/api/cli#execution_success_and_failure
// The rubric allows other exit codes too, so we might need to expand this in
// the future if fidlbolt uses programs that exit with codes besides 0 and 1.
const normalErrorExitCode = 1
// run runs a program with arguments. If it fails to launch or exits with a code
// other than 0 or normalErrorExitCode, returns an error. If ctx is cancelled
// while the program is running, stops the program and returns an error.
func (p program) run(ctx context.Context, arg ...string) (runResult, error) {
cmd := exec.CommandContext(ctx, p.path, arg...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
switch err := cmd.Run().(type) {
case nil:
return runResult{
success: true,
stdout: stdout.String(),
stderr: stderr.String(),
}, nil
case *exec.ExitError:
if err.ExitCode() != normalErrorExitCode {
return runResult{}, p.augmentError(err, arg, stdout.String(), stderr.String())
}
return runResult{
success: false,
stdout: stdout.String(),
stderr: stderr.String(),
}, nil
default:
return runResult{}, err
}
}
// runInfallible is like run, but for programs that are not expected to fail. It
// returns an error for any nonzero exit code, including normalErrorExitCode.
func (p program) runInfallible(ctx context.Context, arg ...string) (runResult, error) {
cmd := exec.CommandContext(ctx, p.path, arg...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
switch err := cmd.Run().(type) {
case nil:
return runResult{
success: true,
stdout: stdout.String(),
stderr: stderr.String(),
}, nil
case *exec.ExitError:
return runResult{}, p.augmentError(err, arg, stdout.String(), stderr.String())
default:
return runResult{}, err
}
}
// augmentError adds diagnostic information to err.
func (p program) augmentError(err *exec.ExitError, args []string, stdout, stderr string) error {
return fmt.Errorf(
`%v
program: %v
args: %v
exit code: %d
--- begin stdout ---
%s
--- end stdout ---
--- begin stderr ---
%s
--- end stderr ---`, err, p.path, args, err.ExitCode(), stdout, stderr)
}
// A tempDir is temporary directory.
type tempDir struct {
path string
}
// newTempDir creates a new temporary directory.
func newTempDir() (tempDir, error) {
path, err := ioutil.TempDir("", "fidlbolt-")
return tempDir{path}, err
}
// join returns a path inside the temporary directory.
func (d tempDir) join(elem ...string) string {
elem = append([]string{d.path}, elem...)
return filepath.Join(elem...)
}
// createFile creates a file in the temporary directory, writes content to it,
// and returns its path.
func (d tempDir) createFile(name, content string) (string, error) {
f, err := os.Create(d.join(name))
if err != nil {
return "", err
}
defer f.Close()
if _, err = f.WriteString(content); err != nil {
return "", err
}
return f.Name(), nil
}
// readFile creates a response from a file in the temporary directory.
func (d tempDir) readFile(name string) (string, error) {
b, err := ioutil.ReadFile(d.join(name))
if err != nil {
return "", err
}
return string(b), nil
}