blob: c120a7c2a099cd2210c6bbd3b488c3060c7fed67 [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 qemu
import (
"archive/tar"
"bufio"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
// 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()
tr := tar.NewReader(gz)
for {
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
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
}
}
}
// Distribution is a collection of QEMU-related artifacts.
type Distribution struct {
exPath string
unpackedPath string
}
type Arch int
const (
X64 Arch = iota
Arm64
)
// Params describes how to run a QEMU instance.
type Params struct {
Arch Arch
ZBI string
AppendCmdline string
Networking bool
Memory int // megabytes
}
type Instance struct {
cmd *exec.Cmd
stdin *bufio.Writer
stdout *bufio.Reader
stderr *bufio.Reader
}
// Unpack unpacks the QEMU distribution.
func Unpack() (*Distribution, error) {
ex, err := os.Executable()
if err != nil {
return nil, err
}
exPath := filepath.Dir(ex)
archivePath := filepath.Join(exPath, "test_data/qemu/qemu.tar.gz")
unpackedPath, err := ioutil.TempDir("", "qemu-distro")
if err != nil {
return nil, err
}
err = untar(unpackedPath, archivePath)
if err != nil {
os.RemoveAll(unpackedPath)
return nil, err
}
return &Distribution{exPath: exPath, unpackedPath: unpackedPath}, nil
}
// Delete removes the QEMU-related artifacts.
func (d *Distribution) Delete() {
os.RemoveAll(d.unpackedPath)
}
func (d *Distribution) systemPath(arch Arch) string {
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 ""
}
func (d *Distribution) kernelPath(arch Arch) string {
switch arch {
case X64:
return filepath.Join(d.exPath, "test_data/qemu/multiboot.bin")
case Arm64:
return filepath.Join(d.exPath, "test_data/qemu/qemu-boot-shim.bin")
}
return ""
}
// TargetCPU returs the target CPU used by the build that produced this library.
func (d *Distribution) TargetCPU() (Arch, error) {
path := filepath.Join(d.exPath, "test_data/qemu/target_cpu.txt")
bytes, err := ioutil.ReadFile(path)
if err != nil {
return X64, err
}
name := string(bytes)
switch name {
case "x64":
return X64, nil
case "arm64":
return Arm64, nil
}
return X64, fmt.Errorf("unknown target CPU: %s", name)
}
func (d *Distribution) appendCommonQemuArgs(params Params, args []string) []string {
args = append(args, "-kernel", d.kernelPath(params.Arch))
args = append(args, "-nographic", "-smp", "4,threads=2",
"-machine", "q35", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
"-cpu", "Haswell,+smap,-check,-fsgsbase")
if params.Memory == 0 {
args = append(args, "-m", "2048")
} else {
args = append(args, "-m", strconv.Itoa(params.Memory))
}
if params.Networking {
args = append(args, "-nic", "tap,ifname=qemu,script=no,downscript=no")
} else {
args = append(args, "-net", "none")
}
return args
}
func getCommonKernelCmdline(params Params) string {
cmdline := "kernel.serial=legacy kernel.entropy-mixin=1420bb81dc0396b37cc2d0aa31bb2785dadaf9473d0780ecee1751afb5867564 kernel.halt-on-panic=true"
if params.AppendCmdline != "" {
cmdline += " "
cmdline += params.AppendCmdline
}
return cmdline
}
// Create creates an instance of QEMU with the given parameters.
func (d *Distribution) Create(params Params) *Instance {
path := d.systemPath(params.Arch)
args := []string{}
if params.ZBI == "" {
panic("ZBI must be specified")
}
args = append(args, "-initrd", params.ZBI)
args = d.appendCommonQemuArgs(params, args)
args = append(args, "-append", getCommonKernelCmdline(params))
return &Instance{
cmd: exec.Command(path, args...),
}
}
// Creates and runs an instance of QEMU that runs a single command and results
// the log that results from doing so.
// and `minfs` to be included in the BUILD.gn file (see disable_syscall_test's
// BUILD file.)
func (d *Distribution) RunNonInteractive(
toRun string,
hostPathMinfsBinary string,
hostPathZbiBinary string,
params Params) (string, string, error) {
// 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 QEMU 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 qemu shutdown, the log file is extracted and returned.
//
// In order to achive this, here we need to create the host minfs
// filesystem, 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.
// Make the temp files we need.
tmpFsFile, err := ioutil.TempFile(os.TempDir(), "*.fs")
if err != nil {
return "", "", err
}
defer os.Remove(tmpFsFile.Name())
tmpRuncmds, err := ioutil.TempFile(os.TempDir(), "runcmds_*")
if err != nil {
return "", "", err
}
defer os.Remove(tmpRuncmds.Name())
tmpZbi, err := ioutil.TempFile(os.TempDir(), "*.zbi")
if err != nil {
return "", "", err
}
defer os.Remove(tmpZbi.Name())
tmpLog, err := ioutil.TempFile(os.TempDir(), "log.*.txt")
if err != nil {
return "", "", err
}
defer os.Remove(tmpLog.Name())
tmpErr, err := ioutil.TempFile(os.TempDir(), "err.*.txt")
if err != nil {
return "", "", err
}
defer os.Remove(tmpErr.Name())
// Write runcmds that mounts the results disk, runs the requested command, and
// shuts down.
tmpRuncmds.WriteString(`mkdir /tmp/testdata-fs
waitfor class=block topo=/dev/sys/pci/00:06.0/virtio-block/block timeout=60000
mount /dev/sys/pci/00:06.0/virtio-block/block /tmp/testdata-fs
`)
tmpRuncmds.WriteString(toRun + " 2>/tmp/testdata-fs/err.txt >/tmp/testdata-fs/log.txt\n")
tmpRuncmds.WriteString(`umount /tmp/testdata-fs
dm poweroff
`)
// Make a minfs filesystem to mount in the target.
cmd := exec.Command(hostPathMinfsBinary, tmpFsFile.Name()+"@100M", "mkfs")
err = cmd.Run()
if err != nil {
return "", "", err
}
// Create the new initrd that references the runcmds file.
cmd = exec.Command(
hostPathZbiBinary, "-o", tmpZbi.Name(),
params.ZBI,
"-e", "runcmds="+tmpRuncmds.Name())
err = cmd.Run()
if err != nil {
return "", "", err
}
// Build up the qemu command line from common arguments and the extra goop to
// add the temporary disk at 00:06.0. This follows how infra runs qemu with an
// extra disk via botanist.
path := d.systemPath(params.Arch)
args := []string{}
args = append(args, "-initrd", tmpZbi.Name())
args = d.appendCommonQemuArgs(params, args)
args = append(args, "-object", "iothread,id=resultiothread")
args = append(args, "-drive",
"id=resultdisk,file="+tmpFsFile.Name()+",format=raw,if=none,cache=unsafe,aio=threads")
args = append(args, "-device", "virtio-blk-pci,drive=resultdisk,iothread=resultiothread,addr=6.0")
cmdline := getCommonKernelCmdline(params)
cmdline += " zircon.autorun.boot=/boot/bin/sh+/boot/runcmds"
args = append(args, "-append", cmdline)
cmd = exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err = cmd.Start(); err != nil {
return "", "", err
}
defer cmd.Process.Kill()
err = cmd.Wait()
if err != nil {
return "", "", err
}
os.Remove(tmpLog.Name()) // `minfs` will refuse to overwrite a local file, so delete first.
cmd = exec.Command(hostPathMinfsBinary, tmpFsFile.Name(), "cp", "::/log.txt", tmpLog.Name())
err = cmd.Run()
if err != nil {
return "", "", err
}
os.Remove(tmpErr.Name()) // `minfs` will refuse to overwrite a local file, so delete first.
cmd = exec.Command(hostPathMinfsBinary, tmpFsFile.Name(), "cp", "::/err.txt", tmpErr.Name())
err = cmd.Run()
if err != nil {
return "", "", err
}
retLog, err := ioutil.ReadFile(tmpLog.Name())
if err != nil {
return "", "", err
}
retErr, err := ioutil.ReadFile(tmpErr.Name())
if err != nil {
return "", "", err
}
fmt.Printf("===== %s non-interactive run stdout =====\n", toRun)
fmt.Print(string(retLog))
fmt.Printf("===== %s non-interactive run stderr =====\n", toRun)
fmt.Print(string(retErr))
fmt.Printf("===== %s end =====\n", toRun)
return string(retLog), string(retErr), nil
}
// Start the QEMU instance.
func (i *Instance) Start() 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
}
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 qemu likely started
// correctly. Loop for a while to give qemu a chance to boot.
fmt.Println("Checking for QEMU boot...")
for j := 0; j < 100; j++ {
if i.CheckForLogMessage("SeaBIOS") == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
return startErr
}
// Kill terminates the QEMU instance.
func (i *Instance) Kill() error {
return i.cmd.Process.Kill()
}
// RunCommand runs the given command in the serial console for the QEMU instance.
func (i *Instance) RunCommand(cmd string) {
_, err := i.stdin.WriteString(fmt.Sprintf("%s\n", cmd))
if err != nil {
panic(err)
}
err = i.stdin.Flush()
if err != nil {
panic(err)
}
}
// WaitForLogMessage reads log messages from the QEMU instance until it reads a
// message that contains the given string. panic()s on error (and in particular
// if the string is not seen until EOF).
func (i *Instance) WaitForLogMessage(msg string) {
err := i.CheckForLogMessage(msg)
if err != nil {
panic(err)
}
}
// WaitForLogMessageAssertNotSeen is the same as WaitForLogMessage() but with
// the addition that it will panic if |notSeen| is contained in a retrieved
// message.
func (i *Instance) WaitForLogMessageAssertNotSeen(msg string, notSeen string) {
for {
line, err := i.stdout.ReadString('\n')
if err != nil {
panic(err)
}
if strings.Contains(line, msg) {
return
}
if strings.Contains(line, notSeen) {
panic(notSeen + " was in output")
}
}
}
// 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) {
// ReadString is blocking, we need to make sure it respects the global timeout.
seen := make(chan bool)
go func() {
for {
select {
case <-seen:
return
default:
line, err := i.stdout.ReadString('\n')
if err == nil {
if strings.Contains(line, notSeen) {
seen <- true
return
}
}
}
}
}()
select {
case <-seen:
close(seen)
panic(notSeen + " was in output")
case <-time.After(timeout):
close(seen)
return
}
}
// Reset display: ESC c
// Reset screen mode: ESC [ ? 7 l
// Move cursor home: ESC [ 2 J
// All text attributes off: ESC [ 0 m
const qemuClearPrefix = "\x1b\x63\x1b\x5b\x3f\x37\x6c\x1b\x5b\x32\x4a\x1b\x5b\x30\x6d"
// Reads all messages from stdout of QEMU, and tests if msg appears. Returns
// error if any. Prefer WaitForLogMessage() unless you're certain this is the
// one you want.
func (i *Instance) CheckForLogMessage(msg string) error {
for {
line, err := i.stdout.ReadString('\n')
if err != nil {
for {
stderr, err2 := i.stderr.ReadString('\n')
if err2 != nil {
break
}
fmt.Print(stderr)
}
panic(err)
}
// Drop the QEMU clearing preamble as it makes it difficult to see output
// when there's multiple qemu runs in a single binary.
toPrint := line
if strings.HasPrefix(toPrint, qemuClearPrefix) {
toPrint = toPrint[len(qemuClearPrefix):]
}
fmt.Print(toPrint)
if strings.Contains(line, msg) {
return nil
}
}
}