Skip to content

Drive an installer

Installers run before there is anything inside the guest to talk to. No SSH, no guest agent, no shell. The tools in this page work at the level QEMU emulates hardware: a framebuffer you can photograph, a keyboard that emits scancodes, and a mouse. That means they work in BIOS menus, bootloaders, partitioners and desktop environments alike, on any operating system, with nothing installed in the guest.

This page assumes a VM is already running (see Your first virtual machine if not).

Every interaction is the same three beats: look, act, look again.

  1. vm_screenshot(name="vm") returns the display as a PNG. Read what is actually on screen.

  2. Send exactly one meaningful action: a keystroke, a line of text, a click.

  3. Screenshot again to see what it did.

The second screenshot is not optional bookkeeping, it is the whole method. A guest takes time to react, and an installer’s next question is rarely the one you predicted. Chaining three blind actions and screenshotting at the end usually means finding out that action one landed somewhere unexpected and actions two and three went into a dialog you did not know was there.

Guests need time. After pressing Enter on a step that formats a filesystem or copies packages, wait before screenshotting again, and if the screen has not changed, wait longer rather than sending the key again. Repeated keystrokes queue up and fire all at once when the guest catches up.

vm_send_keys presses keys and chords. Each entry in keys is one press:

vm_send_keys(name="vm", keys=["down", "down", "ret"])
vm_send_keys(name="vm", keys=["ctrl-alt-f2"])
vm_send_keys(name="vm", keys=["esc"])

A chord is written with hyphens and is pressed together, so "ctrl-c" is one entry, not two. Aliases like enter, space and escape work alongside the QEMU names ret, spc, esc. Function keys are f1 through f12.

Two timing knobs matter when a guest is slow or an installer eats keys during a redraw: hold_ms (default 100) is how long each key is held down, and delay_ms (default 50) is the gap between entries. Raising delay_ms to 200 or so is the usual fix for a menu that seems to miss presses.

vm_type_text types a string character by character, which is what you want for hostnames, passwords and shell commands:

vm_type_text(name="vm", text="setup-alpine", enter=true)

enter=true presses Enter at the end. The layout is US ASCII: printable characters plus tab and newline. There is a 4096 character limit, because every character is a separate round trip to the guest. If you need to put a large file into a guest, wait until the guest agent is available and use guest_file_write instead of typing it.

Many text-mode installers and Linux kernels booted with console=ttyS0 write to the serial port, and mcqemu logs that to a file for every VM it launches:

vm_serial_read(name="vm", tail_lines=100)

Text is much cheaper to read than a screenshot and it carries scrollback, so prefer it when the guest is producing it. Guests that only paint a graphical screen log nothing here, and VMs registered with attach_vm manage their own serial output, so vm_serial_read has nothing to read for those.

Two tools move the pointer, and picking the wrong one wastes a lot of time because the failure is silent. Nothing errors, the click just does not land.

vm_click is the one to try first. It takes pixel coordinates that match what vm_screenshot showed you, and drives an absolute-position tablet device that every VM launched by this server has:

vm_click(name="vm", x=412, y=337)
vm_click(name="vm", x=88, y=120, button="right")
vm_click(name="vm", x=200, y=150, double=true)

Coordinates are validated against the current display size, so a click outside the screen is rejected rather than silently dropped.

This works when the guest has a driver for the tablet device. Modern Linux, Windows 7 and later, and current BSDs do. Screenshot after the click: if the pointer did not move to where you clicked, the guest has no absolute pointer driver and you need the other tool.

vm_mouse_move is for guests without tablet drivers, which in practice means most operating systems older than about 2010, and some installers before their drivers load. It drives the emulated PS/2 mouse, which only understands relative motion: “three pixels left”, never “go to 412, 337”. The guest may also apply its own pointer acceleration, so a request to move 200 pixels can land 260 pixels away.

The reliable pattern is corner, step, verify:

  1. Home the cursor against a corner so you know where it is. Motion is deliberately overshot toward the edge, so the cursor pins there whatever its previous position was.

    vm_mouse_move(name="vm", home="top-left")

    Valid corners are top-left, top-right, bottom-left, bottom-right.

  2. Move toward the target with dx and dy relative to that corner:

    vm_mouse_move(name="vm", home="top-left", dx=412, dy=337)

    Motion is sent in packets no larger than step pixels (default 32) because guests commonly desync or over-accelerate on large deltas. Lower step if a guest behaves erratically; the cost is more round trips.

  3. Screenshot and find the cursor. Acceleration means it is often not where you asked.

  4. Correct with small relative moves, no home this time, until the cursor is on the target:

    vm_mouse_move(name="vm", dx=-14, dy=6)
  5. Screenshot to confirm, then click:

    vm_mouse_move(name="vm", click="left")

    You can also move and click in one call by passing click alongside dx and dy, but only once you trust that guest’s scaling. double=true double-clicks.

Homing costs a screenshot and a burst of motion packets, so for a sequence of clicks in the same area, home once and then work relatively from where you know the cursor is, re-homing whenever you lose track.

  1. Screenshot. The bootloader menu is showing.

  2. vm_send_keys(name="vm", keys=["ret"]) to take the default entry.

  3. Wait, then screenshot. Repeat until the installer’s first screen appears; graphical installers can take a minute to start under emulation.

  4. Screenshot, find the “Next” button, and vm_click on its centre.

  5. Screenshot. If the button highlighted or the page advanced, the tablet works and you can use vm_click for the rest of the installation. If nothing moved, switch to vm_mouse_move with the corner, step, verify pattern.

  6. For text fields, click into the field, screenshot to confirm the caret is there, then vm_type_text. Do not assume focus; installers move it around between pages.

  7. At the partitioning step, screenshot before and after every action, and read the confirmation dialog before answering it.

When the guest is up, install qemu-guest-agent inside it (see Install the guest agent). Screenshots and keystrokes keep working afterwards, and stay the right tool for boot menus and crash screens, but for anything the guest can do itself, guest_exec gives you exit codes and text instead of pixels.