| // 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 |
| } |