blob: 9f554b5033c03c6c46562f35a82830f4dee89f81 [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 Zalega2f7790d2022-08-06 16:10:42 +020017// until the supplied predicate function pf returns true. The function is called
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020018// for each line produced by the new process.
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020019//
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020020// The returned boolean value equals the last value returned by pf.
21//
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020022// The process will be killed both in the event the context is cancelled, and
23// when expectedOutput is found.
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020024func RunCommand(ctx context.Context, path string, args []string, pf func(string) bool) (bool, error) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020025 // Make a sub-context to ensure the process exits when this function is done.
26 ctx, ctxC := context.WithCancel(ctx)
27 defer ctxC()
28
29 // Copy the stdout and stderr output to a single channel of lines so that they
30 // can then be matched against expectedOutput.
31
32 // Since LineBuffer can write its buffered contents on a deferred Close,
33 // after the reader loop is broken, avoid deadlocks by making lineC a
34 // buffered channel.
35 lineC := make(chan string, 2)
36 outBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
37 lineC <- l.Data
38 })
39 defer outBuffer.Close()
40 errBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
41 lineC <- l.Data
42 })
43 defer errBuffer.Close()
44
45 // Prepare the command context, and start the process.
46 cmd := exec.CommandContext(ctx, path, args...)
47 // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
48 // allow easier debugging.
49 cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
50 cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
51 if err := cmd.Start(); err != nil {
52 return false, fmt.Errorf("couldn't start the process: %w", err)
53 }
54
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020055 // Handle the case in which the process finishes before pf takes the chance to
56 // kill it.
57 complC := make(chan error, 1)
58 go func() {
59 complC <- cmd.Wait()
60 }()
61
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020062 // Try matching against expectedOutput and return the result.
63 for {
64 select {
65 case <-ctx.Done():
66 return false, ctx.Err()
67 case line := <-lineC:
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020068 if pf(line) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020069 cmd.Process.Kill()
70 cmd.Wait()
71 return true, nil
72 }
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020073 case err := <-complC:
74 return false, err
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020075 }
76 }
77}
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020078
79// TerminateIfFound creates RunCommand predicates that instantly terminate
80// program execution in the event the given string is found in any line
81// produced. RunCommand will return true, if the string searched for was found,
Mateusz Zalegab838e052022-08-12 18:08:10 +020082// and false otherwise. If logf isn't nil, it will be called whenever a new
83// line is received.
84func TerminateIfFound(needle string, logf func(string)) func(string) bool {
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020085 return func(haystack string) bool {
Mateusz Zalegab838e052022-08-12 18:08:10 +020086 if logf != nil {
87 logf(haystack)
88 }
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020089 return strings.Contains(haystack, needle)
90 }
91}
Mateusz Zalegab838e052022-08-12 18:08:10 +020092
93// WaitUntilCompletion creates a RunCommand predicate that will make it wait
94// for the process to exit on its own. If logf isn't nil, it will be called
95// whenever a new line is received.
96func WaitUntilCompletion(logf func(string)) func(string) bool {
97 return func(line string) bool {
98 if logf != nil {
99 logf(line)
100 }
101 return false
102 }
103}