blob: 486546368d34f9ea0d80e5bd3bc5acecac44e734 [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 codifier
import (
"bufio"
"context"
"fmt"
"log"
"os/exec"
"regexp"
"strings"
"sync"
)
const (
// ansiClearToEOL returns the console to the start of the line without issuing
// a linefeed. Note that this command requires that the output is sent to a
// shell; it won't work if the output is piped to stdout or elsewhere.
ansiClearToEOL = "\u001b[0K"
// When stderr lines are written on the same console line instead of being
// logged, what line width will be used to truncate the lines?
// TODO(gboone@): Consider using a terminal library to manage output.
maxLineWidth = 100
)
// runLoggedCommand runs a command, logging stdout with a ">" prefix and stderr
// with a ">>" prefix as each line is received. The command is run in the given
// directory, which may be relative to the home directory or absolute. Stdout is
// returned, unless it is of the form [\d+/\d+], a status line that is printed
// on the same line repeatedly, but not returned.
// TODO(gboone@): Update to use context.
func runLoggedCommand(ctx context.Context, dir, cmdStr string, args ...string) ([]string, error) {
dirPath, err := resolveGnPath("~", dir)
if err != nil {
return nil, fmt.Errorf("runLoggedCommand path err for dir %s: %w", dir, err)
}
command := cmdStr + " " + strings.Join(args, " ")
cmd := exec.CommandContext(ctx, cmdStr, args...)
cmd.Dir = dirPath
terminateChildProcessesAutomatically(cmd)
cmdStdoutReader, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("runLoggedCommand() error creating StdoutPipe for command %q: %w", command, err)
}
cmdStderrReader, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("runLoggedCommand() error creating StdoutPipe for command %q: %w", command, err)
}
var wg sync.WaitGroup
stdoutScanner := bufio.NewScanner(cmdStdoutReader)
var lines []string
wg.Add(1)
go func() {
var lastLineHadNoLF bool
re := regexp.MustCompile(`^\[\d+/\d+\]`)
for stdoutScanner.Scan() {
// If the line is of the form "[000/000] xxxxxx", then it's likely to be a
// status line such as the file listing for builds. These updates could
// fill the logs with thousands of lines of data. Instead, just output
// them to the screen on the same line as before.
if re.MatchString(stdoutScanner.Text()) {
statusLine := flattenString(stdoutScanner.Text(), maxLineWidth)
// Note fmt here, not log, so written to stdout.
fmt.Printf("\r%s%s", statusLine, ansiClearToEOL)
lastLineHadNoLF = true
} else {
line := fmt.Sprintf("> %s", stdoutScanner.Text())
lines = append(lines, line)
if lastLineHadNoLF {
line = "\n" + line
}
log.Print(line)
lastLineHadNoLF = false
}
}
wg.Done()
}()
stderrScanner := bufio.NewScanner(cmdStderrReader)
wg.Add(1)
go func() {
for stderrScanner.Scan() {
// Log error lines with >> instead of >.
log.Printf(">> %s", stderrScanner.Text())
}
wg.Done()
}()
log.Printf("executing command: %q in directory %q", command, dirPath)
if err = cmd.Start(); err != nil {
return nil, fmt.Errorf("runLoggedCommand() error while starting command %q: %w", command, err)
}
if err = cmd.Wait(); err != nil {
return nil, fmt.Errorf("runLoggedCommand() error while waiting for command %q: %w", command, err)
}
return lines, nil
}
// startCommand starts a command in the given directory. It returns the started
// exec.Cmd, two channels, and an error if there is a problem with the start.
// The channels are:
// • <-signalC, a channel that sends a nil each time the given keyString is seen
// or any errors while running
// • <-resultC, a channel that sends the final command output status (nil=exit
// code 0) or error
func startCommand(ctx context.Context, dir, keyString string, cmdStr string, args ...string) (*exec.Cmd, chan error, chan error, error) {
dirPath, err := resolveGnPath("~", dir)
if err != nil {
return nil, nil, nil, fmt.Errorf("startCommand path err for dir %s: %w", dir, err)
}
command := cmdStr + " " + strings.Join(args, " ")
cmd := exec.CommandContext(ctx, cmdStr, args...)
cmd.Dir = dirPath
terminateChildProcessesAutomatically(cmd)
cmdStdoutReader, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, nil, fmt.Errorf("startCommand() error creating StdoutPipe for command %q: %w", command, err)
}
cmdStderrReader, err := cmd.StderrPipe()
if err != nil {
return nil, nil, nil, fmt.Errorf("startCommand() error creating StdoutPipe for command %q: %w", command, err)
}
stdoutScanner := bufio.NewScanner(cmdStdoutReader)
signalC := make(chan error, 1)
go func() {
for stdoutScanner.Scan() {
if strings.Contains(stdoutScanner.Text(), keyString) {
signalC <- nil // Send a nil to indicate keyString found.
}
}
}()
stderrScanner := bufio.NewScanner(cmdStderrReader)
go func() {
for stderrScanner.Scan() {
signalC <- fmt.Errorf(stderrScanner.Text())
}
}()
log.Printf("startCommand() executing %q in directory %q", command, dirPath)
err = cmd.Start()
if err != nil {
return nil, nil, nil, fmt.Errorf("startCommand() error while starting command: %w", err)
}
log.Printf("startCommand() started %q", command)
// When the command completes, send the result via the result channel.
resultC := make(chan error, 1)
go func() {
resultC <- cmd.Wait()
}()
return cmd, signalC, resultC, nil
}