| // 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" |
| "io/ioutil" |
| "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 fxbug.dev/65990. |
| "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 := ioutil.TempDir("", "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/53751. |
| // TODO(fxbug.dev/58804): 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) { |
| blk, err := d.findImageByName(imageName, "blk") |
| if err != nil { |
| return "", err |
| } |
| |
| 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 := ioutil.TempFile("", "resized_fvm.*.blk") |
| if err != nil { |
| return "", err |
| } |
| f.Close() |
| resizedPath := f.Name() |
| { |
| 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, 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) |
| { |
| 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, 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 := ioutil.TempFile(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 mounts a disk created on the host in /tmp, and runs the |
| // given command with output redirected to a file also 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 minfs |
| // file system, 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 minfs disk. |
| func (d *Distribution) RunNonInteractive( |
| toRun, hostPathMinfsBinary, hostPathZbiBinary string, |
| fvd *fvdpb.VirtualDevice, |
| ) (string, string, error) { |
| root, err := ioutil.TempDir("", "qemu") |
| if err != nil { |
| return "", "", err |
| } |
| log, logerr, err := d.runNonInteractive( |
| root, |
| toRun, |
| hostPathMinfsBinary, |
| hostPathZbiBinary, |
| fvd, |
| ) |
| if err2 := os.RemoveAll(root); err == nil { |
| err = err2 |
| } |
| return log, logerr, err |
| } |
| |
| func (d *Distribution) runNonInteractive( |
| root, toRun, hostPathMinfsBinary, hostPathZbiBinary string, |
| fvd *fvdpb.VirtualDevice, |
| ) (string, string, error) { |
| // Write runcmds that mounts the results disk, runs the requested command, and |
| // shuts down. |
| script := `waitfor class=block topo=/dev/pci-00:06.0/virtio-block/block timeout=60000 |
| mount /dev/pci-00:06.0/virtio-block/block /mnt/testdata-fs |
| ` + toRun + ` 2>/mnt/testdata-fs/err.txt >/mnt/testdata-fs/log.txt |
| umount /mnt/testdata-fs |
| dm poweroff |
| ` |
| runcmds := filepath.Join(root, "runcmds.txt") |
| if err := ioutil.WriteFile(runcmds, []byte(script), 0666); err != nil { |
| return "", "", err |
| } |
| // Make a minfs filesystem to mount in the target. |
| fs := filepath.Join(root, "a.fs") |
| { |
| cmd := exec.Command(hostPathMinfsBinary, fs+"@100M", "mkfs") |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); 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/53751. |
| // TODO(fxbug.dev/58804): Remove this. |
| cmd.Dir = "/" |
| if err := cmd.Run(); err != nil { |
| return "", "", err |
| } |
| } |
| |
| log := filepath.Join(root, "log.txt") |
| logerr := filepath.Join(root, "err.txt") |
| { |
| cmd := exec.Command(hostPathMinfsBinary, fs, "cp", "::/log.txt", log) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return "", "", err |
| } |
| } |
| { |
| cmd := exec.Command(hostPathMinfsBinary, fs, "cp", "::/err.txt", logerr) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return "", "", err |
| } |
| } |
| |
| retLog, err := ioutil.ReadFile(log) |
| if err != nil { |
| return "", "", err |
| } |
| retErr, err := ioutil.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 := ioutil.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 |
| } |
| 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 |
| } |