github.com/jlowellwofford/u-root@v1.0.0/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 is suitable for running QEMU-based integration tests.
     6  //
     7  // The environment variable `UROOT_QEMU` overrides the path to QEMU and the
     8  // first few arguments (defaults to "qemu"). For example, I use:
     9  //
    10  //     UROOT_QEMU='qemu-system-x86_64 -L . -m 4096 -enable-kvm'
    11  //
    12  // For CI, this environment variable is set in `.circleci/images/integration/Dockerfile`.
    13  package qemu
    14  
    15  import (
    16  	"errors"
    17  	"os"
    18  	"regexp"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/google/goexpect"
    23  )
    24  
    25  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    26  var DefaultTimeout = 7 * time.Second
    27  
    28  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    29  // QEMU on a slow machine.
    30  var TimeoutMultiplier = 2.0
    31  
    32  // QEMU is filled and pass to `Start()`.
    33  type QEMU struct {
    34  	// Path to the bzImage kernel
    35  	Kernel string
    36  
    37  	// Path to the initramfs
    38  	InitRAMFS string
    39  
    40  	// Extra kernel arguments
    41  	KernelArgs string
    42  
    43  	// Extra QEMU arguments
    44  	ExtraArgs []string
    45  
    46  	gExpect *expect.GExpect
    47  }
    48  
    49  // CmdLine returns the command line arguments used to start QEMU. These
    50  // arguments are derived from the given QEMU struct.
    51  func (q *QEMU) CmdLine() []string {
    52  	// Read first few arguments for env.
    53  	env := os.Getenv("UROOT_QEMU")
    54  	if env == "" {
    55  		env = "qemu" // default
    56  	}
    57  	args := strings.Fields(env)
    58  
    59  	// Disable graphics because we are using serial.
    60  	args = append(args, "-nographic")
    61  
    62  	// Arguments passed to the kernel:
    63  	//
    64  	// - earlyprintk=ttyS0: print very early debug messages to the serial
    65  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
    66  	// - q.KernelArgs: extra, optional kernel arguments
    67  	args = append(args, "-append", "console=ttyS0 earlyprintk=ttyS0")
    68  	if q.KernelArgs != "" {
    69  		args[len(args)-1] += " " + q.KernelArgs
    70  	}
    71  
    72  	// Kernel and initramfs
    73  	args = append(args, "-kernel", q.Kernel)
    74  	if q.InitRAMFS != "" {
    75  		args = append(args, "-initrd", q.InitRAMFS)
    76  	}
    77  
    78  	return append(args, q.ExtraArgs...)
    79  }
    80  
    81  // CmdLineQuoted quotes any of QEMU's command line arguments containing a space
    82  // so it is easy to copy-n-paste into a shell for debugging.
    83  func (q *QEMU) CmdLineQuoted() (cmdline string) {
    84  	args := q.CmdLine()
    85  	for i, v := range q.CmdLine() {
    86  		if strings.ContainsAny(v, " \t\n") {
    87  			args[i] = "'" + v + "'"
    88  		}
    89  	}
    90  	return strings.Join(args, " ")
    91  }
    92  
    93  // Start QEMU.
    94  func (q *QEMU) Start() error {
    95  	if q.gExpect != nil {
    96  		return errors.New("QEMU already started")
    97  	}
    98  	var err error
    99  	q.gExpect, _, err = expect.SpawnWithArgs(q.CmdLine(), -1)
   100  	return err
   101  }
   102  
   103  // Close stops QEMU.
   104  func (q *QEMU) Close() {
   105  	q.gExpect.Close()
   106  	q.gExpect = nil
   107  }
   108  
   109  // Send sends a string to QEMU's serial.
   110  func (q *QEMU) Send(in string) {
   111  	q.gExpect.Send(in)
   112  }
   113  
   114  // Expect returns an error if the given string is not found in QEMU's serial
   115  // output within `DefaultTimeout`.
   116  func (q *QEMU) Expect(search string) error {
   117  	return q.ExpectTimeout(search, DefaultTimeout)
   118  }
   119  
   120  // ExpectTimeout returns an error if the given string is not found in QEMU's serial
   121  // output within the given timeout.
   122  func (q *QEMU) ExpectTimeout(search string, timeout time.Duration) error {
   123  	return q.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   124  }
   125  
   126  // ExpectRE returns an error if the given regular expression is not found in
   127  // QEMU's serial output within `DefaultTimeout`.
   128  func (q *QEMU) ExpectRE(pattern *regexp.Regexp) error {
   129  	return q.ExpectRETimeout(pattern, DefaultTimeout)
   130  }
   131  
   132  // ExpectRETimeout returns an error if the given regular expression is not
   133  // found in QEMU's serial output within the given timeout.
   134  func (q *QEMU) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) error {
   135  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   136  	_, _, err := q.gExpect.Expect(pattern, scaled)
   137  	return err
   138  }