blob: 9649c0319f4bc1963ae5b34606d83b920234a223 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Mateusz Zalegaf1234a92022-06-22 13:57:38 +02004// Package cmd contains helpers that abstract away the chore of starting new
5// processes, tracking their lifetime, inspecting their output, etc.
6package cmd
7
8import (
9 "context"
10 "fmt"
11 "io"
12 "os"
13 "os/exec"
14 "strings"
15
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020016 "source.monogon.dev/osbase/logbuffer"
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020017)
18
19// RunCommand starts a new process and waits until either its completion, or
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020020// until the supplied predicate function pf returns true. The function is called
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020021// for each line produced by the new process.
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020022//
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020023// The returned boolean value equals the last value returned by pf.
24//
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020025// The process will be killed both in the event the context is cancelled, and
26// when expectedOutput is found.
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020027func RunCommand(ctx context.Context, path string, args []string, pf func(string) bool) (bool, error) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020028 // Make a sub-context to ensure the process exits when this function is done.
29 ctx, ctxC := context.WithCancel(ctx)
30 defer ctxC()
31
32 // Copy the stdout and stderr output to a single channel of lines so that they
33 // can then be matched against expectedOutput.
34
35 // Since LineBuffer can write its buffered contents on a deferred Close,
36 // after the reader loop is broken, avoid deadlocks by making lineC a
37 // buffered channel.
38 lineC := make(chan string, 2)
Serge Bazanskie012b722023-03-29 17:49:04 +020039 lineCB := func(l *logbuffer.Line) {
40 // If the context is canceled, no-one is listening on lineC anymore, so we would
41 // block.
42 select {
43 case <-ctx.Done():
44 return
45 case lineC <- l.Data:
46 }
47 }
48 outBuffer := logbuffer.NewLineBuffer(1024, lineCB)
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020049 defer outBuffer.Close()
Serge Bazanskie012b722023-03-29 17:49:04 +020050 errBuffer := logbuffer.NewLineBuffer(1024, lineCB)
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020051 defer errBuffer.Close()
52
53 // Prepare the command context, and start the process.
54 cmd := exec.CommandContext(ctx, path, args...)
55 // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
56 // allow easier debugging.
57 cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
58 cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
59 if err := cmd.Start(); err != nil {
60 return false, fmt.Errorf("couldn't start the process: %w", err)
61 }
62
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020063 // Handle the case in which the process finishes before pf takes the chance to
64 // kill it.
65 complC := make(chan error, 1)
66 go func() {
67 complC <- cmd.Wait()
68 }()
69
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020070 // Try matching against expectedOutput and return the result.
71 for {
72 select {
73 case <-ctx.Done():
74 return false, ctx.Err()
75 case line := <-lineC:
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020076 if pf(line) {
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020077 cmd.Process.Kill()
Serge Bazanskid20af4f2023-06-20 13:08:24 +020078 <-complC
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020079 return true, nil
80 }
Mateusz Zalega2f7790d2022-08-06 16:10:42 +020081 case err := <-complC:
82 return false, err
Mateusz Zalegaf1234a92022-06-22 13:57:38 +020083 }
84 }
85}
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020086
87// TerminateIfFound creates RunCommand predicates that instantly terminate
88// program execution in the event the given string is found in any line
89// produced. RunCommand will return true, if the string searched for was found,
Mateusz Zalegab838e052022-08-12 18:08:10 +020090// and false otherwise. If logf isn't nil, it will be called whenever a new
91// line is received.
92func TerminateIfFound(needle string, logf func(string)) func(string) bool {
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020093 return func(haystack string) bool {
Mateusz Zalegab838e052022-08-12 18:08:10 +020094 if logf != nil {
95 logf(haystack)
96 }
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020097 return strings.Contains(haystack, needle)
98 }
99}
Mateusz Zalegab838e052022-08-12 18:08:10 +0200100
101// WaitUntilCompletion creates a RunCommand predicate that will make it wait
102// for the process to exit on its own. If logf isn't nil, it will be called
103// whenever a new line is received.
104func WaitUntilCompletion(logf func(string)) func(string) bool {
105 return func(line string) bool {
106 if logf != nil {
107 logf(line)
108 }
109 return false
110 }
111}