blob: 7591d5619b44ce8c6cbdbdc7a953819c42d866ab [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
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020017// until the supplied predicate function returns true. The function is called
18// for each line produced by the new process.
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020019//
20// The process will be killed both in the event the context is cancelled, and
21// when expectedOutput is found.
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020022func RunCommand(ctx context.Context, path string, args []string, pf func(string) bool) (bool, error) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020023 // 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:
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020059 if pf(line) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020060 cmd.Process.Kill()
61 cmd.Wait()
62 return true, nil
63 }
64 }
65 }
66}
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020067
68// TerminateIfFound creates RunCommand predicates that instantly terminate
69// program execution in the event the given string is found in any line
70// produced. RunCommand will return true, if the string searched for was found,
71// and false otherwise.
72func TerminateIfFound(needle string) func(string) bool {
73 return func(haystack string) bool {
74 return strings.Contains(haystack, needle)
75 }
76}