m/p/cmd: implement RunCommand
This implements a new utility function RunCommand, based on existing
m/installer/test implementation.
RunCommand will be used in the upcoming metroctl test implementation.
Change-Id: Ieb98acada7e7408249da0e289861674e80b4d581
Reviewed-on: https://review.monogon.dev/c/monogon/+/789
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/installer/test/BUILD.bazel b/metropolis/installer/test/BUILD.bazel
index ef3c2c0..cd21861 100644
--- a/metropolis/installer/test/BUILD.bazel
+++ b/metropolis/installer/test/BUILD.bazel
@@ -23,7 +23,7 @@
"//metropolis/cli/metroctl/core",
"//metropolis/cli/pkg/datafile",
"//metropolis/node/build/mkimage/osimage",
- "//metropolis/pkg/logbuffer",
+ "//metropolis/pkg/cmd",
"//metropolis/proto/api",
"@com_github_diskfs_go_diskfs//:go-diskfs",
"@com_github_diskfs_go_diskfs//disk",
diff --git a/metropolis/installer/test/main.go b/metropolis/installer/test/main.go
index 9f38b4d..2a03a75 100644
--- a/metropolis/installer/test/main.go
+++ b/metropolis/installer/test/main.go
@@ -23,12 +23,9 @@
"bytes"
"context"
"fmt"
- "io"
"log"
"os"
- "os/exec"
"path/filepath"
- "strings"
"syscall"
"testing"
@@ -39,7 +36,7 @@
mctl "source.monogon.dev/metropolis/cli/metroctl/core"
"source.monogon.dev/metropolis/cli/pkg/datafile"
"source.monogon.dev/metropolis/node/build/mkimage/osimage"
- "source.monogon.dev/metropolis/pkg/logbuffer"
+ "source.monogon.dev/metropolis/pkg/cmd"
"source.monogon.dev/metropolis/proto/api"
)
@@ -54,14 +51,13 @@
nodeStorage string
)
-// runQemu starts a QEMU process and waits until it either finishes or the given
-// expectedOutput appears in a line emitted to stdout or stderr. It returns true
-// if it was found, false otherwise.
+// runQemu starts a new QEMU process, expecting the given output to appear
+// in any line printed. It returns true, if the expected string was found,
+// and false otherwise.
//
-// The qemu process will be killed when the context cancels or the function
-// exits.
+// QEMU is killed shortly after the string is found, or when the context is
+// cancelled.
func runQemu(ctx context.Context, args []string, expectedOutput string) (bool, error) {
- // Prepare the default parameter list.
defaultArgs := []string{
"-machine", "q35", "-accel", "kvm", "-nographic", "-nodefaults",
"-m", "512",
@@ -72,53 +68,8 @@
"-serial", "stdio",
"-no-reboot",
}
-
- // Make a sub-context to ensure that qemu exits when this function is done.
- ctxQ, ctxC := context.WithCancel(ctx)
- defer ctxC()
-
- // Join the parameter lists and prepare the Qemu command, but don't run it
- // just yet.
qemuArgs := append(defaultArgs, args...)
- qemuCmd := exec.CommandContext(ctxQ, "external/qemu/qemu-x86_64-softmmu", qemuArgs...)
-
- // Copy the stdout and stderr output to a single channel of lines so that they
- // can then be matched against expectedOutput.
-
- // Since LineBuffer can write its buffered contents on a deferred Close,
- // after the reader loop is broken, avoid deadlocks by making lineC a
- // buffered channel.
- lineC := make(chan string, 2)
- outBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
- lineC <- l.Data
- })
- defer outBuffer.Close()
- errBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
- lineC <- l.Data
- })
- defer errBuffer.Close()
-
- // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
- // allow easier debugging.
- qemuCmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
- qemuCmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
- if err := qemuCmd.Start(); err != nil {
- return false, fmt.Errorf("couldn't start QEMU: %w", err)
- }
-
- // Try matching against expectedOutput and return the result.
- for {
- select {
- case <-ctx.Done():
- return false, ctx.Err()
- case line := <-lineC:
- if strings.Contains(line, expectedOutput) {
- qemuCmd.Process.Kill()
- qemuCmd.Wait()
- return true, nil
- }
- }
- }
+ return cmd.RunCommand(ctx, "external/qemu/qemu-x86_64-softmmu", qemuArgs, expectedOutput)
}
// runQemuWithInstaller runs the Metropolis Installer in a qemu, performing the
diff --git a/metropolis/pkg/cmd/BUILD.bazel b/metropolis/pkg/cmd/BUILD.bazel
new file mode 100644
index 0000000..7d5bbeb
--- /dev/null
+++ b/metropolis/pkg/cmd/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "cmd",
+ srcs = ["run.go"],
+ importpath = "source.monogon.dev/metropolis/pkg/cmd",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//metropolis/pkg/logbuffer",
+ ],
+)
diff --git a/metropolis/pkg/cmd/run.go b/metropolis/pkg/cmd/run.go
new file mode 100644
index 0000000..d5690a1
--- /dev/null
+++ b/metropolis/pkg/cmd/run.go
@@ -0,0 +1,66 @@
+// Package cmd contains helpers that abstract away the chore of starting new
+// processes, tracking their lifetime, inspecting their output, etc.
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ "source.monogon.dev/metropolis/pkg/logbuffer"
+)
+
+// RunCommand starts a new process and waits until either its completion, or
+// appearance of expectedOutput in any line emitted by it. It returns true, if
+// expectedOutput was found, and false otherwise.
+//
+// The process will be killed both in the event the context is cancelled, and
+// when expectedOutput is found.
+func RunCommand(ctx context.Context, path string, args []string, expectedOutput string) (bool, error) {
+ // Make a sub-context to ensure the process exits when this function is done.
+ ctx, ctxC := context.WithCancel(ctx)
+ defer ctxC()
+
+ // Copy the stdout and stderr output to a single channel of lines so that they
+ // can then be matched against expectedOutput.
+
+ // Since LineBuffer can write its buffered contents on a deferred Close,
+ // after the reader loop is broken, avoid deadlocks by making lineC a
+ // buffered channel.
+ lineC := make(chan string, 2)
+ outBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
+ lineC <- l.Data
+ })
+ defer outBuffer.Close()
+ errBuffer := logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
+ lineC <- l.Data
+ })
+ defer errBuffer.Close()
+
+ // Prepare the command context, and start the process.
+ cmd := exec.CommandContext(ctx, path, args...)
+ // Tee std{out,err} into the linebuffers above and the process' std{out,err}, to
+ // allow easier debugging.
+ cmd.Stdout = io.MultiWriter(os.Stdout, outBuffer)
+ cmd.Stderr = io.MultiWriter(os.Stderr, errBuffer)
+ if err := cmd.Start(); err != nil {
+ return false, fmt.Errorf("couldn't start the process: %w", err)
+ }
+
+ // Try matching against expectedOutput and return the result.
+ for {
+ select {
+ case <-ctx.Done():
+ return false, ctx.Err()
+ case line := <-lineC:
+ if strings.Contains(line, expectedOutput) {
+ cmd.Process.Kill()
+ cmd.Wait()
+ return true, nil
+ }
+ }
+ }
+}