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