nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
from contextlib import _GeneratorContextManager, nullcontext
|
2021-11-20 01:37:08 +01:00
|
|
|
from pathlib import Path
|
2021-12-06 13:49:23 +01:00
|
|
|
from queue import Queue
|
|
|
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
2020-01-28 08:52:30 +01:00
|
|
|
import base64
|
2021-12-06 13:49:23 +01:00
|
|
|
import io
|
2019-09-06 09:25:22 +02:00
|
|
|
import os
|
2021-12-06 13:49:23 +01:00
|
|
|
import queue
|
2019-09-06 09:25:22 +02:00
|
|
|
import re
|
2023-04-26 00:44:23 +02:00
|
|
|
import select
|
2020-01-28 08:52:30 +01:00
|
|
|
import shlex
|
2019-09-06 09:25:22 +02:00
|
|
|
import shutil
|
|
|
|
import socket
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import tempfile
|
2021-12-06 13:49:23 +01:00
|
|
|
import threading
|
2019-09-06 09:25:22 +02:00
|
|
|
import time
|
2021-12-06 13:49:23 +01:00
|
|
|
|
|
|
|
from test_driver.logger import rootlog
|
2019-09-06 09:25:22 +02:00
|
|
|
|
|
|
|
CHAR_TO_KEY = {
|
|
|
|
"A": "shift-a",
|
|
|
|
"N": "shift-n",
|
|
|
|
"-": "0x0C",
|
|
|
|
"_": "shift-0x0C",
|
|
|
|
"B": "shift-b",
|
|
|
|
"O": "shift-o",
|
|
|
|
"=": "0x0D",
|
|
|
|
"+": "shift-0x0D",
|
|
|
|
"C": "shift-c",
|
|
|
|
"P": "shift-p",
|
|
|
|
"[": "0x1A",
|
|
|
|
"{": "shift-0x1A",
|
|
|
|
"D": "shift-d",
|
|
|
|
"Q": "shift-q",
|
|
|
|
"]": "0x1B",
|
|
|
|
"}": "shift-0x1B",
|
|
|
|
"E": "shift-e",
|
|
|
|
"R": "shift-r",
|
|
|
|
";": "0x27",
|
|
|
|
":": "shift-0x27",
|
|
|
|
"F": "shift-f",
|
|
|
|
"S": "shift-s",
|
|
|
|
"'": "0x28",
|
|
|
|
'"': "shift-0x28",
|
|
|
|
"G": "shift-g",
|
|
|
|
"T": "shift-t",
|
|
|
|
"`": "0x29",
|
|
|
|
"~": "shift-0x29",
|
|
|
|
"H": "shift-h",
|
|
|
|
"U": "shift-u",
|
|
|
|
"\\": "0x2B",
|
|
|
|
"|": "shift-0x2B",
|
|
|
|
"I": "shift-i",
|
|
|
|
"V": "shift-v",
|
|
|
|
",": "0x33",
|
|
|
|
"<": "shift-0x33",
|
|
|
|
"J": "shift-j",
|
|
|
|
"W": "shift-w",
|
|
|
|
".": "0x34",
|
|
|
|
">": "shift-0x34",
|
|
|
|
"K": "shift-k",
|
|
|
|
"X": "shift-x",
|
|
|
|
"/": "0x35",
|
|
|
|
"?": "shift-0x35",
|
|
|
|
"L": "shift-l",
|
|
|
|
"Y": "shift-y",
|
|
|
|
" ": "spc",
|
|
|
|
"M": "shift-m",
|
|
|
|
"Z": "shift-z",
|
|
|
|
"\n": "ret",
|
|
|
|
"!": "shift-0x02",
|
|
|
|
"@": "shift-0x03",
|
|
|
|
"#": "shift-0x04",
|
|
|
|
"$": "shift-0x05",
|
|
|
|
"%": "shift-0x06",
|
|
|
|
"^": "shift-0x07",
|
|
|
|
"&": "shift-0x08",
|
|
|
|
"*": "shift-0x09",
|
|
|
|
"(": "shift-0x0A",
|
|
|
|
")": "shift-0x0B",
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
def make_command(args: list) -> str:
|
|
|
|
return " ".join(map(shlex.quote, (map(str, args))))
|
|
|
|
|
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
def _perform_ocr_on_screenshot(
|
|
|
|
screenshot_path: str, model_ids: Iterable[int]
|
|
|
|
) -> List[str]:
|
|
|
|
if shutil.which("tesseract") is None:
|
|
|
|
raise Exception("OCR requested but enableOCR is false")
|
|
|
|
|
|
|
|
magick_args = (
|
|
|
|
"-filter Catrom -density 72 -resample 300 "
|
|
|
|
+ "-contrast -normalize -despeckle -type grayscale "
|
|
|
|
+ "-sharpen 1 -posterize 3 -negate -gamma 100 "
|
|
|
|
+ "-blur 1x65535"
|
|
|
|
)
|
|
|
|
|
2023-04-26 00:44:23 +02:00
|
|
|
tess_args = "-c debug_file=/dev/null --psm 11"
|
2021-04-23 17:02:36 +02:00
|
|
|
|
2022-12-30 13:09:12 +01:00
|
|
|
cmd = f"convert {magick_args} '{screenshot_path}' 'tiff:{screenshot_path}.tiff'"
|
2021-04-23 17:02:36 +02:00
|
|
|
ret = subprocess.run(cmd, shell=True, capture_output=True)
|
|
|
|
if ret.returncode != 0:
|
|
|
|
raise Exception(f"TIFF conversion failed with exit code {ret.returncode}")
|
|
|
|
|
|
|
|
model_results = []
|
|
|
|
for model_id in model_ids:
|
2022-12-30 13:09:12 +01:00
|
|
|
cmd = f"tesseract '{screenshot_path}.tiff' - {tess_args} --oem '{model_id}'"
|
2021-04-23 17:02:36 +02:00
|
|
|
ret = subprocess.run(cmd, shell=True, capture_output=True)
|
|
|
|
if ret.returncode != 0:
|
|
|
|
raise Exception(f"OCR failed with exit code {ret.returncode}")
|
|
|
|
model_results.append(ret.stdout.decode("utf-8"))
|
|
|
|
|
|
|
|
return model_results
|
|
|
|
|
|
|
|
|
2021-12-06 13:49:23 +01:00
|
|
|
def retry(fn: Callable, timeout: int = 900) -> None:
|
|
|
|
"""Call the given function repeatedly, with 1 second intervals,
|
|
|
|
until it returns True or a timeout is reached.
|
|
|
|
"""
|
|
|
|
|
|
|
|
for _ in range(timeout):
|
|
|
|
if fn(False):
|
|
|
|
return
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
if not fn(True):
|
|
|
|
raise Exception(f"action timed out after {timeout} seconds")
|
|
|
|
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
class StartCommand:
|
2023-05-20 04:10:21 +02:00
|
|
|
"""The Base Start Command knows how to append the necessary
|
2021-06-13 00:47:25 +02:00
|
|
|
runtime qemu options as determined by a particular test driver
|
|
|
|
run. Any such start command is expected to happily receive and
|
|
|
|
append additional qemu args.
|
|
|
|
"""
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
_cmd: str
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
def cmd(
|
|
|
|
self,
|
2021-11-20 01:37:08 +01:00
|
|
|
monitor_socket_path: Path,
|
|
|
|
shell_socket_path: Path,
|
2023-03-15 02:24:25 +01:00
|
|
|
allow_reboot: bool = False,
|
2021-06-13 00:47:25 +02:00
|
|
|
) -> str:
|
|
|
|
display_opts = ""
|
|
|
|
display_available = any(x in os.environ for x in ["DISPLAY", "WAYLAND_DISPLAY"])
|
2021-08-20 12:00:40 +02:00
|
|
|
if not display_available:
|
2021-06-13 00:47:25 +02:00
|
|
|
display_opts += " -nographic"
|
|
|
|
|
|
|
|
# qemu options
|
2023-03-15 02:24:25 +01:00
|
|
|
qemu_opts = (
|
2021-06-13 00:47:25 +02:00
|
|
|
" -device virtio-serial"
|
2023-04-26 00:44:23 +02:00
|
|
|
# Note: virtconsole will map to /dev/hvc0 in Linux guests
|
2021-06-13 00:47:25 +02:00
|
|
|
" -device virtconsole,chardev=shell"
|
|
|
|
" -device virtio-rng-pci"
|
|
|
|
" -serial stdio"
|
|
|
|
)
|
2023-03-15 02:24:25 +01:00
|
|
|
if not allow_reboot:
|
|
|
|
qemu_opts += " -no-reboot"
|
2021-06-13 00:47:25 +02:00
|
|
|
# TODO: qemu script already catpures this env variable, legacy?
|
|
|
|
qemu_opts += " " + os.environ.get("QEMU_OPTS", "")
|
|
|
|
|
|
|
|
return (
|
|
|
|
f"{self._cmd}"
|
|
|
|
f" -monitor unix:{monitor_socket_path}"
|
|
|
|
f" -chardev socket,id=shell,path={shell_socket_path}"
|
|
|
|
f"{qemu_opts}"
|
|
|
|
f"{display_opts}"
|
|
|
|
)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
|
|
|
@staticmethod
|
2021-06-13 00:47:25 +02:00
|
|
|
def build_environment(
|
2021-11-20 01:37:08 +01:00
|
|
|
state_dir: Path,
|
|
|
|
shared_dir: Path,
|
2021-06-13 00:47:25 +02:00
|
|
|
) -> dict:
|
|
|
|
# We make a copy to not update the current environment
|
|
|
|
env = dict(os.environ)
|
|
|
|
env.update(
|
|
|
|
{
|
|
|
|
"TMPDIR": str(state_dir),
|
|
|
|
"SHARED_DIR": str(shared_dir),
|
|
|
|
"USE_TMPDIR": "1",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return env
|
|
|
|
|
|
|
|
def run(
|
|
|
|
self,
|
2021-11-20 01:37:08 +01:00
|
|
|
state_dir: Path,
|
|
|
|
shared_dir: Path,
|
|
|
|
monitor_socket_path: Path,
|
|
|
|
shell_socket_path: Path,
|
2023-03-15 02:24:25 +01:00
|
|
|
allow_reboot: bool,
|
2021-06-13 00:47:25 +02:00
|
|
|
) -> subprocess.Popen:
|
|
|
|
return subprocess.Popen(
|
2023-03-15 02:24:25 +01:00
|
|
|
self.cmd(monitor_socket_path, shell_socket_path, allow_reboot),
|
2022-03-25 11:36:29 +01:00
|
|
|
stdin=subprocess.PIPE,
|
2021-06-13 00:47:25 +02:00
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
shell=True,
|
|
|
|
cwd=state_dir,
|
|
|
|
env=self.build_environment(state_dir, shared_dir),
|
|
|
|
)
|
|
|
|
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
class NixStartScript(StartCommand):
|
|
|
|
"""A start script from nixos/modules/virtualiation/qemu-vm.nix
|
|
|
|
that also satisfies the requirement of the BaseStartCommand.
|
2023-05-20 04:10:21 +02:00
|
|
|
These Nix commands have the particular characteristic that the
|
2021-06-13 00:47:25 +02:00
|
|
|
machine name can be extracted out of them via a regex match.
|
|
|
|
(Admittedly a _very_ implicit contract, evtl. TODO fix)
|
|
|
|
"""
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
def __init__(self, script: str):
|
|
|
|
self._cmd = script
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
@property
|
|
|
|
def machine_name(self) -> str:
|
|
|
|
match = re.search("run-(.+)-vm$", self._cmd)
|
|
|
|
name = "machine"
|
|
|
|
if match:
|
|
|
|
name = match.group(1)
|
|
|
|
return name
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
|
|
|
|
class LegacyStartCommand(StartCommand):
|
|
|
|
"""Used in some places to create an ad-hoc machine instead of
|
|
|
|
using nix test instrumentation + module system for that purpose.
|
|
|
|
Legacy.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
netBackendArgs: Optional[str] = None,
|
|
|
|
netFrontendArgs: Optional[str] = None,
|
2021-11-20 01:37:08 +01:00
|
|
|
hda: Optional[Tuple[Path, str]] = None,
|
2021-06-13 00:47:25 +02:00
|
|
|
cdrom: Optional[str] = None,
|
|
|
|
usb: Optional[str] = None,
|
|
|
|
bios: Optional[str] = None,
|
2022-02-18 02:59:37 +01:00
|
|
|
qemuBinary: Optional[str] = None,
|
2021-06-13 00:47:25 +02:00
|
|
|
qemuFlags: Optional[str] = None,
|
|
|
|
):
|
2022-02-18 02:59:37 +01:00
|
|
|
if qemuBinary is not None:
|
|
|
|
self._cmd = qemuBinary
|
|
|
|
else:
|
|
|
|
self._cmd = "qemu-kvm"
|
|
|
|
|
|
|
|
self._cmd += " -m 384"
|
2021-06-13 00:47:25 +02:00
|
|
|
|
|
|
|
# networking
|
|
|
|
net_backend = "-netdev user,id=net0"
|
|
|
|
net_frontend = "-device virtio-net-pci,netdev=net0"
|
|
|
|
if netBackendArgs is not None:
|
|
|
|
net_backend += "," + netBackendArgs
|
|
|
|
if netFrontendArgs is not None:
|
|
|
|
net_frontend += "," + netFrontendArgs
|
|
|
|
self._cmd += f" {net_backend} {net_frontend}"
|
|
|
|
|
|
|
|
# hda
|
|
|
|
hda_cmd = ""
|
|
|
|
if hda is not None:
|
|
|
|
hda_path = hda[0].resolve()
|
|
|
|
hda_interface = hda[1]
|
|
|
|
if hda_interface == "scsi":
|
|
|
|
hda_cmd += (
|
|
|
|
f" -drive id=hda,file={hda_path},werror=report,if=none"
|
|
|
|
" -device scsi-hd,drive=hda"
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
|
|
|
else:
|
2021-06-13 00:47:25 +02:00
|
|
|
hda_cmd += f" -drive file={hda_path},if={hda_interface},werror=report"
|
|
|
|
self._cmd += hda_cmd
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
# cdrom
|
|
|
|
if cdrom is not None:
|
|
|
|
self._cmd += f" -cdrom {cdrom}"
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
# usb
|
|
|
|
usb_cmd = ""
|
|
|
|
if usb is not None:
|
2021-05-20 21:33:08 +02:00
|
|
|
# https://github.com/qemu/qemu/blob/master/docs/usb2.txt
|
2021-06-13 00:47:25 +02:00
|
|
|
usb_cmd += (
|
|
|
|
" -device usb-ehci"
|
|
|
|
f" -drive id=usbdisk,file={usb},if=none,readonly"
|
|
|
|
" -device usb-storage,drive=usbdisk "
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
2021-06-13 00:47:25 +02:00
|
|
|
self._cmd += usb_cmd
|
|
|
|
|
|
|
|
# bios
|
|
|
|
if bios is not None:
|
|
|
|
self._cmd += f" -bios {bios}"
|
|
|
|
|
|
|
|
# qemu flags
|
|
|
|
if qemuFlags is not None:
|
|
|
|
self._cmd += f" {qemuFlags}"
|
|
|
|
|
|
|
|
|
|
|
|
class Machine:
|
|
|
|
"""A handle to the machine with this name, that also knows how to manage
|
|
|
|
the machine lifecycle with the help of a start script / command."""
|
|
|
|
|
|
|
|
name: str
|
2022-01-07 15:45:30 +01:00
|
|
|
out_dir: Path
|
2021-11-20 01:37:08 +01:00
|
|
|
tmp_dir: Path
|
|
|
|
shared_dir: Path
|
|
|
|
state_dir: Path
|
|
|
|
monitor_path: Path
|
|
|
|
shell_path: Path
|
2021-06-13 00:47:25 +02:00
|
|
|
|
|
|
|
start_command: StartCommand
|
|
|
|
keep_vm_state: bool
|
|
|
|
|
2021-10-19 15:29:19 +02:00
|
|
|
process: Optional[subprocess.Popen]
|
|
|
|
pid: Optional[int]
|
|
|
|
monitor: Optional[socket.socket]
|
|
|
|
shell: Optional[socket.socket]
|
2021-10-19 14:42:27 +02:00
|
|
|
serial_thread: Optional[threading.Thread]
|
2021-06-13 00:47:25 +02:00
|
|
|
|
2021-10-19 15:29:19 +02:00
|
|
|
booted: bool
|
|
|
|
connected: bool
|
2021-06-13 00:47:25 +02:00
|
|
|
# Store last serial console lines for use
|
|
|
|
# of wait_for_console_text
|
|
|
|
last_lines: Queue = Queue()
|
2022-01-02 22:52:17 +01:00
|
|
|
callbacks: List[Callable]
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
def __repr__(self) -> str:
|
|
|
|
return f"<Machine '{self.name}'>"
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2022-01-07 15:45:30 +01:00
|
|
|
out_dir: Path,
|
2021-11-20 01:37:08 +01:00
|
|
|
tmp_dir: Path,
|
2021-06-13 00:47:25 +02:00
|
|
|
start_command: StartCommand,
|
|
|
|
name: str = "machine",
|
|
|
|
keep_vm_state: bool = False,
|
2022-01-02 22:52:17 +01:00
|
|
|
callbacks: Optional[List[Callable]] = None,
|
2021-06-13 00:47:25 +02:00
|
|
|
) -> None:
|
2022-01-07 15:45:30 +01:00
|
|
|
self.out_dir = out_dir
|
2021-06-13 00:47:25 +02:00
|
|
|
self.tmp_dir = tmp_dir
|
|
|
|
self.keep_vm_state = keep_vm_state
|
|
|
|
self.name = name
|
|
|
|
self.start_command = start_command
|
2022-01-02 22:52:17 +01:00
|
|
|
self.callbacks = callbacks if callbacks is not None else []
|
2021-06-13 00:47:25 +02:00
|
|
|
|
|
|
|
# set up directories
|
|
|
|
self.shared_dir = self.tmp_dir / "shared-xchg"
|
|
|
|
self.shared_dir.mkdir(mode=0o700, exist_ok=True)
|
|
|
|
|
|
|
|
self.state_dir = self.tmp_dir / f"vm-state-{self.name}"
|
|
|
|
self.monitor_path = self.state_dir / "monitor"
|
|
|
|
self.shell_path = self.state_dir / "shell"
|
|
|
|
if (not self.keep_vm_state) and self.state_dir.exists():
|
|
|
|
self.cleanup_statedir()
|
|
|
|
self.state_dir.mkdir(mode=0o700, exist_ok=True)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-10-19 15:29:19 +02:00
|
|
|
self.process = None
|
|
|
|
self.pid = None
|
|
|
|
self.monitor = None
|
|
|
|
self.shell = None
|
2021-10-19 14:42:27 +02:00
|
|
|
self.serial_thread = None
|
|
|
|
|
2021-10-19 15:29:19 +02:00
|
|
|
self.booted = False
|
|
|
|
self.connected = False
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
@staticmethod
|
|
|
|
def create_startcommand(args: Dict[str, str]) -> StartCommand:
|
|
|
|
rootlog.warning(
|
2023-05-27 15:03:56 +02:00
|
|
|
"Using legacy create_startcommand(), "
|
|
|
|
"please use proper nix test vm instrumentation, instead "
|
2021-06-13 00:47:25 +02:00
|
|
|
"to generate the appropriate nixos test vm qemu startup script"
|
|
|
|
)
|
|
|
|
hda = None
|
|
|
|
if args.get("hda"):
|
|
|
|
hda_arg: str = args.get("hda", "")
|
2021-11-20 01:37:08 +01:00
|
|
|
hda_arg_path: Path = Path(hda_arg)
|
2021-06-13 00:47:25 +02:00
|
|
|
hda = (hda_arg_path, args.get("hdaInterface", ""))
|
|
|
|
return LegacyStartCommand(
|
|
|
|
netBackendArgs=args.get("netBackendArgs"),
|
|
|
|
netFrontendArgs=args.get("netFrontendArgs"),
|
|
|
|
hda=hda,
|
|
|
|
cdrom=args.get("cdrom"),
|
|
|
|
usb=args.get("usb"),
|
|
|
|
bios=args.get("bios"),
|
2022-02-18 02:59:37 +01:00
|
|
|
qemuBinary=args.get("qemuBinary"),
|
2021-06-13 00:47:25 +02:00
|
|
|
qemuFlags=args.get("qemuFlags"),
|
|
|
|
)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def is_up(self) -> bool:
|
2019-09-06 09:25:22 +02:00
|
|
|
return self.booted and self.connected
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
def log(self, msg: str) -> None:
|
2021-06-13 00:47:25 +02:00
|
|
|
rootlog.log(msg, {"machine": self.name})
|
2020-08-31 00:42:06 +02:00
|
|
|
|
2021-05-15 01:57:11 +02:00
|
|
|
def log_serial(self, msg: str) -> None:
|
2021-06-13 00:47:25 +02:00
|
|
|
rootlog.log_serial(msg, self.name)
|
2021-05-15 01:57:11 +02:00
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
def nested(self, msg: str, attrs: Dict[str, str] = {}) -> _GeneratorContextManager:
|
|
|
|
my_attrs = {"machine": self.name}
|
|
|
|
my_attrs.update(attrs)
|
2021-06-13 00:47:25 +02:00
|
|
|
return rootlog.nested(msg, my_attrs)
|
2020-08-31 00:42:06 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def wait_for_monitor_prompt(self) -> str:
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
assert self.monitor is not None
|
|
|
|
answer = ""
|
|
|
|
while True:
|
|
|
|
undecoded_answer = self.monitor.recv(1024)
|
|
|
|
if not undecoded_answer:
|
|
|
|
break
|
|
|
|
answer += undecoded_answer.decode()
|
|
|
|
if answer.endswith("(qemu) "):
|
|
|
|
break
|
|
|
|
return answer
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def send_monitor_command(self, command: str) -> str:
|
2022-01-02 22:52:17 +01:00
|
|
|
self.run_callbacks()
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
message = f"{command}\n".encode()
|
|
|
|
assert self.monitor is not None
|
|
|
|
self.monitor.send(message)
|
|
|
|
return self.wait_for_monitor_prompt()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-09-02 20:06:03 +02:00
|
|
|
def wait_for_unit(
|
|
|
|
self, unit: str, user: Optional[str] = None, timeout: int = 900
|
|
|
|
) -> None:
|
2019-12-03 08:35:17 +01:00
|
|
|
"""Wait for a systemd unit to get into "active" state.
|
|
|
|
Throws exceptions on "failed" and "inactive" states as well as
|
|
|
|
after timing out.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def check_active(_: Any) -> bool:
|
2019-09-06 09:25:22 +02:00
|
|
|
info = self.get_unit_info(unit, user)
|
|
|
|
state = info["ActiveState"]
|
|
|
|
if state == "failed":
|
2022-12-29 21:47:58 +01:00
|
|
|
raise Exception(f'unit "{unit}" reached state "{state}"')
|
2019-09-06 09:25:22 +02:00
|
|
|
|
|
|
|
if state == "inactive":
|
|
|
|
status, jobs = self.systemctl("list-jobs --full 2>&1", user)
|
|
|
|
if "No jobs" in jobs:
|
2019-11-08 15:01:42 +01:00
|
|
|
info = self.get_unit_info(unit, user)
|
2019-09-06 09:25:22 +02:00
|
|
|
if info["ActiveState"] == state:
|
|
|
|
raise Exception(
|
2022-12-29 21:47:58 +01:00
|
|
|
f'unit "{unit}" is inactive and there are no pending jobs'
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
2019-12-03 08:35:17 +01:00
|
|
|
|
|
|
|
return state == "active"
|
|
|
|
|
2021-11-18 18:10:51 +01:00
|
|
|
with self.nested(
|
2022-12-29 21:47:58 +01:00
|
|
|
f"waiting for unit {unit}"
|
|
|
|
+ (f" with user {user}" if user is not None else "")
|
2021-11-18 18:10:51 +01:00
|
|
|
):
|
2022-09-02 20:06:03 +02:00
|
|
|
retry(check_active, timeout)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def get_unit_info(self, unit: str, user: Optional[str] = None) -> Dict[str, str]:
|
2022-12-29 21:47:58 +01:00
|
|
|
status, lines = self.systemctl(f'--no-pager show "{unit}"', user)
|
2019-09-06 09:25:22 +02:00
|
|
|
if status != 0:
|
2019-11-08 10:46:37 +01:00
|
|
|
raise Exception(
|
2022-12-29 21:47:58 +01:00
|
|
|
f'retrieving systemctl info for unit "{unit}"'
|
|
|
|
+ ("" if user is None else f' under user "{user}"')
|
|
|
|
+ f" failed with exit code {status}"
|
2019-11-08 10:46:37 +01:00
|
|
|
)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
|
|
|
line_pattern = re.compile(r"^([^=]+)=(.*)$")
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def tuple_from_line(line: str) -> Tuple[str, str]:
|
2019-09-06 09:25:22 +02:00
|
|
|
match = line_pattern.match(line)
|
2019-11-08 11:01:29 +01:00
|
|
|
assert match is not None
|
2019-09-06 09:25:22 +02:00
|
|
|
return match[1], match[2]
|
|
|
|
|
|
|
|
return dict(
|
|
|
|
tuple_from_line(line)
|
|
|
|
for line in lines.split("\n")
|
|
|
|
if line_pattern.match(line)
|
|
|
|
)
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def systemctl(self, q: str, user: Optional[str] = None) -> Tuple[int, str]:
|
2019-09-06 09:25:22 +02:00
|
|
|
if user is not None:
|
|
|
|
q = q.replace("'", "\\'")
|
|
|
|
return self.execute(
|
2022-12-29 21:47:58 +01:00
|
|
|
f"su -l {user} --shell /bin/sh -c "
|
|
|
|
"$'XDG_RUNTIME_DIR=/run/user/`id -u` "
|
|
|
|
f"systemctl --user {q}'"
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
2022-12-29 21:47:58 +01:00
|
|
|
return self.execute(f"systemctl {q}")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def require_unit_state(self, unit: str, require_state: str = "active") -> None:
|
2020-08-31 00:42:06 +02:00
|
|
|
with self.nested(
|
2022-12-30 12:11:35 +01:00
|
|
|
f"checking if unit '{unit}' has reached state '{require_state}'"
|
2020-08-31 00:42:06 +02:00
|
|
|
):
|
|
|
|
info = self.get_unit_info(unit)
|
|
|
|
state = info["ActiveState"]
|
|
|
|
if state != require_state:
|
|
|
|
raise Exception(
|
2022-12-30 12:11:35 +01:00
|
|
|
f"Expected unit '{unit}' to to be in state "
|
|
|
|
f"'{require_state}' but it is in state '{state}'"
|
2020-08-31 00:42:06 +02:00
|
|
|
)
|
2020-08-25 15:50:47 +02:00
|
|
|
|
2021-10-24 15:46:45 +02:00
|
|
|
def _next_newline_closed_block_from_shell(self) -> str:
|
|
|
|
assert self.shell
|
|
|
|
output_buffer = []
|
|
|
|
while True:
|
|
|
|
# This receives up to 4096 bytes from the socket
|
|
|
|
chunk = self.shell.recv(4096)
|
|
|
|
if not chunk:
|
|
|
|
# Probably a broken pipe, return the output we have
|
|
|
|
break
|
|
|
|
|
|
|
|
decoded = chunk.decode()
|
|
|
|
output_buffer += [decoded]
|
|
|
|
if decoded[-1] == "\n":
|
|
|
|
break
|
|
|
|
return "".join(output_buffer)
|
|
|
|
|
2021-11-15 21:11:18 +01:00
|
|
|
def execute(
|
2023-05-25 02:24:56 +02:00
|
|
|
self,
|
|
|
|
command: str,
|
|
|
|
check_return: bool = True,
|
|
|
|
check_output: bool = True,
|
|
|
|
timeout: Optional[int] = 900,
|
2021-11-15 21:11:18 +01:00
|
|
|
) -> Tuple[int, str]:
|
2022-01-02 22:52:17 +01:00
|
|
|
self.run_callbacks()
|
2019-09-06 09:25:22 +02:00
|
|
|
self.connect()
|
|
|
|
|
2022-04-07 09:39:33 +02:00
|
|
|
# Always run command with shell opts
|
|
|
|
command = f"set -euo pipefail; {command}"
|
|
|
|
|
2022-04-11 19:16:03 +02:00
|
|
|
timeout_str = ""
|
2021-11-15 21:11:18 +01:00
|
|
|
if timeout is not None:
|
2022-04-11 19:16:03 +02:00
|
|
|
timeout_str = f"timeout {timeout}"
|
2022-04-07 09:39:33 +02:00
|
|
|
|
2023-04-26 00:44:23 +02:00
|
|
|
# While sh is bash on NixOS, this is not the case for every distro.
|
2023-05-20 04:10:21 +02:00
|
|
|
# We explicitly call bash here to allow for the driver to boot other distros as well.
|
2022-04-11 19:16:03 +02:00
|
|
|
out_command = (
|
2023-04-26 00:44:23 +02:00
|
|
|
f"{timeout_str} bash -c {shlex.quote(command)} | (base64 --wrap 0; echo)\n"
|
2022-04-11 19:16:03 +02:00
|
|
|
)
|
2021-11-15 21:11:18 +01:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
assert self.shell
|
2019-09-06 09:25:22 +02:00
|
|
|
self.shell.send(out_command.encode())
|
|
|
|
|
2023-05-25 02:24:56 +02:00
|
|
|
if not check_output:
|
|
|
|
return (-2, "")
|
|
|
|
|
2021-10-24 15:46:45 +02:00
|
|
|
# Get the output
|
|
|
|
output = base64.b64decode(self._next_newline_closed_block_from_shell())
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-10-24 15:46:45 +02:00
|
|
|
if not check_return:
|
|
|
|
return (-1, output.decode())
|
|
|
|
|
|
|
|
# Get the return code
|
|
|
|
self.shell.send("echo ${PIPESTATUS[0]}\n".encode())
|
|
|
|
rc = int(self._next_newline_closed_block_from_shell().strip())
|
|
|
|
|
2023-02-02 13:26:18 +01:00
|
|
|
return (rc, output.decode(errors="replace"))
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-06-20 14:48:38 +02:00
|
|
|
def shell_interact(self, address: Optional[str] = None) -> None:
|
|
|
|
"""Allows you to interact with the guest shell for debugging purposes.
|
2021-06-02 14:19:08 +02:00
|
|
|
|
2022-06-20 14:48:38 +02:00
|
|
|
@address string passed to socat that will be connected to the guest shell.
|
|
|
|
Check the `Running Tests interactivly` chapter of NixOS manual for an example.
|
|
|
|
"""
|
2021-06-02 14:19:08 +02:00
|
|
|
self.connect()
|
2022-06-20 14:48:38 +02:00
|
|
|
|
|
|
|
if address is None:
|
|
|
|
address = "READLINE,prompt=$ "
|
|
|
|
self.log("Terminal is ready (there is no initial prompt):")
|
2021-06-13 00:47:25 +02:00
|
|
|
|
|
|
|
assert self.shell
|
2022-06-20 14:48:38 +02:00
|
|
|
try:
|
|
|
|
subprocess.run(
|
|
|
|
["socat", address, f"FD:{self.shell.fileno()}"],
|
|
|
|
pass_fds=[self.shell.fileno()],
|
|
|
|
)
|
|
|
|
# allow users to cancel this command without breaking the test
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
2021-06-02 14:19:08 +02:00
|
|
|
|
2022-03-25 11:36:29 +01:00
|
|
|
def console_interact(self) -> None:
|
|
|
|
"""Allows you to interact with QEMU's stdin
|
|
|
|
|
|
|
|
The shell can be exited with Ctrl+D. Note that Ctrl+C is not allowed to be used.
|
|
|
|
QEMU's stdout is read line-wise.
|
|
|
|
|
|
|
|
Should only be used during test development, not in the production test."""
|
|
|
|
self.log("Terminal is ready (there is no prompt):")
|
|
|
|
|
|
|
|
assert self.process
|
|
|
|
assert self.process.stdin
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
char = sys.stdin.buffer.read(1)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
break
|
|
|
|
if char == b"": # ctrl+d
|
|
|
|
self.log("Closing connection to the console")
|
|
|
|
break
|
|
|
|
self.send_console(char.decode())
|
|
|
|
|
2021-11-15 21:23:25 +01:00
|
|
|
def succeed(self, *commands: str, timeout: Optional[int] = None) -> str:
|
2019-09-06 09:25:22 +02:00
|
|
|
"""Execute each command and check that it succeeds."""
|
2019-11-11 01:24:49 +01:00
|
|
|
output = ""
|
2019-09-06 09:25:22 +02:00
|
|
|
for command in commands:
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"must succeed: {command}"):
|
2021-11-15 21:23:25 +01:00
|
|
|
(status, out) = self.execute(command, timeout=timeout)
|
2020-08-31 00:42:06 +02:00
|
|
|
if status != 0:
|
2022-12-29 21:47:58 +01:00
|
|
|
self.log(f"output: {out}")
|
|
|
|
raise Exception(f"command `{command}` failed (exit code {status})")
|
2020-08-31 00:42:06 +02:00
|
|
|
output += out
|
2019-11-11 01:24:49 +01:00
|
|
|
return output
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-11-15 21:23:25 +01:00
|
|
|
def fail(self, *commands: str, timeout: Optional[int] = None) -> str:
|
2019-09-06 09:25:22 +02:00
|
|
|
"""Execute each command and check that it fails."""
|
2020-08-21 21:28:24 +02:00
|
|
|
output = ""
|
2019-09-06 09:25:22 +02:00
|
|
|
for command in commands:
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"must fail: {command}"):
|
2021-11-15 21:23:25 +01:00
|
|
|
(status, out) = self.execute(command, timeout=timeout)
|
2020-08-31 00:42:06 +02:00
|
|
|
if status == 0:
|
2022-12-29 21:47:58 +01:00
|
|
|
raise Exception(f"command `{command}` unexpectedly succeeded")
|
2020-08-31 00:42:06 +02:00
|
|
|
output += out
|
2020-08-21 21:28:24 +02:00
|
|
|
return output
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-04-13 05:56:20 +02:00
|
|
|
def wait_until_succeeds(self, command: str, timeout: int = 900) -> str:
|
2019-12-03 08:35:17 +01:00
|
|
|
"""Wait until a command returns success and return its output.
|
|
|
|
Throws an exception on timeout.
|
|
|
|
"""
|
|
|
|
output = ""
|
|
|
|
|
|
|
|
def check_success(_: Any) -> bool:
|
|
|
|
nonlocal output
|
2021-11-15 21:23:25 +01:00
|
|
|
status, output = self.execute(command, timeout=timeout)
|
2019-12-03 08:35:17 +01:00
|
|
|
return status == 0
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for success: {command}"):
|
2020-04-13 05:56:20 +02:00
|
|
|
retry(check_success, timeout)
|
2020-08-31 00:42:06 +02:00
|
|
|
return output
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-11-15 21:23:25 +01:00
|
|
|
def wait_until_fails(self, command: str, timeout: int = 900) -> str:
|
2019-12-03 08:35:17 +01:00
|
|
|
"""Wait until a command returns failure.
|
|
|
|
Throws an exception on timeout.
|
|
|
|
"""
|
|
|
|
output = ""
|
|
|
|
|
|
|
|
def check_failure(_: Any) -> bool:
|
|
|
|
nonlocal output
|
2021-11-15 21:23:25 +01:00
|
|
|
status, output = self.execute(command, timeout=timeout)
|
2019-12-03 08:35:17 +01:00
|
|
|
return status != 0
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for failure: {command}"):
|
2023-06-13 06:11:33 +02:00
|
|
|
retry(check_failure, timeout)
|
2020-08-31 00:42:06 +02:00
|
|
|
return output
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def wait_for_shutdown(self) -> None:
|
2019-09-06 09:25:22 +02:00
|
|
|
if not self.booted:
|
|
|
|
return
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
with self.nested("waiting for the VM to power off"):
|
|
|
|
sys.stdout.flush()
|
2021-06-13 00:47:25 +02:00
|
|
|
assert self.process
|
2020-08-31 00:42:06 +02:00
|
|
|
self.process.wait()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
self.pid = None
|
|
|
|
self.booted = False
|
|
|
|
self.connected = False
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def get_tty_text(self, tty: str) -> str:
|
2019-09-06 09:25:22 +02:00
|
|
|
status, output = self.execute(
|
2022-12-29 21:47:58 +01:00
|
|
|
f"fold -w$(stty -F /dev/tty{tty} size | "
|
|
|
|
f"awk '{{print $2}}') /dev/vcs{tty}"
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
|
|
|
return output
|
|
|
|
|
2019-12-03 08:35:17 +01:00
|
|
|
def wait_until_tty_matches(self, tty: str, regexp: str) -> None:
|
|
|
|
"""Wait until the visible output on the chosen TTY matches regular
|
|
|
|
expression. Throws an exception on timeout.
|
|
|
|
"""
|
2019-09-06 09:25:22 +02:00
|
|
|
matcher = re.compile(regexp)
|
2019-12-03 08:35:17 +01:00
|
|
|
|
|
|
|
def tty_matches(last: bool) -> bool:
|
|
|
|
text = self.get_tty_text(tty)
|
|
|
|
if last:
|
2020-08-31 00:42:06 +02:00
|
|
|
self.log(
|
2019-12-03 08:35:17 +01:00
|
|
|
f"Last chance to match /{regexp}/ on TTY{tty}, "
|
|
|
|
f"which currently contains: {text}"
|
|
|
|
)
|
|
|
|
return len(matcher.findall(text)) > 0
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for {regexp} to appear on tty {tty}"):
|
2020-08-31 00:42:06 +02:00
|
|
|
retry(tty_matches)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-08-02 21:34:37 +02:00
|
|
|
def send_chars(self, chars: str, delay: Optional[float] = 0.01) -> None:
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
with self.nested(f"sending keys {repr(chars)}"):
|
2020-08-31 00:42:06 +02:00
|
|
|
for char in chars:
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
self.send_key(char, delay, log=False)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-12-03 08:35:17 +01:00
|
|
|
def wait_for_file(self, filename: str) -> None:
|
|
|
|
"""Waits until the file exists in machine's file system."""
|
|
|
|
|
|
|
|
def check_file(_: Any) -> bool:
|
2022-12-29 21:47:58 +01:00
|
|
|
status, _ = self.execute(f"test -e {filename}")
|
2019-12-03 08:35:17 +01:00
|
|
|
return status == 0
|
|
|
|
|
2022-12-30 12:11:35 +01:00
|
|
|
with self.nested(f"waiting for file '{filename}'"):
|
2020-08-31 00:42:06 +02:00
|
|
|
retry(check_file)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-12-29 13:33:25 +01:00
|
|
|
def wait_for_open_port(self, port: int, addr: str = "localhost") -> None:
|
2019-11-08 11:01:29 +01:00
|
|
|
def port_is_open(_: Any) -> bool:
|
2022-12-29 21:47:58 +01:00
|
|
|
status, _ = self.execute(f"nc -z {addr} {port}")
|
2019-09-06 09:25:22 +02:00
|
|
|
return status == 0
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for TCP port {port} on {addr}"):
|
2020-08-31 00:42:06 +02:00
|
|
|
retry(port_is_open)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-12-29 13:33:25 +01:00
|
|
|
def wait_for_closed_port(self, port: int, addr: str = "localhost") -> None:
|
2019-11-08 11:01:29 +01:00
|
|
|
def port_is_closed(_: Any) -> bool:
|
2022-12-29 21:47:58 +01:00
|
|
|
status, _ = self.execute(f"nc -z {addr} {port}")
|
2019-09-06 09:25:22 +02:00
|
|
|
return status != 0
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for TCP port {port} on {addr} to be closed"):
|
2021-11-18 18:10:51 +01:00
|
|
|
retry(port_is_closed)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def start_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
|
2022-12-29 21:47:58 +01:00
|
|
|
return self.systemctl(f"start {jobname}", user)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def stop_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
|
2022-12-29 21:47:58 +01:00
|
|
|
return self.systemctl(f"stop {jobname}", user)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-12-03 08:35:17 +01:00
|
|
|
def wait_for_job(self, jobname: str) -> None:
|
|
|
|
self.wait_for_unit(jobname)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def connect(self) -> None:
|
2023-04-26 00:44:23 +02:00
|
|
|
def shell_ready(timeout_secs: int) -> bool:
|
|
|
|
"""We sent some data from the backdoor service running on the guest
|
|
|
|
to indicate that the backdoor shell is ready.
|
|
|
|
As soon as we read some data from the socket here, we assume that
|
|
|
|
our root shell is operational.
|
|
|
|
"""
|
|
|
|
(ready, _, _) = select.select([self.shell], [], [], timeout_secs)
|
|
|
|
return bool(ready)
|
|
|
|
|
2019-09-06 09:25:22 +02:00
|
|
|
if self.connected:
|
|
|
|
return
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
with self.nested("waiting for the VM to finish booting"):
|
|
|
|
self.start()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
assert self.shell
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
tic = time.time()
|
2023-04-26 00:44:23 +02:00
|
|
|
# TODO: do we want to bail after a set number of attempts?
|
|
|
|
while not shell_ready(timeout_secs=30):
|
|
|
|
self.log("Guest root shell did not produce any data yet...")
|
|
|
|
|
|
|
|
self.log(self.shell.recv(1024).decode())
|
2020-08-31 00:42:06 +02:00
|
|
|
toc = time.time()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
self.log("connected to guest root shell")
|
2022-12-29 21:47:58 +01:00
|
|
|
self.log(f"(connecting took {toc - tic:.2f} seconds)")
|
2020-08-31 00:42:06 +02:00
|
|
|
self.connected = True
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def screenshot(self, filename: str) -> None:
|
2023-03-22 16:49:32 +01:00
|
|
|
if "." not in filename:
|
|
|
|
filename += ".png"
|
|
|
|
if "/" not in filename:
|
|
|
|
filename = os.path.join(self.out_dir, filename)
|
2022-12-29 21:47:58 +01:00
|
|
|
tmp = f"{filename}.ppm"
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
with self.nested(
|
2022-12-29 21:47:58 +01:00
|
|
|
f"making screenshot {filename}",
|
2020-08-31 00:42:06 +02:00
|
|
|
{"image": os.path.basename(filename)},
|
|
|
|
):
|
2022-12-29 21:47:58 +01:00
|
|
|
self.send_monitor_command(f"screendump {tmp}")
|
2022-12-30 13:09:12 +01:00
|
|
|
ret = subprocess.run(f"pnmtopng '{tmp}' > '{filename}'", shell=True)
|
2020-08-31 00:42:06 +02:00
|
|
|
os.unlink(tmp)
|
|
|
|
if ret.returncode != 0:
|
|
|
|
raise Exception("Cannot convert screenshot")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-01-28 08:52:30 +01:00
|
|
|
def copy_from_host_via_shell(self, source: str, target: str) -> None:
|
|
|
|
"""Copy a file from the host into the guest by piping it over the
|
|
|
|
shell into the destination file. Works without host-guest shared folder.
|
|
|
|
Prefer copy_from_host for whenever possible.
|
|
|
|
"""
|
|
|
|
with open(source, "rb") as fh:
|
|
|
|
content_b64 = base64.b64encode(fh.read()).decode()
|
|
|
|
self.succeed(
|
|
|
|
f"mkdir -p $(dirname {target})",
|
|
|
|
f"echo -n {content_b64} | base64 -d > {target}",
|
|
|
|
)
|
|
|
|
|
|
|
|
def copy_from_host(self, source: str, target: str) -> None:
|
|
|
|
"""Copy a file from the host into the guest via the `shared_dir` shared
|
|
|
|
among all the VMs (using a temporary directory).
|
|
|
|
"""
|
2021-11-20 01:37:08 +01:00
|
|
|
host_src = Path(source)
|
|
|
|
vm_target = Path(target)
|
2020-01-28 08:52:30 +01:00
|
|
|
with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
|
2021-11-20 01:37:08 +01:00
|
|
|
shared_temp = Path(shared_td)
|
2020-01-28 08:52:30 +01:00
|
|
|
host_intermediate = shared_temp / host_src.name
|
2021-11-20 01:37:08 +01:00
|
|
|
vm_shared_temp = Path("/tmp/shared") / shared_temp.name
|
2020-01-28 08:52:30 +01:00
|
|
|
vm_intermediate = vm_shared_temp / host_src.name
|
|
|
|
|
|
|
|
self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
|
|
|
|
if host_src.is_dir():
|
|
|
|
shutil.copytree(host_src, host_intermediate)
|
|
|
|
else:
|
|
|
|
shutil.copy(host_src, host_intermediate)
|
|
|
|
self.succeed(make_command(["mkdir", "-p", vm_target.parent]))
|
|
|
|
self.succeed(make_command(["cp", "-r", vm_intermediate, vm_target]))
|
|
|
|
|
2019-11-25 00:03:16 +01:00
|
|
|
def copy_from_vm(self, source: str, target_dir: str = "") -> None:
|
|
|
|
"""Copy a file from the VM (specified by an in-VM source path) to a path
|
|
|
|
relative to `$out`. The file is copied via the `shared_dir` shared among
|
|
|
|
all the VMs (using a temporary directory).
|
|
|
|
"""
|
|
|
|
# Compute the source, target, and intermediate shared file names
|
2021-11-20 01:37:08 +01:00
|
|
|
vm_src = Path(source)
|
2019-11-25 00:03:16 +01:00
|
|
|
with tempfile.TemporaryDirectory(dir=self.shared_dir) as shared_td:
|
2021-11-20 01:37:08 +01:00
|
|
|
shared_temp = Path(shared_td)
|
|
|
|
vm_shared_temp = Path("/tmp/shared") / shared_temp.name
|
2019-11-25 00:03:16 +01:00
|
|
|
vm_intermediate = vm_shared_temp / vm_src.name
|
|
|
|
intermediate = shared_temp / vm_src.name
|
|
|
|
# Copy the file to the shared directory inside VM
|
|
|
|
self.succeed(make_command(["mkdir", "-p", vm_shared_temp]))
|
|
|
|
self.succeed(make_command(["cp", "-r", vm_src, vm_intermediate]))
|
2022-01-07 15:45:30 +01:00
|
|
|
abs_target = self.out_dir / target_dir / vm_src.name
|
2019-11-25 00:03:16 +01:00
|
|
|
abs_target.parent.mkdir(exist_ok=True, parents=True)
|
|
|
|
# Copy the file from the shared directory outside VM
|
|
|
|
if intermediate.is_dir():
|
|
|
|
shutil.copytree(intermediate, abs_target)
|
|
|
|
else:
|
|
|
|
shutil.copy(intermediate, abs_target)
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def dump_tty_contents(self, tty: str) -> None:
|
2020-11-21 09:50:03 +01:00
|
|
|
"""Debugging: Dump the contents of the TTY<n>"""
|
2022-12-29 21:47:58 +01:00
|
|
|
self.execute(f"fold -w 80 /dev/vcs{tty} | systemd-cat")
|
2019-11-06 22:40:02 +01:00
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
def _get_screen_text_variants(self, model_ids: Iterable[int]) -> List[str]:
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
screenshot_path = os.path.join(tmpdir, "ppm")
|
|
|
|
self.send_monitor_command(f"screendump {screenshot_path}")
|
|
|
|
return _perform_ocr_on_screenshot(screenshot_path, model_ids)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
def get_screen_text_variants(self) -> List[str]:
|
|
|
|
return self._get_screen_text_variants([0, 1, 2])
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
def get_screen_text(self) -> str:
|
|
|
|
return self._get_screen_text_variants([2])[0]
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def wait_for_text(self, regex: str) -> None:
|
|
|
|
def screen_matches(last: bool) -> bool:
|
2021-04-23 17:02:36 +02:00
|
|
|
variants = self.get_screen_text_variants()
|
|
|
|
for text in variants:
|
|
|
|
if re.search(regex, text) is not None:
|
|
|
|
return True
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
if last:
|
2022-12-29 21:47:58 +01:00
|
|
|
self.log(f"Last OCR attempt failed. Text was: {variants}")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-04-23 17:02:36 +02:00
|
|
|
return False
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for {regex} to appear on screen"):
|
2020-08-31 00:42:06 +02:00
|
|
|
retry(screen_matches)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2023-05-28 00:07:43 +02:00
|
|
|
def wait_for_console_text(self, regex: str, timeout: int | None = None) -> None:
|
2023-05-27 23:54:42 +02:00
|
|
|
"""
|
2023-05-28 13:49:23 +02:00
|
|
|
Wait for the provided regex to appear on console.
|
|
|
|
For each reads,
|
2023-05-27 23:54:42 +02:00
|
|
|
|
2023-05-28 13:49:23 +02:00
|
|
|
If timeout is None, timeout is infinite.
|
2023-05-27 23:54:42 +02:00
|
|
|
|
2023-05-28 13:49:23 +02:00
|
|
|
`timeout` is in seconds.
|
2023-05-27 23:54:42 +02:00
|
|
|
"""
|
2023-05-28 00:07:43 +02:00
|
|
|
# Buffer the console output, this is needed
|
|
|
|
# to match multiline regexes.
|
|
|
|
console = io.StringIO()
|
2023-05-28 13:49:23 +02:00
|
|
|
|
2023-06-15 12:47:49 +02:00
|
|
|
def console_matches(_: Any) -> bool:
|
2023-05-28 00:07:43 +02:00
|
|
|
nonlocal console
|
|
|
|
try:
|
|
|
|
# This will return as soon as possible and
|
|
|
|
# sleep 1 second.
|
|
|
|
console.write(self.last_lines.get(block=False))
|
|
|
|
except queue.Empty:
|
|
|
|
pass
|
|
|
|
console.seek(0)
|
|
|
|
matches = re.search(regex, console.read())
|
2023-05-28 13:49:23 +02:00
|
|
|
return matches is not None
|
2023-05-28 00:07:43 +02:00
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
with self.nested(f"waiting for {regex} to appear on console"):
|
2023-05-28 00:07:43 +02:00
|
|
|
if timeout is not None:
|
|
|
|
retry(console_matches, timeout)
|
|
|
|
else:
|
2023-06-15 12:47:49 +02:00
|
|
|
while not console_matches(False):
|
2023-05-28 00:07:43 +02:00
|
|
|
pass
|
2020-06-13 12:04:05 +02:00
|
|
|
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
def send_key(
|
|
|
|
self, key: str, delay: Optional[float] = 0.01, log: Optional[bool] = True
|
|
|
|
) -> None:
|
2019-09-06 09:25:22 +02:00
|
|
|
key = CHAR_TO_KEY.get(key, key)
|
nixos/test-driver: drop logging from Machine.send_monitor_command
Several machine operations, like `send_chars` and `send_key`, are
implemented by calling `send_monitor_command`, possibly multiple times.
This generates a huge amount of unnecessary noise in the log, because
`send_monitor_command` is a low-level operation and an implementation
detail.
Here's an excerpt from a highlighted log before and afte the change.
Before:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey p[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey p, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey spc[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey spc, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey g[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey g, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey n[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey n, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey 0x0C[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey 0x0C, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey k[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey k, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey e[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey e, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey y[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey y, in 0.00 seconds)
machine: [1m[32msending monitor command: sendkey ret[0m[0m
machine: [1m[32mwaiting for monitor prompt[0m[0m
(finished: waiting for monitor prompt, in 0.00 seconds)
(finished: sending monitor command: sendkey ret, in 0.00 seconds)
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
After:
[1m[32msubtest: Can generate a PGP key[0m[0m
machine: [1m[32msending keys 'gpg --gen-key\n'[0m[0m
(finished: sending keys 'gpg --gen-key\n', in 0.15 seconds)
2023-02-02 13:19:10 +01:00
|
|
|
context = self.nested(f"sending key {repr(key)}") if log else nullcontext()
|
|
|
|
with context:
|
|
|
|
self.send_monitor_command(f"sendkey {key}")
|
|
|
|
if delay is not None:
|
|
|
|
time.sleep(delay)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2022-03-25 11:36:29 +01:00
|
|
|
def send_console(self, chars: str) -> None:
|
|
|
|
assert self.process
|
|
|
|
assert self.process.stdin
|
|
|
|
self.process.stdin.write(chars.encode())
|
|
|
|
self.process.stdin.flush()
|
|
|
|
|
2023-03-16 01:50:15 +01:00
|
|
|
def start(self, allow_reboot: bool = False) -> None:
|
2019-09-06 09:25:22 +02:00
|
|
|
if self.booted:
|
|
|
|
return
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
self.log("starting vm")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-11-20 01:37:08 +01:00
|
|
|
def clear(path: Path) -> Path:
|
2021-06-13 00:47:25 +02:00
|
|
|
if path.exists():
|
|
|
|
path.unlink()
|
|
|
|
return path
|
|
|
|
|
2021-11-20 01:37:08 +01:00
|
|
|
def create_socket(path: Path) -> socket.socket:
|
2019-09-06 09:25:22 +02:00
|
|
|
s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
|
2021-06-13 00:47:25 +02:00
|
|
|
s.bind(str(path))
|
2019-09-06 09:25:22 +02:00
|
|
|
s.listen(1)
|
|
|
|
return s
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
monitor_socket = create_socket(clear(self.monitor_path))
|
|
|
|
shell_socket = create_socket(clear(self.shell_path))
|
|
|
|
self.process = self.start_command.run(
|
|
|
|
self.state_dir,
|
|
|
|
self.shared_dir,
|
|
|
|
self.monitor_path,
|
|
|
|
self.shell_path,
|
2023-03-16 01:50:15 +01:00
|
|
|
allow_reboot,
|
2019-09-06 09:25:22 +02:00
|
|
|
)
|
2021-06-13 00:47:25 +02:00
|
|
|
self.monitor, _ = monitor_socket.accept()
|
|
|
|
self.shell, _ = shell_socket.accept()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-06-13 12:04:05 +02:00
|
|
|
# Store last serial console lines for use
|
|
|
|
# of wait_for_console_text
|
2020-08-31 00:42:06 +02:00
|
|
|
self.last_lines: Queue = Queue()
|
2020-06-13 12:04:05 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def process_serial_output() -> None:
|
2021-06-13 00:47:25 +02:00
|
|
|
assert self.process
|
|
|
|
assert self.process.stdout
|
2019-11-08 11:01:29 +01:00
|
|
|
for _line in self.process.stdout:
|
2020-01-14 19:18:17 +01:00
|
|
|
# Ignore undecodable bytes that may occur in boot menus
|
|
|
|
line = _line.decode(errors="ignore").replace("\r", "").rstrip()
|
2020-06-13 12:04:05 +02:00
|
|
|
self.last_lines.put(line)
|
2021-05-15 01:57:11 +02:00
|
|
|
self.log_serial(line)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2021-10-19 14:42:27 +02:00
|
|
|
self.serial_thread = threading.Thread(target=process_serial_output)
|
|
|
|
self.serial_thread.start()
|
2019-09-06 09:25:22 +02:00
|
|
|
|
|
|
|
self.wait_for_monitor_prompt()
|
|
|
|
|
|
|
|
self.pid = self.process.pid
|
|
|
|
self.booted = True
|
|
|
|
|
2022-12-29 21:47:58 +01:00
|
|
|
self.log(f"QEMU running (pid {self.pid})")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2020-06-18 13:23:32 +02:00
|
|
|
def cleanup_statedir(self) -> None:
|
2021-06-13 00:47:25 +02:00
|
|
|
shutil.rmtree(self.state_dir)
|
|
|
|
rootlog.log(f"deleting VM state directory {self.state_dir}")
|
|
|
|
rootlog.log("if you want to keep the VM state, pass --keep-vm-state")
|
2020-06-18 13:23:32 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def shutdown(self) -> None:
|
2019-11-06 16:06:43 +01:00
|
|
|
if not self.booted:
|
2019-09-06 09:25:22 +02:00
|
|
|
return
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
assert self.shell
|
2019-09-06 09:25:22 +02:00
|
|
|
self.shell.send("poweroff\n".encode())
|
|
|
|
self.wait_for_shutdown()
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def crash(self) -> None:
|
2019-11-06 16:06:43 +01:00
|
|
|
if not self.booted:
|
2019-09-06 09:25:22 +02:00
|
|
|
return
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
self.log("forced crash")
|
2019-09-06 09:25:22 +02:00
|
|
|
self.send_monitor_command("quit")
|
|
|
|
self.wait_for_shutdown()
|
|
|
|
|
2023-03-16 00:30:01 +01:00
|
|
|
def reboot(self) -> None:
|
|
|
|
"""Press Ctrl+Alt+Delete in the guest.
|
|
|
|
|
|
|
|
Prepares the machine to be reconnected which is useful if the
|
2023-03-16 01:50:15 +01:00
|
|
|
machine was started with `allow_reboot = True`
|
2023-03-16 00:30:01 +01:00
|
|
|
"""
|
2023-04-26 00:44:23 +02:00
|
|
|
self.send_key("ctrl-alt-delete")
|
2023-03-16 00:30:01 +01:00
|
|
|
self.connected = False
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def wait_for_x(self) -> None:
|
2019-09-06 09:25:22 +02:00
|
|
|
"""Wait until it is possible to connect to the X server. Note that
|
|
|
|
testing the existence of /tmp/.X11-unix/X0 is insufficient.
|
|
|
|
"""
|
2019-12-03 08:35:17 +01:00
|
|
|
|
|
|
|
def check_x(_: Any) -> bool:
|
|
|
|
cmd = (
|
|
|
|
"journalctl -b SYSLOG_IDENTIFIER=systemd | "
|
|
|
|
+ 'grep "Reached target Current graphical"'
|
|
|
|
)
|
|
|
|
status, _ = self.execute(cmd)
|
|
|
|
if status != 0:
|
|
|
|
return False
|
|
|
|
status, _ = self.execute("[ -e /tmp/.X11-unix/X0 ]")
|
|
|
|
return status == 0
|
|
|
|
|
2020-08-31 00:42:06 +02:00
|
|
|
with self.nested("waiting for the X11 server"):
|
|
|
|
retry(check_x)
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def get_window_names(self) -> List[str]:
|
2019-11-05 15:59:29 +01:00
|
|
|
return self.succeed(
|
|
|
|
r"xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'"
|
|
|
|
).splitlines()
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def wait_for_window(self, regexp: str) -> None:
|
2019-11-05 15:59:29 +01:00
|
|
|
pattern = re.compile(regexp)
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def window_is_visible(last_try: bool) -> bool:
|
2019-11-05 15:59:29 +01:00
|
|
|
names = self.get_window_names()
|
|
|
|
if last_try:
|
2020-08-31 00:42:06 +02:00
|
|
|
self.log(
|
2022-12-29 21:47:58 +01:00
|
|
|
f"Last chance to match {regexp} on the window list,"
|
2020-08-31 00:42:06 +02:00
|
|
|
+ " which currently contains: "
|
|
|
|
+ ", ".join(names)
|
2019-11-05 15:59:29 +01:00
|
|
|
)
|
|
|
|
return any(pattern.search(name) for name in names)
|
|
|
|
|
2021-11-18 18:10:51 +01:00
|
|
|
with self.nested("waiting for a window to appear"):
|
2020-08-31 00:42:06 +02:00
|
|
|
retry(window_is_visible)
|
2019-11-05 15:59:29 +01:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def sleep(self, secs: int) -> None:
|
2020-08-29 00:47:17 +02:00
|
|
|
# We want to sleep in *guest* time, not *host* time.
|
|
|
|
self.succeed(f"sleep {secs}")
|
2019-09-06 09:25:22 +02:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def forward_port(self, host_port: int = 8080, guest_port: int = 80) -> None:
|
2019-11-06 22:41:03 +01:00
|
|
|
"""Forward a TCP port on the host to a TCP port on the guest.
|
|
|
|
Useful during interactive testing.
|
|
|
|
"""
|
2022-12-29 21:47:58 +01:00
|
|
|
self.send_monitor_command(f"hostfwd_add tcp::{host_port}-:{guest_port}")
|
2019-11-06 22:41:03 +01:00
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def block(self) -> None:
|
2019-09-06 09:25:22 +02:00
|
|
|
"""Make the machine unreachable by shutting down eth1 (the multicast
|
|
|
|
interface used to talk to the other VMs). We keep eth0 up so that
|
|
|
|
the test driver can continue to talk to the machine.
|
|
|
|
"""
|
|
|
|
self.send_monitor_command("set_link virtio-net-pci.1 off")
|
|
|
|
|
2019-11-08 11:01:29 +01:00
|
|
|
def unblock(self) -> None:
|
2020-11-21 09:50:03 +01:00
|
|
|
"""Make the machine reachable."""
|
2019-09-06 09:25:22 +02:00
|
|
|
self.send_monitor_command("set_link virtio-net-pci.1 on")
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
def release(self) -> None:
|
|
|
|
if self.pid is None:
|
|
|
|
return
|
|
|
|
rootlog.info(f"kill machine (pid {self.pid})")
|
|
|
|
assert self.process
|
|
|
|
assert self.shell
|
|
|
|
assert self.monitor
|
2021-10-19 14:42:27 +02:00
|
|
|
assert self.serial_thread
|
|
|
|
|
2021-06-13 00:47:25 +02:00
|
|
|
self.process.terminate()
|
|
|
|
self.shell.close()
|
|
|
|
self.monitor.close()
|
2021-10-19 14:42:27 +02:00
|
|
|
self.serial_thread.join()
|
2022-01-02 22:52:17 +01:00
|
|
|
|
|
|
|
def run_callbacks(self) -> None:
|
|
|
|
for callback in self.callbacks:
|
|
|
|
callback()
|