github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/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.CheckDuration(2*time.Millisecond)) 82 if err != nil { 83 return nil, err 84 } 85 return &VM{ 86 Options: o, 87 errCh: ch, 88 cmdline: cmdline, 89 gExpect: gExpect, 90 }, nil 91 } 92 93 // cmdline returns the command line arguments used to start QEMU. These 94 // arguments are derived from the given QEMU struct. 95 func (o *Options) Cmdline() ([]string, error) { 96 var args []string 97 if len(o.QEMUPath) > 0 { 98 args = append(args, o.QEMUPath) 99 } else { 100 // Read first few arguments for env. 101 env := os.Getenv("UROOT_QEMU") 102 if env == "" { 103 env = "qemu" // default 104 } 105 args = append(args, strings.Fields(env)...) 106 } 107 108 // Disable graphics because we are using serial. 109 args = append(args, "-nographic") 110 111 // Arguments passed to the kernel: 112 // 113 // - earlyprintk=ttyS0: print very early debug messages to the serial 114 // - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port) 115 // - o.KernelArgs: extra, optional kernel arguments 116 // - args required by devices 117 for _, dev := range o.Devices { 118 if dev != nil { 119 if a := dev.KArgs(); a != nil { 120 o.KernelArgs += " " + strings.Join(a, " ") 121 } 122 } 123 } 124 if len(o.Kernel) != 0 { 125 args = append(args, "-kernel", o.Kernel) 126 if len(o.KernelArgs) != 0 { 127 args = append(args, "-append", o.KernelArgs) 128 } 129 } else if len(o.KernelArgs) != 0 { 130 err := fmt.Errorf("kernel args are required but cannot be added due to bootloader") 131 return nil, err 132 } 133 if len(o.Initramfs) != 0 { 134 args = append(args, "-initrd", o.Initramfs) 135 } 136 137 for _, dev := range o.Devices { 138 if dev != nil { 139 if c := dev.Cmdline(); c != nil { 140 args = append(args, c...) 141 } 142 } 143 } 144 return args, nil 145 } 146 147 // VM is a running QEMU virtual machine. 148 type VM struct { 149 Options *Options 150 cmdline []string 151 errCh <-chan error 152 gExpect *expect.GExpect 153 } 154 155 // Wait waits for the VM to exit. 156 func (v *VM) Wait() error { 157 return <-v.errCh 158 } 159 160 // Cmdline is the command-line the VM was started with. 161 func (v *VM) Cmdline() []string { 162 // Maybe return a copy? 163 return v.cmdline 164 } 165 166 // CmdlineQuoted quotes any of QEMU's command line arguments containing a space 167 // so it is easy to copy-n-paste into a shell for debugging. 168 func (v *VM) CmdlineQuoted() string { 169 args := make([]string, len(v.cmdline)) 170 for i, arg := range v.cmdline { 171 if strings.ContainsAny(arg, " \t\n") { 172 args[i] = fmt.Sprintf("'%s'", arg) 173 } else { 174 args[i] = arg 175 } 176 } 177 return strings.Join(args, " ") 178 } 179 180 // Close stops QEMU. 181 func (v *VM) Close() { 182 v.gExpect.Close() 183 v.gExpect = nil 184 // Ensure the logs are closed by waiting the complete end 185 <-v.errCh 186 } 187 188 // Send sends a string to QEMU's serial. 189 func (v *VM) Send(in string) { 190 v.gExpect.Send(in) 191 } 192 193 func (v *VM) TimeoutOr() time.Duration { 194 if v.Options.Timeout == 0 { 195 return DefaultTimeout 196 } 197 return v.Options.Timeout 198 } 199 200 // Expect returns an error if the given string is not found in vEMU's serial 201 // output within `DefaultTimeout`. 202 func (v *VM) Expect(search string) error { 203 return v.ExpectTimeout(search, v.TimeoutOr()) 204 } 205 206 // ExpectTimeout returns an error if the given string is not found in vEMU's serial 207 // output within the given timeout. 208 func (v *VM) ExpectTimeout(search string, timeout time.Duration) error { 209 _, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout) 210 return err 211 } 212 213 // ExpectRE returns an error if the given regular expression is not found in 214 // vEMU's serial output within `DefaultTimeout`. The matched string is 215 // returned. 216 func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) { 217 return v.ExpectRETimeout(pattern, v.TimeoutOr()) 218 } 219 220 // ExpectRETimeout returns an error if the given regular expression is not 221 // found in vEMU's serial output within the given timeout. The matched string 222 // is returned. 223 func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) { 224 scaled := time.Duration(float64(timeout) * TimeoutMultiplier) 225 str, _, err := v.gExpect.Expect(pattern, scaled) 226 return str, err 227 }