github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/pkg/qemu/qemu.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package qemu provides a Go API for starting QEMU VMs.
     6  //
     7  // qemu is mainly suitable for running QEMU-based integration tests.
     8  //
     9  // The environment variable `UROOT_QEMU` overrides the path to QEMU and the
    10  // first few arguments (defaults to "qemu"). For example, I use:
    11  //
    12  //     UROOT_QEMU='qemu-system-x86_64 -L . -m 4096 -enable-kvm'
    13  //
    14  // For CI, this environment variable is set in `.circleci/images/integration/Dockerfile`.
    15  package qemu
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"regexp"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	expect "github.com/google/goexpect"
    27  )
    28  
    29  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    30  var DefaultTimeout = 7 * time.Second
    31  
    32  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    33  // QEMU on a slow machine.
    34  var TimeoutMultiplier = 1.0
    35  
    36  func init() {
    37  	if timeoutMultS := os.Getenv("UROOT_QEMU_TIMEOUT_X"); len(timeoutMultS) > 0 {
    38  		t, err := strconv.ParseFloat(timeoutMultS, 64)
    39  		if err == nil {
    40  			TimeoutMultiplier = t
    41  		}
    42  	}
    43  }
    44  
    45  // Options are VM start-up parameters.
    46  type Options struct {
    47  	// QEMUPath is the path to the QEMU binary to invoke.
    48  	//
    49  	// If left unspecified, the UROOT_QEMU env var will be used.
    50  	// If the env var is unspecified, "qemu" is the default.
    51  	QEMUPath string
    52  
    53  	// Path to the kernel to boot.
    54  	Kernel string
    55  
    56  	// Path to the initramfs.
    57  	Initramfs string
    58  
    59  	// Extra kernel command-line arguments.
    60  	KernelArgs string
    61  
    62  	// Where to send serial output.
    63  	SerialOutput io.WriteCloser
    64  
    65  	// Timeout is the expect timeout.
    66  	Timeout time.Duration
    67  
    68  	// Devices are devices to expose to the QEMU VM.
    69  	Devices []Device
    70  }
    71  
    72  // Start starts a QEMU VM.
    73  func (o *Options) Start() (*VM, error) {
    74  	cmdline, err := o.Cmdline()
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	gExpect, ch, err := expect.SpawnWithArgs(cmdline, -1,
    80  		expect.Tee(o.SerialOutput),
    81  		expect.PartialMatch(true),
    82  		expect.CheckDuration(2*time.Millisecond))
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	return &VM{
    87  		Options: o,
    88  		errCh:   ch,
    89  		cmdline: cmdline,
    90  		gExpect: gExpect,
    91  	}, nil
    92  }
    93  
    94  // Cmdline returns the command line arguments used to start QEMU. These
    95  // arguments are derived from the given QEMU struct.
    96  func (o *Options) Cmdline() ([]string, error) {
    97  	var args []string
    98  	if len(o.QEMUPath) > 0 {
    99  		args = append(args, o.QEMUPath)
   100  	} else {
   101  		// Read first few arguments for env.
   102  		env := os.Getenv("UROOT_QEMU")
   103  		if env == "" {
   104  			env = "qemu" // default
   105  		}
   106  		args = append(args, strings.Fields(env)...)
   107  	}
   108  
   109  	// Disable graphics because we are using serial.
   110  	args = append(args, "-nographic")
   111  
   112  	// Arguments passed to the kernel:
   113  	//
   114  	// - earlyprintk=ttyS0: print very early debug messages to the serial
   115  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
   116  	// - o.KernelArgs: extra, optional kernel arguments
   117  	// - args required by devices
   118  	for _, dev := range o.Devices {
   119  		if dev != nil {
   120  			if a := dev.KArgs(); a != nil {
   121  				o.KernelArgs += " " + strings.Join(a, " ")
   122  			}
   123  		}
   124  	}
   125  	if len(o.Kernel) != 0 {
   126  		args = append(args, "-kernel", o.Kernel)
   127  		if len(o.KernelArgs) != 0 {
   128  			args = append(args, "-append", o.KernelArgs)
   129  		}
   130  	} else if len(o.KernelArgs) != 0 {
   131  		err := fmt.Errorf("kernel args are required but cannot be added due to bootloader")
   132  		return nil, err
   133  	}
   134  	if len(o.Initramfs) != 0 {
   135  		args = append(args, "-initrd", o.Initramfs)
   136  	}
   137  
   138  	for _, dev := range o.Devices {
   139  		if dev != nil {
   140  			if c := dev.Cmdline(); c != nil {
   141  				args = append(args, c...)
   142  			}
   143  		}
   144  	}
   145  	return args, nil
   146  }
   147  
   148  // VM is a running QEMU virtual machine.
   149  type VM struct {
   150  	Options *Options
   151  	cmdline []string
   152  	errCh   <-chan error
   153  	gExpect *expect.GExpect
   154  }
   155  
   156  // Wait waits for the VM to exit.
   157  func (v *VM) Wait() error {
   158  	return <-v.errCh
   159  }
   160  
   161  // Cmdline is the command-line the VM was started with.
   162  func (v *VM) Cmdline() []string {
   163  	// Maybe return a copy?
   164  	return v.cmdline
   165  }
   166  
   167  // CmdlineQuoted quotes any of QEMU's command line arguments containing a space
   168  // so it is easy to copy-n-paste into a shell for debugging.
   169  func (v *VM) CmdlineQuoted() string {
   170  	args := make([]string, len(v.cmdline))
   171  	for i, arg := range v.cmdline {
   172  		if strings.ContainsAny(arg, " \t\n") {
   173  			args[i] = fmt.Sprintf("'%s'", arg)
   174  		} else {
   175  			args[i] = arg
   176  		}
   177  	}
   178  	return strings.Join(args, " ")
   179  }
   180  
   181  // Close stops QEMU.
   182  func (v *VM) Close() {
   183  	v.gExpect.Close()
   184  	v.gExpect = nil
   185  	// Ensure the logs are closed by waiting the complete end
   186  	<-v.errCh
   187  }
   188  
   189  // Send sends a string to QEMU's serial.
   190  func (v *VM) Send(in string) {
   191  	v.gExpect.Send(in)
   192  }
   193  
   194  func (v *VM) TimeoutOr() time.Duration {
   195  	if v.Options.Timeout == 0 {
   196  		return DefaultTimeout
   197  	}
   198  	return v.Options.Timeout
   199  }
   200  
   201  // Expect returns an error if the given string is not found in vEMU's serial
   202  // output within `DefaultTimeout`.
   203  func (v *VM) Expect(search string) error {
   204  	return v.ExpectTimeout(search, v.TimeoutOr())
   205  }
   206  
   207  // ExpectTimeout returns an error if the given string is not found in vEMU's serial
   208  // output within the given timeout.
   209  func (v *VM) ExpectTimeout(search string, timeout time.Duration) error {
   210  	_, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   211  	return err
   212  }
   213  
   214  // ExpectRE returns an error if the given regular expression is not found in
   215  // vEMU's serial output within `DefaultTimeout`. The matched string is
   216  // returned.
   217  func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) {
   218  	return v.ExpectRETimeout(pattern, v.TimeoutOr())
   219  }
   220  
   221  // ExpectRETimeout returns an error if the given regular expression is not
   222  // found in vEMU's serial output within the given timeout. The matched string
   223  // is returned.
   224  func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) {
   225  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   226  	str, _, err := v.gExpect.Expect(pattern, scaled)
   227  	return str, err
   228  }
   229  
   230  // ExpectBatch matches many regular expressions.
   231  func (v *VM) ExpectBatch(batch []expect.Batcher, timeout time.Duration) ([]expect.BatchRes, error) {
   232  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   233  	return v.gExpect.ExpectBatch(batch, scaled)
   234  }