
Remove 'blender' from a variable name, since this is actually generic code and not Blender-specific.
137 lines
3.8 KiB
Go
137 lines
3.8 KiB
Go
package cli_runner
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// The buffer size used to read stdout/stderr output from subprocesses, in
|
|
// bytes. Effectively this determines the maximum line length that can be
|
|
// handled in one go. Lines that are longer will be broken up.
|
|
const StdoutBufferSize = 40 * 1024
|
|
|
|
// CLIRunner is a wrapper around exec.CommandContext() to allow mocking.
|
|
type CLIRunner struct {
|
|
}
|
|
|
|
func NewCLIRunner() *CLIRunner {
|
|
return &CLIRunner{}
|
|
}
|
|
|
|
func (cli *CLIRunner) CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
return exec.CommandContext(ctx, name, arg...)
|
|
}
|
|
|
|
// RunWithTextOutput runs a command and sends its output line-by-line to the
|
|
// lineChannel. Stdout and stderr are combined.
|
|
// Before returning. RunWithTextOutput() waits for the subprocess, to ensure it
|
|
// doesn't become defunct.
|
|
func (cli *CLIRunner) RunWithTextOutput(
|
|
ctx context.Context,
|
|
logger zerolog.Logger,
|
|
execCmd *exec.Cmd,
|
|
logChunker LogChunker,
|
|
lineChannel chan<- string,
|
|
) error {
|
|
outPipe, err := execCmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
execCmd.Stderr = execCmd.Stdout // Redirect stderr to stdout.
|
|
|
|
if err := execCmd.Start(); err != nil {
|
|
logger.Error().Err(err).Msg("error starting CLI execution")
|
|
return err
|
|
}
|
|
|
|
subprocPID := execCmd.Process.Pid
|
|
logger = logger.With().Int("pid", subprocPID).Logger()
|
|
|
|
reader := bufio.NewReaderSize(outPipe, StdoutBufferSize)
|
|
|
|
// returnErr determines which error is returned to the caller. More important
|
|
// errors overwrite less important ones. This is done via a variable instead
|
|
// of simply returning, because the function must be run to completion in
|
|
// order to wait for processes (and not create defunct ones).
|
|
var returnErr error = nil
|
|
|
|
// If a line longer than our buffer is received, it will be trimmed to the
|
|
// bufffer length. This means that it may not end on a valid character
|
|
// boundary. Any leftover bytes are collected here, and prepended to the next
|
|
// line.
|
|
leftovers := []byte{}
|
|
readloop:
|
|
for {
|
|
lineBytes, isPrefix, readErr := reader.ReadLine()
|
|
|
|
switch {
|
|
case readErr == io.EOF:
|
|
break readloop
|
|
case readErr != nil:
|
|
logger.Error().Err(err).Msg("error reading stdout/err")
|
|
returnErr = readErr
|
|
break readloop
|
|
}
|
|
|
|
// Prepend any leftovers from the previous line to the received bytes.
|
|
if len(leftovers) > 0 {
|
|
lineBytes = append(leftovers, lineBytes...)
|
|
leftovers = []byte{}
|
|
}
|
|
// Make sure long lines are broken on character boundaries.
|
|
lineBytes, leftovers = splitOnCharacterBoundary(lineBytes)
|
|
|
|
line := string(lineBytes)
|
|
if isPrefix {
|
|
prefix := []rune(line)
|
|
if len(prefix) > 256 {
|
|
prefix = prefix[:256]
|
|
}
|
|
logger.Warn().
|
|
Str("line", fmt.Sprintf("%s...", string(prefix))).
|
|
Int("bytesRead", len(lineBytes)).
|
|
Msg("unexpectedly long line read, will be split up")
|
|
}
|
|
|
|
logger.Debug().Msg(line)
|
|
if lineChannel != nil {
|
|
lineChannel <- line
|
|
}
|
|
|
|
if err := logChunker.Append(ctx, fmt.Sprintf("pid=%d > %s", subprocPID, line)); err != nil {
|
|
returnErr = fmt.Errorf("appending log entry to log chunker: %w", err)
|
|
break readloop
|
|
}
|
|
}
|
|
|
|
if err := logChunker.Flush(ctx); err != nil {
|
|
// any readErr is less important, as these are likely caused by other
|
|
// issues, which will surface on the Wait() and Success() calls.
|
|
returnErr = fmt.Errorf("flushing log chunker: %w", err)
|
|
}
|
|
|
|
if err := execCmd.Wait(); err != nil {
|
|
logger.Error().
|
|
Int("exitCode", execCmd.ProcessState.ExitCode()).
|
|
Msg("command exited abnormally")
|
|
returnErr = fmt.Errorf("command exited abnormally with code %d", execCmd.ProcessState.ExitCode())
|
|
}
|
|
|
|
if returnErr != nil {
|
|
logger.Error().Err(err).
|
|
Int("exitCode", execCmd.ProcessState.ExitCode()).
|
|
Msg("command exited abnormally")
|
|
return returnErr
|
|
}
|
|
|
|
logger.Info().Msg("command exited succesfully")
|
|
return nil
|
|
}
|