github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/k8s/client/cmd.go (about)

     1  package client
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"io"
     8  	"os/exec"
     9  	"strings"
    10  
    11  	"github.com/rs/zerolog/log"
    12  )
    13  
    14  func ExecCmd(command string) error {
    15  	return ExecCmdWithContext(context.Background(), command)
    16  }
    17  
    18  func ExecCmdWithContext(ctx context.Context, command string) error {
    19  	return ExecCmdWithOptions(ctx, command, func(m string) {
    20  		log.Debug().Str("Text", m).Msg("Std Pipe")
    21  	})
    22  }
    23  
    24  // readStdPipe continuously reads from a given pipe (either stdout or stderr)
    25  // and processes the output line by line using the provided outputFunction.
    26  // It handles lines of any length dynamically without the need for a large predefined buffer.
    27  func readStdPipe(pipe io.ReadCloser, outputFunction func(string)) {
    28  	reader := bufio.NewReader(pipe)
    29  	var output []rune
    30  
    31  	for {
    32  		// ReadLine tries to return a single line, not including the end-of-line bytes.
    33  		// The returned line may be incomplete if the line's too long for the buffer.
    34  		// isPrefix will be true if the line is longer than the buffer.
    35  		chunk, isPrefix, err := reader.ReadLine()
    36  
    37  		// Handle any errors that occurred during the read.
    38  		if err != nil {
    39  			// Log any error that's not an EOF (end of file).
    40  			if err != io.EOF {
    41  				log.Warn().Err(err).Msg("Error while reading standard pipe, this can be caused by really long logs and can be ignored if nothing else is wrong.")
    42  			}
    43  			break
    44  		}
    45  
    46  		// Append the chunk to the output buffer.
    47  		// bytes.Runes converts the byte slice to a slice of runes, handling multi-byte characters.
    48  		output = append(output, bytes.Runes(chunk)...)
    49  
    50  		// If isPrefix is false, we've reached the end of the line and can process it.
    51  		if !isPrefix {
    52  			// Call the output function with the complete line if it's defined.
    53  			if outputFunction != nil {
    54  				outputFunction(string(output))
    55  			}
    56  			// Reset output to an empty slice for reading the next line.
    57  			output = output[:0]
    58  		}
    59  	}
    60  }
    61  
    62  func ExecCmdWithOptions(ctx context.Context, command string, outputFunction func(string)) error {
    63  	c := strings.Split(command, " ")
    64  	cmd := exec.CommandContext(ctx, c[0], c[1:]...) // #nosec: G204
    65  	stderr, err := cmd.StderrPipe()
    66  	if err != nil {
    67  		return err
    68  	}
    69  	stdout, err := cmd.StdoutPipe()
    70  	if err != nil {
    71  		return err
    72  	}
    73  	if err := cmd.Start(); err != nil {
    74  		return err
    75  	}
    76  	go readStdPipe(stderr, outputFunction)
    77  	go readStdPipe(stdout, outputFunction)
    78  	return cmd.Wait()
    79  }