From 0e8622c491cbb58e6189dc75887a1db63b8e2860 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 6 Mar 2024 16:32:19 +0700 Subject: [PATCH] clan_vm_manager: Fix switch desync when pressed too fast. Secrets folder shared between build and run. clan_cli: run_vm now can have custom tmpdir location --- pkgs/clan-cli/clan_cli/vms/run.py | 130 ++++++++++-------- pkgs/clan-vm-manager/clan_vm_manager/app.py | 3 +- .../clan_vm_manager/components/vmobj.py | 18 ++- .../clan_vm_manager/singletons/use_join.py | 2 - 4 files changed, 86 insertions(+), 67 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index f58db06c0..b4751cb46 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -101,7 +101,13 @@ def prepare_disk( return disk_img -def run_vm(vm: VmConfig, nix_options: list[str] = []) -> None: +def run_vm( + vm: VmConfig, + *, + cachedir: Path | None = None, + socketdir: Path | None = None, + nix_options: list[str] = [], +) -> None: machine = Machine(vm.machine_name, vm.flake_url) log.debug(f"Creating VM for {machine}") @@ -109,72 +115,78 @@ def run_vm(vm: VmConfig, nix_options: list[str] = []) -> None: # otherwise, when using /tmp, we risk running out of memory cache = user_cache_dir() / "clan" cache.mkdir(exist_ok=True) - with TemporaryDirectory(dir=cache) as cachedir, TemporaryDirectory() as sockets: - tmpdir = Path(cachedir) - # TODO: We should get this from the vm argument - nixos_config = build_vm(machine, tmpdir, nix_options) + if cachedir is None: + cache_tmp = TemporaryDirectory(dir=cache) + cachedir = Path(cache_tmp.name) - state_dir = vm_state_dir(str(vm.flake_url), machine.name) - state_dir.mkdir(parents=True, exist_ok=True) + if socketdir is None: + socket_tmp = TemporaryDirectory() + socketdir = Path(socket_tmp.name) - # specify socket files for qmp and qga - qmp_socket_file = Path(sockets) / "qmp.sock" - qga_socket_file = Path(sockets) / "qga.sock" - # Create symlinks to the qmp/qga sockets to be able to find them later. - # This indirection is needed because we cannot put the sockets directly - # in the state_dir. - # The reason is, qemu has a length limit of 108 bytes for the qmp socket - # path which is violated easily. - qmp_link = state_dir / "qmp.sock" - if os.path.lexists(qmp_link): - qmp_link.unlink() - qmp_link.symlink_to(qmp_socket_file) + # TODO: We should get this from the vm argument + nixos_config = build_vm(machine, cachedir, nix_options) - qga_link = state_dir / "qga.sock" - if os.path.lexists(qga_link): - qga_link.unlink() - qga_link.symlink_to(qga_socket_file) + state_dir = vm_state_dir(str(vm.flake_url), machine.name) + state_dir.mkdir(parents=True, exist_ok=True) - rootfs_img = prepare_disk(tmpdir) - state_img = state_dir / "state.qcow2" - if not state_img.exists(): - state_img = prepare_disk( - directory=state_dir, - file_name="state.qcow2", - size="50G", - ) - virtiofsd_socket = Path(sockets) / "virtiofsd.sock" - qemu_cmd = qemu_command( - vm, - nixos_config, - secrets_dir=Path(nixos_config["secrets_dir"]), - rootfs_img=rootfs_img, - state_img=state_img, - virtiofsd_socket=virtiofsd_socket, - qmp_socket_file=qmp_socket_file, - qga_socket_file=qga_socket_file, + # specify socket files for qmp and qga + qmp_socket_file = socketdir / "qmp.sock" + qga_socket_file = socketdir / "qga.sock" + # Create symlinks to the qmp/qga sockets to be able to find them later. + # This indirection is needed because we cannot put the sockets directly + # in the state_dir. + # The reason is, qemu has a length limit of 108 bytes for the qmp socket + # path which is violated easily. + qmp_link = state_dir / "qmp.sock" + if os.path.lexists(qmp_link): + qmp_link.unlink() + qmp_link.symlink_to(qmp_socket_file) + + qga_link = state_dir / "qga.sock" + if os.path.lexists(qga_link): + qga_link.unlink() + qga_link.symlink_to(qga_socket_file) + + rootfs_img = prepare_disk(cachedir) + state_img = state_dir / "state.qcow2" + if not state_img.exists(): + state_img = prepare_disk( + directory=state_dir, + file_name="state.qcow2", + size="50G", ) + virtiofsd_socket = socketdir / "virtiofsd.sock" + qemu_cmd = qemu_command( + vm, + nixos_config, + secrets_dir=Path(nixos_config["secrets_dir"]), + rootfs_img=rootfs_img, + state_img=state_img, + virtiofsd_socket=virtiofsd_socket, + qmp_socket_file=qmp_socket_file, + qga_socket_file=qga_socket_file, + ) - packages = ["nixpkgs#qemu"] + packages = ["nixpkgs#qemu"] - env = os.environ.copy() - if vm.graphics and not vm.waypipe: - packages.append("nixpkgs#virt-viewer") - remote_viewer_mimetypes = module_root() / "vms" / "mimetypes" - env[ - "XDG_DATA_DIRS" - ] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" + env = os.environ.copy() + if vm.graphics and not vm.waypipe: + packages.append("nixpkgs#virt-viewer") + remote_viewer_mimetypes = module_root() / "vms" / "mimetypes" + env[ + "XDG_DATA_DIRS" + ] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" - with start_waypipe( - qemu_cmd.vsock_cid, f"[{vm.machine_name}] " - ), start_virtiofsd(virtiofsd_socket): - run( - nix_shell(packages, qemu_cmd.args), - env=env, - log=Log.BOTH, - error_msg=f"Could not start vm {machine}", - ) + with start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "), start_virtiofsd( + virtiofsd_socket + ): + run( + nix_shell(packages, qemu_cmd.args), + env=env, + log=Log.BOTH, + error_msg=f"Could not start vm {machine}", + ) @dataclass @@ -196,7 +208,7 @@ def run_command(args: argparse.Namespace) -> None: vm = inspect_vm(machine=machine) - run_vm(vm, run_options.nix_options) + run_vm(vm, nix_options=run_options.nix_options) def register_run_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index dd965265e..11d61e1f6 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -54,7 +54,8 @@ class MainApplication(Adw.Application): def on_shutdown(self, source: "MainApplication") -> None: log.debug("Shutting down Adw.Application") - log.debug(f"get_windows: {self.get_windows()}") + if self.get_windows() == []: + log.warning("No windows to destroy") if self.window: # TODO: Doesn't seem to raise the destroy signal. Need to investigate # self.get_windows() returns an empty list. Desync between window and application? diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index baaa0b3d1..a713d6434 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -14,7 +14,6 @@ from typing import IO, ClassVar import gi from clan_cli import vms from clan_cli.clan_uri import ClanScheme, ClanURI -from clan_cli.errors import ClanError from clan_cli.history.add import HistoryEntry from clan_cli.machines.machines import Machine @@ -102,6 +101,7 @@ class VMObject(GObject.Object): def _on_switch_toggle(self, switch: Gtk.Switch, user_state: bool) -> None: if switch.get_active(): switch.set_state(False) + switch.set_sensitive(False) self.start() else: switch.set_state(True) @@ -142,6 +142,8 @@ class VMObject(GObject.Object): tstart = datetime.now() log.info(f"Building VM {self.get_id()}") log_dir = Path(str(self.log_dir.name)) + + # Start the build process self.build_process = spawn( on_except=None, out_file=log_dir / "build.log", @@ -150,7 +152,7 @@ class VMObject(GObject.Object): tmpdir=log_dir, ) GLib.idle_add(self._vm_status_changed_task) - + self.switch.set_sensitive(True) # Start the logs watcher self._logs_id = GLib.timeout_add( 50, self._get_logs_task, self.build_process @@ -184,6 +186,8 @@ class VMObject(GObject.Object): out_file=Path(str(self.log_dir.name)) / "vm.log", func=vms.run.run_vm, vm=self.data.flake.vm, + cachedir=log_dir, + socketdir=log_dir, ) log.debug(f"Started VM {self.get_id()}") GLib.idle_add(self._vm_status_changed_task) @@ -268,7 +272,7 @@ class VMObject(GObject.Object): try: with self.machine.vm.qmp_ctx() as qmp: qmp.command("system_powerdown") - except (OSError, ClanError) as ex: + except Exception as ex: log.debug(f"QMP command 'system_powerdown' ignored. Error: {ex}") # Try 20 times to stop the VM @@ -298,8 +302,12 @@ class VMObject(GObject.Object): log.warning(f"Tried to kill VM {self.get_id()} is not running") return log.info(f"Killing VM {self.get_id()} now") - self.vm_process.kill_group() - self.build_process.kill_group() + + if self.vm_process.proc.is_alive(): + self.vm_process.kill_group() + + if self.build_process.proc.is_alive(): + self.build_process.kill_group() def read_whole_log(self) -> str: if not self.vm_process.out_file.exists(): diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py index 5fc1ed5e6..b8d9963dc 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py @@ -18,8 +18,6 @@ log = logging.getLogger(__name__) class JoinValue(GObject.Object): - # TODO: custom signals for async join - __gsignals__: ClassVar = { "join_finished": (GObject.SignalFlags.RUN_FIRST, None, []), }