|  | // Package cmd contains helpers that abstract away the chore of starting new | 
|  | // processes, tracking their lifetime, inspecting their output, etc. | 
|  | package cmd | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "fmt" | 
|  | "io" | 
|  | "os" | 
|  | "os/exec" | 
|  | "strings" | 
|  |  | 
|  | "source.monogon.dev/metropolis/pkg/logbuffer" | 
|  | ) | 
|  |  | 
|  | // RunCommand starts a new process and waits until either its completion, or | 
|  | // until the supplied predicate function pf returns true. The function is called | 
|  | // for each line produced by the new process. | 
|  | // | 
|  | // The returned boolean value equals the last value returned by pf. | 
|  | // | 
|  | // The process will be killed both in the event the context is cancelled, and | 
|  | // when expectedOutput is found. | 
|  | func RunCommand(ctx context.Context, path string, args []string, pf func(string) bool) (bool, error) { | 
|  | // Make a sub-context to ensure the process exits when this function is done. | 
|  | ctx, ctxC := context.WithCancel(ctx) | 
|  | defer ctxC() | 
|  |  | 
|  | // Copy the stdout and stderr output to a single channel of lines so that they | 
|  | // can then be matched against expectedOutput. | 
|  |  | 
|  | // Since LineBuffer can write its buffered contents on a deferred Close, | 
|  | // after the reader loop is broken, avoid deadlocks by making lineC a | 
|  | // buffered channel. | 
|  | lineC := make(chan string, 2) | 
|  | outBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) { | 
|  | lineC <- l.Data | 
|  | }) | 
|  | defer outBuffer.Close() | 
|  | errBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) { | 
|  | lineC <- l.Data | 
|  | }) | 
|  | defer errBuffer.Close() | 
|  |  | 
|  | // Prepare the command context, and start the process. | 
|  | cmd := exec.CommandContext(ctx, path, args...) | 
|  | // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to | 
|  | // allow easier debugging. | 
|  | cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer) | 
|  | cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer) | 
|  | if err := cmd.Start(); err != nil { | 
|  | return false, fmt.Errorf("couldn't start the process: %w", err) | 
|  | } | 
|  |  | 
|  | // Handle the case in which the process finishes before pf takes the chance to | 
|  | // kill it. | 
|  | complC := make(chan error, 1) | 
|  | go func() { | 
|  | complC <- cmd.Wait() | 
|  | }() | 
|  |  | 
|  | // Try matching against expectedOutput and return the result. | 
|  | for { | 
|  | select { | 
|  | case <-ctx.Done(): | 
|  | return false, ctx.Err() | 
|  | case line := <-lineC: | 
|  | if pf(line) { | 
|  | cmd.Process.Kill() | 
|  | cmd.Wait() | 
|  | return true, nil | 
|  | } | 
|  | case err := <-complC: | 
|  | return false, err | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TerminateIfFound creates RunCommand predicates that instantly terminate | 
|  | // program execution in the event the given string is found in any line | 
|  | // produced. RunCommand will return true, if the string searched for was found, | 
|  | // and false otherwise. If logf isn't nil, it will be called whenever a new | 
|  | // line is received. | 
|  | func TerminateIfFound(needle string, logf func(string)) func(string) bool { | 
|  | return func(haystack string) bool { | 
|  | if logf != nil { | 
|  | logf(haystack) | 
|  | } | 
|  | return strings.Contains(haystack, needle) | 
|  | } | 
|  | } | 
|  |  | 
|  | // WaitUntilCompletion creates a RunCommand predicate that will make it wait | 
|  | // for the process to exit on its own. If logf isn't nil, it will be called | 
|  | // whenever a new line is received. | 
|  | func WaitUntilCompletion(logf func(string)) func(string) bool { | 
|  | return func(line string) bool { | 
|  | if logf != nil { | 
|  | logf(line) | 
|  | } | 
|  | return false | 
|  | } | 
|  | } |