From 8dd8404789b0f49d199362364d7168932df0ebe2 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Wed, 7 Dec 2022 13:08:13 -0700 Subject: [PATCH 1/6] Refactor finding EFI images This will be useful for a follow up commit, which will attempt to look for a binary relative to QEMU's prefix. It happens to simplify the code as well. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 19 +++---------------- utils.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index eaa86a6..61dd967 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -380,13 +380,11 @@ def get_efi_args(guest_arch): Path("edk2/aarch64/QEMU_EFI.fd"), # Arch Linux (current) Path("edk2-armvirt/aarch64/QEMU_EFI.fd"), # Arch Linux (old) Path("qemu-efi-aarch64/QEMU_EFI.fd"), # Debian and Ubuntu - None # Terminator ], "x86_64": [ Path("edk2/x64/OVMF_CODE.fd"), # Arch Linux (current), Fedora Path("edk2-ovmf/x64/OVMF_CODE.fd"), # Arch Linux (old) Path("OVMF/OVMF_CODE.fd"), # Debian and Ubuntu - None # Terminator ] } # yapf: disable @@ -396,12 +394,8 @@ def get_efi_args(guest_arch): ) return [] - for efi_img_location in efi_img_locations[guest_arch]: - if efi_img_location is None: - raise Exception(f"edk2 could not be found for {guest_arch}!") - efi_img = Path("/usr/share", efi_img_location) - if efi_img.exists(): - break + usr_share = Path('/usr/share') + efi_img = utils.find_first_file(usr_share, efi_img_locations[guest_arch]) if guest_arch == "arm64": # Sizing the images to 64M is recommended by "Prepare the firmware" section at @@ -424,15 +418,8 @@ def get_efi_args(guest_arch): efi_vars_locations = [ Path("edk2/x64/OVMF_VARS.fd"), # Arch Linux and Fedora Path("OVMF/OVMF_VARS.fd"), # Debian and Ubuntu - None # Terminator ] - for efi_vars_location in efi_vars_locations: - if efi_vars_location is None: - raise Exception("OVMF_VARS.fd could not be found!") - efi_vars = Path('/usr/share', efi_vars_location) - if efi_vars.exists(): - break - + efi_vars = utils.find_first_file(usr_share, efi_vars_locations) efi_vars_qemu = base_folder.joinpath("images", guest_arch, efi_vars.name) shutil.copyfile(efi_vars, efi_vars_qemu) diff --git a/utils.py b/utils.py index ddba68e..09dbfba 100755 --- a/utils.py +++ b/utils.py @@ -30,6 +30,29 @@ def die(string): sys.exit(1) +def find_first_file(relative_root, possible_files): + """ + Attempts to find the first option available in the list of files relative + to a specified root folder. + + Parameters: + relative_root (Path): A Path object containing the folder to search for + files within. + possible_files (list): A list of Paths that may be within the relative + root folder. They will be automatically appended + to relative_root. + Returns: + The full path to the first file found in the list. If none could be + found, an Exception is raised. + """ + for possible_file in possible_files: + if (full_path := relative_root.joinpath(possible_file)).exists(): + return full_path + raise Exception( + f"No files from list ('{', '.join(possible_files)}') could be found within '{relative_root}'!" + ) + + def get_full_kernel_path(kernel_location, image, arch=None): """ Get the full path to a kernel image based on the architecture and image From e5f9f809b3f9bed0e4eeb09233e4af84ddabeef2 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Wed, 7 Dec 2022 15:17:42 -0700 Subject: [PATCH 2/6] boot-qemu.py: Add support for passing through a folder to the guest virtiofs, available in QEMU 5.2 or newer and Linux guests 5.4 or newer, is a more modern way to pass local folders along to QEMU, as it takes advantage of the fact that the folders are on the same machine as the hypervisor. To use virtiofs, we first need to run virtiofsd, which is included with most base QEMU packages. Once we find it, we run it in the background and connect to it using some QEMU parameters, which were shamelessly taken from the official virtiofs website: https://virtio-fs.gitlab.io/howto-qemu.html To use it within the guest (you can use a different path than /mnt/shared but 'mount -t virtio shared' must be used): # mkdir /mnt/shared # mount -t virtiofs shared /mnt/shared # echo "$(uname -a)" >/mnt/shared/foo On the host: $ cat shared/foo Linux (none) 6.1.0-rc8-next-20221207 #2 SMP PREEMPT Wed Dec 7 14:56:03 MST 2022 aarch64 GNU/Linux This does require guest kernel support (CONFIG_VIRTIO_FS=y), otherwise it will not work inside the guest: / # mount -t virtiofs shared /mnt/shared mount: mounting shared on /mnt/shared failed: No such device Link: https://github.com/ClangBuiltLinux/boot-utils/issues/81 Signed-off-by: Nathan Chancellor --- .gitignore | 2 ++ boot-qemu.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index b8bf959..ef91c63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ qemu-binaries/ *.pyc +shared/ +.vfsd.* diff --git a/boot-qemu.py b/boot-qemu.py index 61dd967..85056f2 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -2,6 +2,8 @@ # pylint: disable=invalid-name import argparse +import contextlib +import grp import os from pathlib import Path import platform @@ -13,6 +15,7 @@ import utils base_folder = Path(__file__).resolve().parent +shared_folder = base_folder.joinpath('shared') supported_architectures = [ "arm", "arm32_v5", "arm32_v6", "arm32_v7", "arm64", "arm64be", "m68k", "mips", "mipsel", "ppc32", "ppc32_mac", "ppc64", "ppc64le", "riscv", @@ -83,6 +86,12 @@ def parse_arguments(): help= # noqa: E251 "Number of processors for virtual machine. By default, only machines spawned with KVM will use multiple vCPUS." ) + parser.add_argument( + "--share-folder", + action='store_true', + help= # noqa: E251 + f"Share {shared_folder} with the guest using virtiofs (requires interactive, not supported with gdb)." + ) parser.add_argument( "-t", "--timeout", @@ -223,6 +232,7 @@ def setup_cfg(args): * interactive: Whether or not the user is going to be running the machine interactively. * kernel_location: The full path to the kernel image or build folder. + * share_folder_with_guest: Share a folder on the host with a guest. * smp_requested: Whether or not the user specified a value with '--smp'. * smp_value: The value to use with '-smp' (will be used when @@ -248,6 +258,7 @@ def setup_cfg(args): "gdb": args.gdb, "gdb_bin": args.gdb_bin, "interactive": args.interactive or args.gdb, + "share_folder_with_guest": args.share_folder, "smp_requested": args.smp is not None, "smp_value": get_smp_value(args), "timeout": args.timeout, @@ -735,8 +746,58 @@ def launch_qemu(cfg): gdb_bin = cfg["gdb_bin"] kernel_location = cfg["kernel_location"] qemu_cmd = cfg["qemu_cmd"] + share_folder_with_guest = cfg["share_folder_with_guest"] timeout = cfg["timeout"] + if share_folder_with_guest and not interactive: + utils.yellow( + 'Shared folder requested without an interactive session, ignoring...' + ) + share_folder_with_guest = False + if share_folder_with_guest and gdb: + utils.yellow( + 'Shared folder requested during a debugging session, ignoring...') + share_folder_with_guest = False + + if share_folder_with_guest: + shared_folder.mkdir(exist_ok=True, parents=True) + + # If shared folder was requested, we need to search for virtiofsd in + # certain known locations. + qemu_prefix = Path(qemu_cmd[0]).resolve().parent.parent + virtiofsd_locations = [ + Path('libexec', 'virtiofsd'), # Default QEMU installation, Fedora + Path('lib', 'qemu', 'virtiofsd'), # Arch Linux, Debian, Ubuntu + ] + virtiofsd = utils.find_first_file(qemu_prefix, virtiofsd_locations) + + if not (sudo := shutil.which('sudo')): + raise Exception( + 'sudo is required to use virtiofsd but it could not be found!') + utils.green( + 'Requesting sudo permission to run virtiofsd in the background...') + subprocess.run([sudo, 'true'], check=True) + + virtiofsd_log = base_folder.joinpath('.vfsd.log') + virtiofsd_mem = base_folder.joinpath('.vfsd.mem') + virtiofsd_socket = base_folder.joinpath('.vfsd.sock') + virtiofsd_cmd = [ + sudo, + virtiofsd, + f"--socket-group={grp.getgrgid(os.getgid()).gr_name}", + f"--socket-path={virtiofsd_socket}", + '-o', f"source={shared_folder}", + '-o', 'cache=always', + ] # yapf: disable + + qemu_mem = qemu_cmd[qemu_cmd.index('-m') + 1] + qemu_cmd += [ + '-chardev', f"socket,id=char0,path={virtiofsd_socket}", + '-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=shared', + '-object', f"memory-backend-file,id=shm,mem-path={virtiofsd_mem},share=on,size={qemu_mem}", + '-numa', 'node,memdev=shm', + ] # yapf: disable + # Print information about the QEMU binary pretty_print_qemu_info(qemu_cmd[0]) @@ -782,14 +843,31 @@ def launch_qemu(cfg): qemu_cmd = timeout_cmd + stdbuf_cmd + qemu_cmd pretty_print_qemu_cmd(qemu_cmd) - try: - subprocess.run(qemu_cmd, check=True) - except subprocess.CalledProcessError as ex: - if ex.returncode == 124: - utils.red("ERROR: QEMU timed out!") - else: - utils.red("ERROR: QEMU did not exit cleanly!") - sys.exit(ex.returncode) + null_cm = contextlib.nullcontext() + with open(virtiofsd_log, 'w', encoding='utf-8') if share_folder_with_guest else null_cm as vfsd_log, \ + subprocess.Popen(virtiofsd_cmd, stderr=vfsd_log, stdout=vfsd_log) if share_folder_with_guest else null_cm as vfsd_process: + try: + subprocess.run(qemu_cmd, check=True) + except subprocess.CalledProcessError as ex: + if ex.returncode == 124: + utils.red("ERROR: QEMU timed out!") + else: + utils.red("ERROR: QEMU did not exit cleanly!") + # If virtiofsd is dead, it is pretty likely that it was the + # cause of QEMU failing so add to the existing exception using + # 'from'. + if vfsd_process and vfsd_process.poll(): + vfsd_log_txt = virtiofsd_log.read_text( + encoding='utf-8') + raise Exception( + f"virtiofsd failed with: {vfsd_log_txt}") from ex + sys.exit(ex.returncode) + finally: + if vfsd_process: + vfsd_process.kill() + # Delete the memory to save space, it does not have to be + # persistent + virtiofsd_mem.unlink(missing_ok=True) if __name__ == '__main__': From 044bea25211a49975504a1ce3bd3173b635f529a Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Wed, 7 Dec 2022 15:41:16 -0700 Subject: [PATCH 3/6] utils.py: Fix joining possible_files in find_first_file() Fix the following backtrace: Traceback (most recent call last): File ".../boot-qemu.py", line 870, in launch_qemu(config) File ".../boot-qemu.py", line 770, in launch_qemu virtiofsd = utils.find_first_file(qemu_prefix, virtiofsd_locations) File ".../utils.py", line 52, in find_first_file f"No files from list ('{', '.join(possible_files)}') could be found within '{relative_root}'!" TypeError: sequence item 0: expected str instance, PosixPath found Signed-off-by: Nathan Chancellor --- utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 09dbfba..805c872 100755 --- a/utils.py +++ b/utils.py @@ -48,8 +48,10 @@ def find_first_file(relative_root, possible_files): for possible_file in possible_files: if (full_path := relative_root.joinpath(possible_file)).exists(): return full_path + + files_str = "', '".join([str(elem) for elem in possible_files]) raise Exception( - f"No files from list ('{', '.join(possible_files)}') could be found within '{relative_root}'!" + f"No files from list ('{files_str}') could be found within '{relative_root}'!" ) From 6c414760ec81119c09bcd3c9d9567154d4f3a071 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Fri, 9 Dec 2022 13:13:07 -0700 Subject: [PATCH 4/6] boot-qemu.py: Break launch_qemu() into smaller functions launch_qemu() has gotten quite unweildy in terms of indentation level and readability. Fix it by breaking it a part into several different functions, each of which has full documentation behind it to understand what they are doing and why they exist. Take the opportunity to optimize launch_qemu_gdb() a bit by pulling as many unnecessary changes out of the while loop as possible. There are a couple of minor changes around spacing in this, which came about due to changes to the order in which strings are printed. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 308 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 187 insertions(+), 121 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index 85056f2..98acf4d 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -698,7 +698,7 @@ def pretty_print_qemu_info(qemu): qemu_version_string = get_qemu_ver_string(qemu) utils.green(f"QEMU location: \033[0m{qemu_dir}") - utils.green(f"QEMU version: \033[0m{qemu_version_string}\n") + utils.green(f"QEMU version: \033[0m{qemu_version_string}") def pretty_print_qemu_cmd(qemu_cmd): @@ -723,29 +723,159 @@ def pretty_print_qemu_cmd(qemu_cmd): qemu_cmd_pretty += f' {element.split("/")[-1]}' else: qemu_cmd_pretty += f" {element}" - print(f"$ {qemu_cmd_pretty.strip()}", flush=True) + print(f"\n$ {qemu_cmd_pretty.strip()}", flush=True) -def launch_qemu(cfg): +def launch_qemu_gdb(cfg): """ - Runs the QEMU command generated from get_qemu_args(), depending on whether - or not the user wants to debug with GDB. + Spawn QEMU in the background with '-s -S' and call gdb_bin against + 'vmlinux' with the target remote command. This is repeated until the user + quits. - If debugging with GDB, QEMU is called with '-s -S' in the background then - gdb_bin is called against 'vmlinux' connected to the target remote. This - can be repeated multiple times. + Parameters: + cfg (dict): The configuration dictionary generated with setup_cfg(). + """ + gdb_bin = cfg["gdb_bin"] + kernel_location = cfg["kernel_location"] + qemu_cmd = cfg["qemu_cmd"] + ['-s', '-S'] + + if cfg['share_folder_with_guest']: + utils.yellow( + 'Shared folder requested during a debugging session, ignoring...') + + # Make sure necessary commands are present + utils.check_cmd(gdb_bin) + utils.check_cmd('lsof') + + # Generate gdb command and add necessary arguments to QEMU command + gdb_cmd = [ + gdb_bin, + kernel_location.joinpath('vmlinux'), + '-ex', 'target remove :1234' + ] # yapf: disable + + while True: + lsof = subprocess.run(['lsof', '-i:1234'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False) + if lsof.returncode == 0: + utils.die("Port 1234 is already in use, is QEMU running?") + + utils.green("Starting QEMU with GDB connection on port 1234...") + with subprocess.Popen(qemu_cmd, preexec_fn=os.setpgrp) as qemu_process: + utils.green("Starting GDB...") + with subprocess.Popen(gdb_cmd) as gdb_process: + try: + gdb_process.wait() + except KeyboardInterrupt: + pass + + utils.red("Killing QEMU...") + qemu_process.kill() + + answer = input("Re-run QEMU + gdb? [y/n] ") + if answer.lower() == "n": + break + + +def find_virtiofsd(qemu_prefix): + """ + Find virtiofsd relative to qemu_prefix. + + Parameters: + qemu_prefix (Path): A Path object pointing to QEMU's installation prefix. + + Returns: + The full path to virtiofsd. + """ + virtiofsd_locations = [ + Path('libexec', 'virtiofsd'), # Default QEMU installation, Fedora + Path('lib', 'qemu', 'virtiofsd'), # Arch Linux, Debian, Ubuntu + ] + return utils.find_first_file(qemu_prefix, virtiofsd_locations) + + +def get_and_call_sudo(): + """ + Get the full path to a sudo binary and call it to gain sudo permission to + run virtiofsd in the background. virtiofsd is spawned in the background so + we cannot interact with it; getting permission beforehand allows everything + to work properly. + + Returns: + The full path to a suitable sudo binary. + """ + if not (sudo := shutil.which('sudo')): + raise Exception( + 'sudo is required to use virtiofsd but it could not be found!') + utils.green( + 'Requesting sudo permission to run virtiofsd in the background...') + subprocess.run([sudo, 'true'], check=True) + return sudo + + +def get_virtiofsd_cmd(qemu_path, socket_path): + """ + Generate a virtiofsd command suitable for running through + subprocess.Popen(). + + This is the command as recommended by the virtio-fs website: + https://virtio-fs.gitlab.io/howto-qemu.html + + Parameters: + qemu_path (Path): An absolute path to the QEMU binary being used. + socket_path (Path): An absolute path to the socket file virtiofsd + will use to communicate with QEMU. - Otherwise, QEMU is called with 'timeout' so that it is terminated if there - is a problem while booting, passing along any error code that is returned. + Returns: + The virtiofsd command as a list. + """ + sudo = get_and_call_sudo() + virtiofsd = find_virtiofsd(qemu_path.resolve().parent.parent) + + return [ + sudo, + virtiofsd, + f"--socket-group={grp.getgrgid(os.getgid()).gr_name}", + f"--socket-path={socket_path}", + '-o', f"source={shared_folder}", + '-o', 'cache=always', + ] # yapf: disable + + +def get_virtiofs_qemu_args(mem_path, qemu_mem, socket_path): + """ + Generate a list of arguments for QEMU to use virtiofs. + + These are the arguments as recommended by the virtio-fs website: + https://virtio-fs.gitlab.io/howto-qemu.html + + Parameters: + mem_path (Path): An absolute path to the memory file that virtiofs will + be using. + qemu_mem (str): The amount of memory QEMU will be using. + socket_path (Path): An absolute path to the socket file virtiofsd + will use to communicate with QEMU. + """ + return [ + '-chardev', f"socket,id=char0,path={socket_path}", + '-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=shared', + '-object', f"memory-backend-file,id=shm,mem-path={mem_path},share=on,size={qemu_mem}", + '-numa', 'node,memdev=shm', + ] # yapf: disable + + +def launch_qemu_fg(cfg): + """ + Spawn QEMU in the foreground, using 'timeout' if running non-interactively + to see if the kernel successfully gets to userspace. Parameters: cfg (dict): The configuration dictionary generated with setup_cfg(). """ interactive = cfg["interactive"] - gdb = cfg["gdb"] - gdb_bin = cfg["gdb_bin"] - kernel_location = cfg["kernel_location"] - qemu_cmd = cfg["qemu_cmd"] + qemu_cmd = cfg["qemu_cmd"] + ["-serial", "mon:stdio"] share_folder_with_guest = cfg["share_folder_with_guest"] timeout = cfg["timeout"] @@ -754,120 +884,50 @@ def launch_qemu(cfg): 'Shared folder requested without an interactive session, ignoring...' ) share_folder_with_guest = False - if share_folder_with_guest and gdb: - utils.yellow( - 'Shared folder requested during a debugging session, ignoring...') - share_folder_with_guest = False if share_folder_with_guest: shared_folder.mkdir(exist_ok=True, parents=True) - # If shared folder was requested, we need to search for virtiofsd in - # certain known locations. - qemu_prefix = Path(qemu_cmd[0]).resolve().parent.parent - virtiofsd_locations = [ - Path('libexec', 'virtiofsd'), # Default QEMU installation, Fedora - Path('lib', 'qemu', 'virtiofsd'), # Arch Linux, Debian, Ubuntu - ] - virtiofsd = utils.find_first_file(qemu_prefix, virtiofsd_locations) - - if not (sudo := shutil.which('sudo')): - raise Exception( - 'sudo is required to use virtiofsd but it could not be found!') - utils.green( - 'Requesting sudo permission to run virtiofsd in the background...') - subprocess.run([sudo, 'true'], check=True) - virtiofsd_log = base_folder.joinpath('.vfsd.log') virtiofsd_mem = base_folder.joinpath('.vfsd.mem') virtiofsd_socket = base_folder.joinpath('.vfsd.sock') - virtiofsd_cmd = [ - sudo, - virtiofsd, - f"--socket-group={grp.getgrgid(os.getgid()).gr_name}", - f"--socket-path={virtiofsd_socket}", - '-o', f"source={shared_folder}", - '-o', 'cache=always', - ] # yapf: disable - - qemu_mem = qemu_cmd[qemu_cmd.index('-m') + 1] - qemu_cmd += [ - '-chardev', f"socket,id=char0,path={virtiofsd_socket}", - '-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=shared', - '-object', f"memory-backend-file,id=shm,mem-path={virtiofsd_mem},share=on,size={qemu_mem}", - '-numa', 'node,memdev=shm', - ] # yapf: disable - - # Print information about the QEMU binary - pretty_print_qemu_info(qemu_cmd[0]) - if gdb: - utils.check_cmd(gdb_bin) - qemu_cmd += ["-s", "-S"] - - while True: - utils.check_cmd("lsof") - lsof = subprocess.run(["lsof", "-i:1234"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False) - if lsof.returncode == 0: - utils.die("Port 1234 is already in use, is QEMU running?") - - utils.green("Starting QEMU with GDB connection on port 1234...") - with subprocess.Popen(qemu_cmd, - preexec_fn=os.setpgrp) as qemu_process: - utils.green("Starting GDB...") - gdb_cmd = [gdb_bin] - gdb_cmd += [kernel_location.joinpath("vmlinux")] - gdb_cmd += ["-ex", "target remote :1234"] - - with subprocess.Popen(gdb_cmd) as gdb_process: - try: - gdb_process.wait() - except KeyboardInterrupt: - pass - - utils.red("Killing QEMU...") - qemu_process.kill() - - answer = input("Re-run QEMU + gdb? [y/n] ") - if answer.lower() == "n": - break - else: - qemu_cmd += ["-serial", "mon:stdio"] - - if not interactive: - timeout_cmd = ["timeout", "--foreground", timeout] - stdbuf_cmd = ["stdbuf", "-oL", "-eL"] - qemu_cmd = timeout_cmd + stdbuf_cmd + qemu_cmd - - pretty_print_qemu_cmd(qemu_cmd) - null_cm = contextlib.nullcontext() - with open(virtiofsd_log, 'w', encoding='utf-8') if share_folder_with_guest else null_cm as vfsd_log, \ - subprocess.Popen(virtiofsd_cmd, stderr=vfsd_log, stdout=vfsd_log) if share_folder_with_guest else null_cm as vfsd_process: - try: - subprocess.run(qemu_cmd, check=True) - except subprocess.CalledProcessError as ex: - if ex.returncode == 124: - utils.red("ERROR: QEMU timed out!") - else: - utils.red("ERROR: QEMU did not exit cleanly!") - # If virtiofsd is dead, it is pretty likely that it was the - # cause of QEMU failing so add to the existing exception using - # 'from'. - if vfsd_process and vfsd_process.poll(): - vfsd_log_txt = virtiofsd_log.read_text( - encoding='utf-8') - raise Exception( - f"virtiofsd failed with: {vfsd_log_txt}") from ex - sys.exit(ex.returncode) - finally: - if vfsd_process: - vfsd_process.kill() - # Delete the memory to save space, it does not have to be - # persistent - virtiofsd_mem.unlink(missing_ok=True) + virtiofsd_cmd = get_virtiofsd_cmd(Path(qemu_cmd[0]), virtiofsd_socket) + + qemu_mem_val = qemu_cmd[qemu_cmd.index('-m') + 1] + qemu_cmd += get_virtiofs_qemu_args(virtiofsd_mem, qemu_mem_val, + virtiofsd_socket) + + if not interactive: + timeout_cmd = ["timeout", "--foreground", timeout] + stdbuf_cmd = ["stdbuf", "-oL", "-eL"] + qemu_cmd = timeout_cmd + stdbuf_cmd + qemu_cmd + + pretty_print_qemu_cmd(qemu_cmd) + null_cm = contextlib.nullcontext() + with open(virtiofsd_log, 'w', encoding='utf-8') if share_folder_with_guest else null_cm as vfsd_log, \ + subprocess.Popen(virtiofsd_cmd, stderr=vfsd_log, stdout=vfsd_log) if share_folder_with_guest else null_cm as vfsd_process: + try: + subprocess.run(qemu_cmd, check=True) + except subprocess.CalledProcessError as ex: + if ex.returncode == 124: + utils.red("ERROR: QEMU timed out!") + else: + utils.red("ERROR: QEMU did not exit cleanly!") + # If virtiofsd is dead, it is pretty likely that it was the + # cause of QEMU failing so add to the existing exception using + # 'from'. + if vfsd_process and vfsd_process.poll(): + vfsd_log_txt = virtiofsd_log.read_text(encoding='utf-8') + raise Exception( + f"virtiofsd failed with: {vfsd_log_txt}") from ex + sys.exit(ex.returncode) + finally: + if vfsd_process: + vfsd_process.kill() + # Delete the memory to save space, it does not have to be + # persistent + virtiofsd_mem.unlink(missing_ok=True) if __name__ == '__main__': @@ -877,4 +937,10 @@ def launch_qemu(cfg): config = setup_cfg(arguments) config = get_qemu_args(config) - launch_qemu(config) + # Print information about the QEMU binary + pretty_print_qemu_info(config['qemu_cmd'][0]) + + if config['gdb']: + launch_qemu_gdb(config) + else: + launch_qemu_fg(config) From 5bd3cc7a3f1cb9c51dd57c66cff831cb33363bd2 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Fri, 9 Dec 2022 13:42:26 -0700 Subject: [PATCH 5/6] boot-qemu.py: Refactor getting values from configuration This will come in handy in trying to warn people when they are missing configurations needed for certain features. While we are at it, make getting the configuration value a little more robust using re.search() + match groups. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 69 ++++++++++++++++++++++++++++++---------------------- utils.py | 15 ++++++++---- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/boot-qemu.py b/boot-qemu.py index 98acf4d..129e408 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -154,6 +154,42 @@ def can_use_kvm(can_test_for_kvm, guest_arch): return False +def get_config_val(kernel_arg, config_key): + """ + Attempt to get configuration value from a .config relative to + kernel_location. + + Parameters: + kernel_arg (str): The value of the '--kernel' argument. + + Returns: + The configuration value if it can be found, None if not. + """ + # kernel_arg is either a path to the kernel source or a full kernel + # location. If it is a file, we need to strip off the basename so that we + # can easily navigate around with '..'. + if (kernel_dir := Path(kernel_arg)).is_file(): + kernel_dir = kernel_dir.parent + + # If kernel_location is the kernel source, the configuration will be at + # /.config + # + # If kernel_location is a full kernel location, it could either be: + # * /.config (if the image is vmlinux) + # * /../../../.config (if the image is in arch/*/boot/) + # * /config (if the image is in a TuxMake folder) + config_locations = [".config", "../../../.config", "config"] + if (config_file := utils.find_first_file(kernel_dir, + config_locations, + required=False)): + config_txt = config_file.read_text(encoding='utf-8') + if (match := re.search(f"^{config_key}=(.*)$", config_txt, + flags=re.M)): + return match.groups()[0] + + return None + + def get_smp_value(args): """ Get the value of '-smp' based on user input and kernel configuration. @@ -178,41 +214,16 @@ def get_smp_value(args): if args.smp: return args.smp - # kernel_location is either a path to the kernel source or a full kernel - # location. If it is a file, we need to strip off the basename so that we - # can easily navigate around with '..'. - kernel_dir = Path(args.kernel_location) - if kernel_dir.is_file(): - kernel_dir = kernel_dir.parent - - # If kernel_location is the kernel source, the configuration will be at - # /.config - # - # If kernel_location is a full kernel location, it could either be: - # * /.config (if the image is vmlinux) - # * /../../../.config (if the image is in arch/*/boot/) - # * /config (if the image is in a TuxMake folder) - config_file = None - for config_name in [".config", "../../../.config", "config"]: - config_path = kernel_dir.joinpath(config_name) - if config_path.is_file(): - config_file = config_path - break - # Choose a sensible default value based on treewide defaults for # CONFIG_NR_CPUS then get the actual value if possible. - config_nr_cpus = 8 - if config_file: - with open(config_file, encoding='utf-8') as file: - for line in file: - if "CONFIG_NR_CPUS=" in line: - config_nr_cpus = int(line.split("=", 1)[1]) - break + if not (config_nr_cpus := get_config_val(args.kernel_location, + 'CONFIG_NR_CPUS')): + config_nr_cpus = 8 # Use the minimum of the number of usable processors for the script or # CONFIG_NR_CPUS. usable_cpus = os.cpu_count() - return min(usable_cpus, config_nr_cpus) + return min(usable_cpus, int(config_nr_cpus)) def setup_cfg(args): diff --git a/utils.py b/utils.py index 805c872..012696a 100755 --- a/utils.py +++ b/utils.py @@ -30,7 +30,7 @@ def die(string): sys.exit(1) -def find_first_file(relative_root, possible_files): +def find_first_file(relative_root, possible_files, required=True): """ Attempts to find the first option available in the list of files relative to a specified root folder. @@ -41,6 +41,8 @@ def find_first_file(relative_root, possible_files): possible_files (list): A list of Paths that may be within the relative root folder. They will be automatically appended to relative_root. + required (bool): Whether or not the requested file is required for the + script to work properly. Returns: The full path to the first file found in the list. If none could be found, an Exception is raised. @@ -49,10 +51,13 @@ def find_first_file(relative_root, possible_files): if (full_path := relative_root.joinpath(possible_file)).exists(): return full_path - files_str = "', '".join([str(elem) for elem in possible_files]) - raise Exception( - f"No files from list ('{files_str}') could be found within '{relative_root}'!" - ) + if required: + files_str = "', '".join([str(elem) for elem in possible_files]) + raise Exception( + f"No files from list ('{files_str}') could be found within '{relative_root}'!" + ) + + return None def get_full_kernel_path(kernel_location, image, arch=None): From 14adce2fbae85d6441bf5f5bda02ecea288f72b6 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Fri, 9 Dec 2022 13:52:54 -0700 Subject: [PATCH 6/6] boot-qemu.py: Quality of life improvements around virtiofs 1. Warn the user if CONFIG_VIRTIO_FS=y cannot be found in the configuration (or if the configuration cannot be found). 2. Print information about using the shared folder within the guest. Signed-off-by: Nathan Chancellor --- boot-qemu.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/boot-qemu.py b/boot-qemu.py index 129e408..5878e6f 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -886,6 +886,7 @@ def launch_qemu_fg(cfg): cfg (dict): The configuration dictionary generated with setup_cfg(). """ interactive = cfg["interactive"] + kernel_location = cfg["kernel_location"] qemu_cmd = cfg["qemu_cmd"] + ["-serial", "mon:stdio"] share_folder_with_guest = cfg["share_folder_with_guest"] timeout = cfg["timeout"] @@ -897,6 +898,16 @@ def launch_qemu_fg(cfg): share_folder_with_guest = False if share_folder_with_guest: + if not get_config_val(kernel_location, 'CONFIG_VIRTIO_FS'): + utils.yellow( + 'CONFIG_VIRTIO_FS may not be enabled in your configuration, shared folder may not work...' + ) + + # Print information about using shared folder + utils.green('To mount shared folder in guest (e.g. to /mnt/shared):') + utils.green('\t/ # mkdir /mnt/shared') + utils.green('\t/ # mount -t virtiofs shared /mnt/shared') + shared_folder.mkdir(exist_ok=True, parents=True) virtiofsd_log = base_folder.joinpath('.vfsd.log')