blob: 808a7c0f730ef61ca9ed60e3dd3c770eea530795 [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)
Serge Bazanskie012b722023-03-29 17:49:04 +020036 lineCB := func(l *logbuffer.Line) {
37 // If the context is canceled, no-one is listening on lineC anymore, so we would
38 // block.
39 select {
40 case <-ctx.Done():
41 return
42 case lineC <- l.Data:
43 }
44 }
45 outBuffer := logbuffer.NewLineBuffer(1024, lineCB)
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020046 defer outBuffer.Close()
Serge Bazanskie012b722023-03-29 17:49:04 +020047 errBuffer := logbuffer.NewLineBuffer(1024, lineCB)
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020048 defer errBuffer.Close()
49
50 // Prepare the command context, and start the process.
51 cmd := exec.CommandContext(ctx, path, args...)
52 // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
53 // allow easier debugging.
54 cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
55 cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
56 if err := cmd.Start(); err != nil {
57 return false, fmt.Errorf("couldn't start the process: %w", err)
58 }
59
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020060 // Handle the case in which the process finishes before pf takes the chance to
61 // kill it.
62 complC := make(chan error, 1)
63 go func() {
64 complC <- cmd.Wait()
65 }()
66
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020067 // Try matching against expectedOutput and return the result.
68 for {
69 select {
70 case <-ctx.Done():
71 return false, ctx.Err()
72 case line := <-lineC:
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020073 if pf(line) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020074 cmd.Process.Kill()
Serge Bazanskid20af4f2023-06-20 13:08:24 +020075 <-complC
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020076 return true, nil
77 }
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020078 case err := <-complC:
79 return false, err
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020080 }
81 }
82}
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020083
84// TerminateIfFound creates RunCommand predicates that instantly terminate
85// program execution in the event the given string is found in any line
86// produced. RunCommand will return true, if the string searched for was found,
Mateusz Zalegab838e052022-08-12 18:08:10 +020087// and false otherwise. If logf isn't nil, it will be called whenever a new
88// line is received.
89func TerminateIfFound(needle string, logf func(string)) func(string) bool {
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020090 return func(haystack string) bool {
Mateusz Zalegab838e052022-08-12 18:08:10 +020091 if logf != nil {
92 logf(haystack)
93 }
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020094 return strings.Contains(haystack, needle)
95 }
96}
Mateusz Zalegab838e052022-08-12 18:08:10 +020097
98// WaitUntilCompletion creates a RunCommand predicate that will make it wait
99// for the process to exit on its own. If logf isn't nil, it will be called
100// whenever a new line is received.
101func WaitUntilCompletion(logf func(string)) func(string) bool {
102 return func(line string) bool {
103 if logf != nil {
104 logf(line)
105 }
106 return false
107 }
108}