blob: b86ca3c2352777d7572238ca99c198a344d03b25 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package emulator
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/lib/jsonutil"
"go.fuchsia.dev/fuchsia/tools/qemu"
"go.fuchsia.dev/fuchsia/tools/virtual_device"
fvdpb "go.fuchsia.dev/fuchsia/tools/virtual_device/proto"
)
// Untar untars a tar.gz file into a directory.
func untar(dst string, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gz.Close()
for tr := tar.NewReader(gz); ; {
header, err := tr.Next()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
path := filepath.Join(dst, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err := os.MkdirAll(path, info.Mode()); err != nil {
return err
}
} else {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode())
if err != nil {
return err
}
_, err = io.Copy(f, tr)
f.Close()
if err != nil {
return err
}
}
}
}
// Distribution is a collection of QEMU-related artifacts.
//
// Delete must be called once done with it.
type Distribution struct {
testDataDir string
unpackedPath string
Emulator Emulator
}
// Arch is the architecture to emulate.
type Arch string
const (
X64 Arch = "x64"
Arm64 Arch = "arm64"
)
// Emulator is the emulator to use.
type Emulator int
const (
Qemu Emulator = iota
Femu
)
// Disk represents a single disk that will be attached to the virtual machine.
type Disk struct {
Path string
USB bool
}
// Instance is a live emulator instance.
type Instance struct {
cmd *exec.Cmd
piped *exec.Cmd
stdin *bufio.Writer
stdout *bufio.Reader
stderr *bufio.Reader
emulator Emulator
logDestination io.Writer
}
// DistributionParams is passed to UnpackFrom().
type DistributionParams struct {
Emulator Emulator
}
// buildInfo carries information about the Fuchsia build.
//
// This is read from a file generated by BUILD.gn.
type buildInfo struct {
// The path to the image.json manifest produced by the build.
//
// This path is relative to the file from which this struct was read.
ImageManifestPath string
// The build's target CPU architecture.
TargetCPU string
}
// DefaultVirtualDevice returns a virtual device configuration for testing.
//
// The returned virtual device is compatible with the image manifest produced by a Fuchsia
// build for the specified architecture.
func DefaultVirtualDevice(arch string) *fvdpb.VirtualDevice {
var arch_kernel_args []string
// legacy is only supported for x64 arch.
if arch == "x64" {
arch_kernel_args = append(arch_kernel_args, "kernel.serial=legacy")
}
return &fvdpb.VirtualDevice{
Name: "default",
Kernel: "qemu-kernel",
Initrd: "zircon-a",
Hw: &fvdpb.HardwareProfile{
Arch: arch,
Mac: "52:54:00:63:5e:7a",
Ram: "8G",
CpuCount: 8,
EnableKvm: arch == "x64",
},
KernelArgs: append(
arch_kernel_args,
"kernel.entropy-mixin=1420bb81dc0396b37cc2d0aa31bb2785dadaf9473d0780ecee1751afb5867564",
"kernel.halt-on-panic=true",
// Disable lockup detector heartbeats. In emulated environments, virtualized
// CPUs may be starved or fail to execute in a timely fashion, resulting in
// apparent lockups. See https://fxbug.dev/42144748.
"kernel.lockup-detector.heartbeat-period-ms=0",
"kernel.lockup-detector.heartbeat-age-threshold-ms=0",
),
}
}
// UnpackFrom unpacks the emulator distribution.
//
// path is the path to host_x64/test_data containing the emulator.
// emulator is the emulator to unpack.
func UnpackFrom(path string, distroParams DistributionParams) (*Distribution, error) {
// Since the emulator will be started from a different directory, make the base path
// absolute.
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}
emulator_file := "qemu.tar.gz"
if distroParams.Emulator == Femu {
emulator_file = "femu.tar.gz"
}
archivePath := filepath.Join(path, "emulator", emulator_file)
unpackedPath, err := os.MkdirTemp("", "emulator-distro")
if err != nil {
return nil, err
}
if err = untar(unpackedPath, archivePath); err != nil {
os.RemoveAll(unpackedPath)
return nil, err
}
return &Distribution{
testDataDir: path,
unpackedPath: unpackedPath,
Emulator: distroParams.Emulator,
}, nil
}
// Delete removes the emulator-related artifacts.
func (d *Distribution) Delete() error {
return os.RemoveAll(d.unpackedPath)
}
func (d *Distribution) systemPath(arch Arch) string {
if d.Emulator == Femu {
// FEMU has one binary for all arches.
return filepath.Join(d.unpackedPath, "emulator")
}
switch arch {
case X64:
return filepath.Join(d.unpackedPath, "bin", "qemu-system-x86_64")
case Arm64:
return filepath.Join(d.unpackedPath, "bin", "qemu-system-aarch64")
}
return ""
}
// TargetCPU returns the target CPU used by the build that produced this library.
func (d *Distribution) TargetCPU() (Arch, error) {
buildinfo, err := d.loadBuildInfo()
if err != nil {
return X64, err
}
switch buildinfo.TargetCPU {
case "x64":
return X64, nil
case "arm64":
return Arm64, nil
}
return X64, fmt.Errorf("unknown target CPU: %s", buildinfo.TargetCPU)
}
func (d *Distribution) buildCommandLine(
fvd *fvdpb.VirtualDevice,
images build.ImageManifest,
) ([]string, error) {
if d.Emulator == Femu {
b := qemu.NewAEMUCommandBuilder()
b.SetBinary(d.systemPath(Arch(fvd.Hw.Arch)))
// Ask QEMU to emit a message on stderr once the VM is running
// so we'll know whether QEMU has started or not.
b.SetFlag("-trace", "enable=vm_state_notify")
b.SetFlag("-nographic")
if err := virtual_device.AEMUCommand(b, fvd, images); err != nil {
return nil, err
}
return b.Build()
}
b := &qemu.QEMUCommandBuilder{}
b.SetBinary(d.systemPath(Arch(fvd.Hw.Arch)))
// Ask QEMU to emit a message on stderr once the VM is running
// so we'll know whether QEMU has started or not.
b.SetFlag("-trace", "enable=vm_state_notify")
b.SetFlag("-nographic")
if err := virtual_device.QEMUCommand(b, fvd, images); err != nil {
return nil, err
}
return b.Build()
}
// CreateContext creates an instance of the emulator with the given parameters,
// passing through ctx to the underlying exec.Cmd.
func (d *Distribution) CreateContext(
ctx context.Context,
fvd *fvdpb.VirtualDevice,
) (*Instance, error) {
return d.create(
func(args []string) *exec.Cmd { return exec.CommandContext(ctx, args[0], args[1:]...) },
fvd,
nil,
)
}
// CreateContextWithAuthorizedKeys creates an instance of the emulator, passing through ctx to the
// underlying exec.Cmd, and updating the virtual device's initrd to contain the specified authorized
// keys.
func (d *Distribution) CreateContextWithAuthorizedKeys(
ctx context.Context,
fvd *fvdpb.VirtualDevice,
hostPathZbiBinary, hostPathAuthorizedKeys string,
) (*Instance, error) {
return d.create(
func(args []string) *exec.Cmd { return exec.CommandContext(ctx, args[0], args[1:]...) },
fvd,
&addAuthorizedKeys{
hostPathZbiBinary: hostPathZbiBinary,
hostPathAuthorizedKeys: hostPathAuthorizedKeys,
},
)
}
type addAuthorizedKeys = struct {
hostPathZbiBinary string
hostPathAuthorizedKeys string
}
// The create method is structured like this because the context docs explicitly warn
// against passing a nil Context, and exec.CommandContext(context.Background(), ...)
// is not equivalent to exec.Command(...).
func (d *Distribution) create(
makeCmd func(args []string) *exec.Cmd,
fvd *fvdpb.VirtualDevice,
addAuthorizedKeys *addAuthorizedKeys,
) (*Instance, error) {
images, err := d.loadImageManifest()
if err != nil {
return nil, err
}
if addAuthorizedKeys != nil {
hostPathZbiBinary := addAuthorizedKeys.hostPathZbiBinary
hostPathAuthorizedKeys := addAuthorizedKeys.hostPathAuthorizedKeys
// This will get cleaned up by d.Delete().
root, err := os.MkdirTemp(d.unpackedPath, "zbi-tmp-dir-*")
if err != nil {
return nil, fmt.Errorf(
"error making temp directory in %s: %w",
d.unpackedPath,
err,
)
}
if err := runZbi(runZbiArgs{
imagesIWillMutateThis: images,
workingDirectory: root,
hostPathZbiBinary: hostPathZbiBinary,
initrdName: fvd.Initrd,
zbiArgs: []string{
"--entry",
fmt.Sprintf("data/ssh/authorized_keys=%s", hostPathAuthorizedKeys),
},
}); err != nil {
if rmErr := os.RemoveAll(root); rmErr != nil {
log.Println(rmErr)
}
return nil, err
}
}
args, err := d.buildCommandLine(fvd, images)
if err != nil {
return nil, err
}
fmt.Printf("Running %s %s\n", args[0], args[1:])
i := &Instance{
cmd: makeCmd(args),
emulator: d.Emulator,
logDestination: os.Stdout,
}
// QEMU looks in the cwd for some specially named files, in particular
// multiboot.bin, so avoid picking those up accidentally. See
// https://fxbug.dev/42131198.
// TODO(https://fxbug.dev/42136802): Remove this.
i.cmd.Dir = "/"
return i, nil
}
type runZbiArgs struct {
imagesIWillMutateThis []build.Image
workingDirectory string
hostPathZbiBinary string
initrdName string
zbiArgs []string
}
func runZbi(args runZbiArgs) error {
images := args.imagesIWillMutateThis
root := args.workingDirectory
oldZBIPath := ""
newZBIPath := filepath.Join(root, "a.zbi")
// Replace the ZBI in the image manifest with our modified one.
for i, image := range images {
if image.Name == args.initrdName && image.Type == "zbi" {
oldZBIPath = image.Path
images[i].Path = newZBIPath
break
}
}
cmd := exec.Command(
args.hostPathZbiBinary,
append([]string{"-o", newZBIPath, oldZBIPath}, args.zbiArgs...)...)
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running %q: %w; stderr:\n%s", cmd, err, stderrBuf.String())
}
return nil
}
// ResizeRawImage finds a raw FVM image by name in the build's image manifest and generates a fresh
// image with additional free space using that as a basis. It returns the path to that image file
// on-disk.
//
// NB: Caller is responsible for cleaning up the image file.
//
// fshost fails to mount a disk with no free space, and so some raw images cannot be used in tests
// that try to boot all the way to a running Fuchsia session. This creates and returns the on-disk
// path to a fresh raw image that's twice the size as the original.
func (d *Distribution) ResizeRawImage(imageName, hostPathFvmBinary string) (string, error) {
isFvm := true
blk, err := d.findImageByName(imageName, "blk")
if err != nil {
blk, err = d.findImageByName(imageName, "fxfs-blk")
if err != nil {
return "", err
}
isFvm = false
}
resizedPath, err := func() (string, error) {
// Don't want a tempfile, just a safe name. Close the file object as soon as is safe.
f, err := os.CreateTemp("", "resized_fvm.*.blk")
if err != nil {
return "", err
}
f.Close()
resizedPath := f.Name()
{
if isFvm {
cmd := exec.Command(hostPathFvmBinary, resizedPath, "decompress", "--default", blk.Path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return resizedPath, fmt.Errorf("error running %q: %w", cmd, err)
}
} else { // Create copy at resized path for truncating in place.
if info, err := os.Stat(blk.Path); err != nil {
return resizedPath, err
} else if !info.Mode().IsRegular() {
return resizedPath, fmt.Errorf("image is not a regular file")
}
image, err := os.Open(blk.Path)
if err != nil {
return resizedPath, err
}
defer image.Close()
resizedImage, err := os.Create(resizedPath)
if err != nil {
return resizedPath, err
}
defer resizedImage.Close()
if _, err = io.Copy(resizedImage, image); err != nil {
return resizedPath, err
}
}
}
info, err := os.Stat(resizedPath)
if err != nil {
return resizedPath, err
}
// Upsize the image by 2x, and express the size in KB
size := fmt.Sprintf("%dK", (2*info.Size())/1024)
{
if isFvm {
cmd := exec.Command(hostPathFvmBinary, resizedPath, "extend", "--length", size, "--length-is-lowerbound")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return resizedPath, fmt.Errorf("error running %q: %w", cmd, err)
}
} else {
if err := os.Truncate(resizedPath, info.Size()*2); err != nil {
return resizedPath, err
}
}
}
return resizedPath, nil
}()
if err != nil {
os.Remove(resizedPath)
}
return resizedPath, err
}
func (d *Distribution) findImageByName(name, typ string) (*build.Image, error) {
images, err := d.loadImageManifest()
if err != nil {
return nil, err
}
for _, image := range images {
if image.Name == name && image.Type == typ {
return &image, nil
}
}
return nil, fmt.Errorf("Could not find %s of type %s in image manifest.", name, typ)
}
func tempFilePath(dir, template string) (string, error) {
// Don't want a tempfile, just a safe name. Close the file object as soon as is safe.
f, err := os.CreateTemp(dir, template)
if err != nil {
return "", err
}
defer f.Close() // So the file object doesn't hang around
return f.Name(), nil
}
// RunNonInteractive runs an instance of the emulator that runs a single command and
// returns the log that results from doing so.
//
// This mode is non-interactive and is intended specifically to test the case
// where the serial port has been disabled. The following modifications are
// made to the emulator invocation compared with Create()/Start():
//
// - amalgamate the given ZBI into a larger one that includes an additional
// entry of a script which includes commands to run.
// - that script runs the given command with output written to a file on
// the /tmp disk
// - the script triggers shutdown of the machine
// - after emulator shutdown, the log file is extracted and returned.
//
// In order to achieve this, here we need to create the host block
// device, write the commands to run, build the augmented .zbi to
// be used to boot. We then use Start() and wait for shutdown.
// Finally, extract and return the log from the block device.
func (d *Distribution) RunNonInteractive(
toRun, hostPathExtractLogsBinary, hostPathZbiBinary string,
fvd *fvdpb.VirtualDevice,
) (string, string, error) {
root, err := os.MkdirTemp("", "qemu")
if err != nil {
return "", "", err
}
log, logerr, err := d.runNonInteractive(
root,
toRun,
hostPathExtractLogsBinary,
hostPathZbiBinary,
fvd,
)
if err2 := os.RemoveAll(root); err == nil {
err = err2
}
return log, logerr, err
}
func (d *Distribution) runNonInteractive(
root, toRun, hostPathExtractLogsBinary, hostPathZbiBinary string,
fvd *fvdpb.VirtualDevice,
) (string, string, error) {
// Write runcmds that mounts the results disk, runs the requested command, and
// shuts down.
script := `DEV=$(waitfor class=block topo=/00:06.0/00_06_0/virtio-block/block timeout=60000 print)
run-with-logs "$DEV" ` + toRun + `
dm poweroff
`
runcmds := filepath.Join(root, "runcmds.txt")
if err := os.WriteFile(runcmds, []byte(script), 0666); err != nil {
return "", "", err
}
// Make a block device to write to in the target.
fs := filepath.Join(root, "a.fs")
{
fs_file, err := os.Create(fs)
if err != nil {
return "", "", err
}
if err := fs_file.Truncate(100 * 1000 * 1000); err != nil {
return "", "", err
}
if err := fs_file.Close(); err != nil {
return "", "", err
}
}
images, err := d.loadImageManifest()
if err != nil {
return "", "", err
}
if err := runZbi(runZbiArgs{
imagesIWillMutateThis: images,
workingDirectory: root,
hostPathZbiBinary: hostPathZbiBinary,
initrdName: fvd.Initrd,
zbiArgs: []string{"-e", "runcmds=" + runcmds},
}); err != nil {
return "", "", err
}
fvd.KernelArgs = append(fvd.KernelArgs, "zircon.autorun.boot=/boot/bin/sh+/boot/runcmds")
// Add the temporary disk at 00:06.0. This follows how infra runs qemu with an extra
// disk via botanist.
fvd.Drive = &fvdpb.Drive{
Id: "resultdisk",
Image: fs,
IsFilename: true,
PciAddress: "6.0",
}
args, err := d.buildCommandLine(fvd, images)
if err != nil {
return "", "", err
}
fmt.Printf("Running non-interactive %s %s\n", args[0], args[1:])
{
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// QEMU looks in the cwd for some specially named files, in particular
// multiboot.bin, so avoid picking those up accidentally. See
// https://fxbug.dev/42131198.
// TODO(https://fxbug.dev/42136802): Remove this.
cmd.Dir = "/"
if err := cmd.Run(); err != nil {
return "", "", fmt.Errorf("error running %q: %w", cmd, err)
}
}
log := filepath.Join(root, "log.txt")
logerr := filepath.Join(root, "err.txt")
{
cmd := exec.Command(hostPathExtractLogsBinary, fs, log, logerr)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", "", fmt.Errorf("error running %q: %w", cmd, err)
}
}
retLog, err := os.ReadFile(log)
if err != nil {
return "", "", err
}
retErr, err := os.ReadFile(logerr)
if err != nil {
return "", "", err
}
fmt.Printf("===== %s non-interactive run stdout =====\n%s\n", toRun, retLog)
fmt.Printf("===== %s non-interactive run stderr =====\n%s\n", toRun, retErr)
fmt.Printf("===== %s end =====\n", toRun)
return string(retLog), string(retErr), nil
}
// Decodes the buildinfo.ini file generated by BUILD.gn
func (d *Distribution) loadBuildInfo() (*buildInfo, error) {
var buildinfo buildInfo
path := filepath.Join(d.testDataDir, "emulator", "buildinfo.ini")
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %q: %w", path, err)
}
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
continue
}
kv := strings.SplitN(line, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("invalid buildinfo line: %q", line)
}
switch kv[0] {
case "image_manifest_path":
buildinfo.ImageManifestPath = kv[1]
case "target_cpu":
buildinfo.TargetCPU = kv[1]
default:
return nil, fmt.Errorf("unknown buildinfo key: %q", kv[0])
}
}
return &buildinfo, nil
}
func (d *Distribution) loadImageManifest() (build.ImageManifest, error) {
buildinfo, err := d.loadBuildInfo()
if err != nil {
return nil, err
}
var images build.ImageManifest
// ImageManifestPath is given as a relative path from test_data/emulator.
imageManifestPath := filepath.Clean(
filepath.Join(d.testDataDir, "emulator", buildinfo.ImageManifestPath),
)
if err := jsonutil.ReadFromFile(imageManifestPath, &images); err != nil {
return nil, fmt.Errorf("json read %q: %w", imageManifestPath, err)
}
// Patch each image in the manifest to have an absolute path. The paths are already
// relative to the image manifest.
imageManifestDir := filepath.Dir(imageManifestPath)
for i := range images {
if !filepath.IsAbs(images[i].Path) {
images[i].Path = filepath.Join(imageManifestDir, images[i].Path)
}
}
return images, nil
}
// Start the emulator instance.
func (i *Instance) Start() error {
return i.StartPiped(nil)
}
// StartPiped starts the emulator instance with stdin/stdout piped through a
// different process.
//
// Assumes that the stderr from the piped process should replace the stdout
// from the emulator.
func (i *Instance) StartPiped(piped *exec.Cmd) error {
stdin, err := i.cmd.StdinPipe()
if err != nil {
return err
}
stdout, err := i.cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := i.cmd.StderrPipe()
if err != nil {
return err
}
if piped != nil {
piped.Stdin = stdout
piped.Stdout = stdin
pipedStderr, err := piped.StderrPipe()
if err != nil {
return err
}
i.stdout = bufio.NewReader(pipedStderr)
i.stderr = bufio.NewReader(stderr)
if err := piped.Start(); err != nil {
return err
}
i.piped = piped
} else {
i.stdin = bufio.NewWriter(stdin)
i.stdout = bufio.NewReader(stdout)
i.stderr = bufio.NewReader(stderr)
}
startErr := i.cmd.Start()
// Look for very early log message to validate that the emulator likely started
// correctly. Loop for a while to give the emulator a chance to boot.
fmt.Println("Checking for QEMU boot...")
for j := 0; j < 100; j++ {
if i.emulator == Femu {
// FEMU isn't built with support for outputting trace events.
// Instead we look for a message that occurs very early during Zircon boot.
if i.checkForLogMessage(i.stdout, "welcome to Zircon") == nil {
break
}
} else {
// The flag `-trace enable=vm_state_notify` will cause qemu to
// print this message early in boot.
if i.checkForLogMessage(i.stderr, "vm_state_notify running") == nil {
break
}
}
time.Sleep(100 * time.Millisecond)
}
return startErr
}
// Wait waits for the emulator instance to terminate while printing the emulator's stdout to
// whichever Writer has been passed to SetLogDestination (defaults to os.Stdout).
func (i *Instance) Wait() (*os.ProcessState, error) {
scanDone := make(chan struct{})
// Finish consuming the emulator process's stdout before returning.
defer func() {
<-scanDone
}()
go func() {
defer close(scanDone)
// Line-buffer writes to stdout to avoid messy interleaving.
scanner := bufio.NewScanner(i.stdout)
for scanner.Scan() {
fmt.Fprintln(i.logDestination, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Printf("%T.stdout: %s\n", i, err)
}
}()
return func() *os.Process {
if i.piped != nil {
return i.piped.Process
}
return i.cmd.Process
}().Wait()
}
// RunCommand runs the given command in the serial console for the emulator
// instance.
func (i *Instance) RunCommand(cmd string) error {
if _, err := fmt.Fprintf(i.stdin, "%s\n", cmd); err != nil {
return err
}
return i.stdin.Flush()
}
// WaitForLogMessage reads log messages from the emulator instance until it reads a
// message that contains the given string.
func (i *Instance) WaitForLogMessage(msg string) error {
return i.WaitForLogMessages([]string{msg})
}
// WaitForLogMessages reads log messages from the emulator instance until it reads all
// message in |msgs|. The log messages can appear in *any* order. Only one
// expected message from |msgs| is retired per matching actual message even if
// multiple messages from |msgs| match the log line.
func (i *Instance) WaitForLogMessages(msgs []string) error {
return i.checkForLogMessages(i.stdout, msgs)
}
// WaitForAnyLogMessage reads log messages from the emulator instance looking for any line that
// contains a message from msgs.
// Returns the first message that was found, or an error.
func (i *Instance) WaitForAnyLogMessage(msgs ...string) (string, error) {
for {
line, err := i.stdout.ReadString('\n')
if err != nil {
i.printStderr()
return "", err
}
fmt.Print(line)
for _, msg := range msgs {
if strings.Contains(line, msg) {
return msg, nil
}
}
}
}
// WaitForLogMessageAssertNotSeen is the same as WaitForLogMessage() but with
// the addition that it will return an error if |notSeen| is contained in a
// retrieved message.
func (i *Instance) WaitForLogMessageAssertNotSeen(msg string, notSeen string) error {
for {
line, err := i.stdout.ReadString('\n')
if err != nil {
i.printStderr()
return fmt.Errorf("failed to find: %q: %w", msg, err)
}
fmt.Print(line)
if strings.Contains(line, msg) {
return nil
}
if strings.Contains(line, notSeen) {
return fmt.Errorf("found in output: %q", notSeen)
}
}
}
// AssertLogMessageNotSeenWithinTimeout will fail if |notSeen| is seen within the
// |timeout| period. This function will timeout as success if more than |timeout| has
// passed without seeing |notSeen|.
func (i *Instance) AssertLogMessageNotSeenWithinTimeout(
notSeen string,
timeout time.Duration,
) error {
// ReadString is blocking, we need to make sure it respects the global timeout.
seen := make(chan struct{})
stop := make(chan struct{})
defer close(stop)
go func() {
defer close(seen)
for {
select {
case <-stop:
return
default:
if line, err := i.stdout.ReadString('\n'); err == nil {
if strings.Contains(line, notSeen) {
seen <- struct{}{}
return
}
}
}
}
}()
select {
case <-seen:
return fmt.Errorf("found in output: %q", notSeen)
case <-time.After(timeout):
return nil
}
}
// Reset display: ESC c
// Reset screen mode: ESC [ ? 7 l
// Move cursor home: ESC [ 2 J
// All text attributes off: ESC [ 0 m
const emuClearPrefix = "\x1b\x63\x1b\x5b\x3f\x37\x6c\x1b\x5b\x32\x4a\x1b\x5b\x30\x6d"
// Reads all messages from |b| and tests if |msg| appears. Returns error if any.
func (i *Instance) checkForLogMessage(b *bufio.Reader, msg string) error {
return i.checkForLogMessages(b, []string{msg})
}
// printStderr prints all the lines from the instance's stderr stream.
func (i *Instance) printStderr() {
fmt.Printf("printing stderr...\n")
for {
stderr, err := i.stderr.ReadString('\n')
if err != nil {
return
}
fmt.Print(stderr)
}
}
// Reads all messages from |b| and tests if all messages of |msgs| appear in *any* order. Returns
// error if any.
func (i *Instance) checkForLogMessages(b *bufio.Reader, msgs []string) error {
for {
line, err := b.ReadString('\n')
if err != nil {
i.printStderr()
return err
}
// Drop the clearing preamble as it makes it difficult to see output
// when there's multiple emulator runs in a single binary.
fmt.Print(strings.TrimPrefix(line, emuClearPrefix))
for i, msg := range msgs {
if strings.Contains(line, msg) {
msgs = append(msgs[:i], msgs[i+1:]...)
if len(msgs) == 0 {
return nil
}
break
}
}
}
}
// CaptureLinesContaining returns all the lines that contain the given msg, up until a line
// containing stop is found.
func (i *Instance) CaptureLinesContaining(msg string, stop string) ([]string, error) {
res := []string{}
for {
line, err := i.stdout.ReadString('\n')
if err != nil {
i.printStderr()
return nil, err
}
if strings.Contains(line, msg) {
res = append(res, line)
}
if strings.Contains(line, stop) {
return res, nil
}
}
}
func (i *Instance) SetLogDestination(dest io.Writer) {
i.logDestination = dest
}