blob: d5690a11723c91aae67c142ca482536c4f897b57 [file] [log] [blame]
Mateusz Zalegaf1234a92022-06-22 13:57:38 +02001// Package cmd contains helpers that abstract away the chore of starting new
2// processes, tracking their lifetime, inspecting their output, etc.
3package cmd
4
5import (
6 "context"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "strings"
12
13 "source.monogon.dev/metropolis/pkg/logbuffer"
14)
15
16// RunCommand starts a new process and waits until either its completion, or
17// appearance of expectedOutput in any line emitted by it. It returns true, if
18// expectedOutput was found, and false otherwise.
19//
20// The process will be killed both in the event the context is cancelled, and
21// when expectedOutput is found.
22func RunCommand(ctx context.Context, path string, args []string, expectedOutput string) (bool, error) {
23 // Make a sub-context to ensure the process exits when this function is done.
24 ctx, ctxC := context.WithCancel(ctx)
25 defer ctxC()
26
27 // Copy the stdout and stderr output to a single channel of lines so that they
28 // can then be matched against expectedOutput.
29
30 // Since LineBuffer can write its buffered contents on a deferred Close,
31 // after the reader loop is broken, avoid deadlocks by making lineC a
32 // buffered channel.
33 lineC := make(chan string, 2)
34 outBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
35 lineC <- l.Data
36 })
37 defer outBuffer.Close()
38 errBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
39 lineC <- l.Data
40 })
41 defer errBuffer.Close()
42
43 // Prepare the command context, and start the process.
44 cmd := exec.CommandContext(ctx, path, args...)
45 // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
46 // allow easier debugging.
47 cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
48 cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
49 if err := cmd.Start(); err != nil {
50 return false, fmt.Errorf("couldn't start the process: %w", err)
51 }
52
53 // Try matching against expectedOutput and return the result.
54 for {
55 select {
56 case <-ctx.Done():
57 return false, ctx.Err()
58 case line := <-lineC:
59 if strings.Contains(line, expectedOutput) {
60 cmd.Process.Kill()
61 cmd.Wait()
62 return true, nil
63 }
64 }
65 }
66}