diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 5744b1f5a..bab07ea3f 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -32,7 +32,9 @@ class VMAttr: log.debug(f"qmp_socket: {self._qmp_socket}") rpath = self._qmp_socket.resolve() if not rpath.exists(): - raise ClanError(f"qmp socket {rpath} does not exist. Is the VM running?") + raise ClanError( + f"qmp socket {rpath} does not exist. Is the VM running?" + ) self._qmp = QEMUMonitorProtocol(str(rpath)) self._qmp.connect() try: diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index cac5ab9a2..abeb28f9f 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -37,7 +37,7 @@ def facts_to_nixos_config(facts: dict[str, dict[str, bytes]]) -> dict: # TODO move this to the Machines class def build_vm( - machine: Machine, vm: VmConfig, tmpdir: Path, nix_options: list[str] + machine: Machine, vm: VmConfig, tmpdir: Path, nix_options: list[str] = [] ) -> dict[str, str]: secrets_dir = get_secrets(machine, tmpdir) diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index c96fd1919..9c2700ad3 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -43,7 +43,9 @@ def wait_vm_up(state_dir: Path) -> None: timeout: float = 300 while True: if timeout <= 0: - raise TimeoutError(f"qga socket {socket_file} not found. Is the VM running?") + raise TimeoutError( + f"qga socket {socket_file} not found. Is the VM running?" + ) if socket_file.exists(): break sleep(0.1) @@ -56,7 +58,9 @@ def wait_vm_down(state_dir: Path) -> None: timeout: float = 300 while socket_file.exists(): if timeout <= 0: - raise TimeoutError(f"qga socket {socket_file} still exists. Is the VM down?") + raise TimeoutError( + f"qga socket {socket_file} still exists. Is the VM down?" + ) sleep(0.1) timeout -= 0.1 diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index 57fbdd99a..48c0fb149 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -14,7 +14,7 @@ from gi.repository import Adw, Gdk, Gio, Gtk from clan_vm_manager.models.interfaces import ClanConfig from clan_vm_manager.models.use_join import GLib, GObject -from clan_vm_manager.models.use_vms import VMS +from clan_vm_manager.models.use_vms import VMs from .trayicon import TrayIcon from .windows.main_window import MainWindow @@ -44,7 +44,8 @@ class MainApplication(Adw.Application): "enable debug mode", None, ) - + self.vms = VMs.use() + log.debug(f"VMS object: {self.vms}") self.window: Adw.ApplicationWindow | None = None self.connect("shutdown", self.on_shutdown) self.connect("activate", self.show_window) @@ -69,35 +70,16 @@ class MainApplication(Adw.Application): log.debug(f"Join request: {args[1]}") uri = args[1] self.emit("join_request", uri) - return 0 - def get_application_icon_path(self) -> None: - self.icon_name = "lol.clan.vm.manager" - if not self.icon_name: - return None - - icon_theme = Gtk.IconTheme.get_for_display( - self.get_active_window().get_display() - ) - # Use the correct method to look up an icon - icon_lookup_flags = 16 - icon = icon_theme.lookup_icon( - self.icon_name, 128, 1.0, Gtk.TextDirection.NONE, icon_lookup_flags - ) - - if icon: - return icon.get_file().get_path() - return None - def on_shutdown(self, app: Gtk.Application) -> None: log.debug("Shutting down") + self.vms.kill_all() + if self.tray_icon is not None: self.tray_icon.destroy() - VMS.use().kill_all() - def on_window_hide_unhide(self, *_args: Any) -> None: assert self.window is not None if self.window.is_visible(): diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models/executor.py b/pkgs/clan-vm-manager/clan_vm_manager/models/executor.py index dfc568326..5987a9a18 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/models/executor.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/models/executor.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Any import gi -from clan_cli.errors import ClanError gi.require_version("GdkPixbuf", "2.0") @@ -24,7 +23,7 @@ def _kill_group(proc: mp.Process) -> None: if proc.is_alive() and pid: os.killpg(pid, signal.SIGTERM) else: - log.warning(f"Process {proc.name} with pid {pid} is already dead") + log.warning(f"Process '{proc.name}' with pid '{pid}' is already dead") @dataclasses.dataclass(frozen=True) @@ -102,7 +101,7 @@ def _init_proc( def spawn( *, - log_dir: Path, + out_file: Path, on_except: Callable[[Exception, mp.process.BaseProcess], None] | None, func: Callable, **kwargs: Any, @@ -111,13 +110,8 @@ def spawn( if mp.get_start_method(allow_none=True) is None: mp.set_start_method(method="forkserver") - if not log_dir.is_dir(): - raise ClanError(f"Log path {log_dir} is not a directory") - log_dir.mkdir(parents=True, exist_ok=True) - # Set names proc_name = f"MPExec:{func.__name__}" - out_file = log_dir / "out.log" # Start the process proc = mp.Process( diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models/use_join.py b/pkgs/clan-vm-manager/clan_vm_manager/models/use_join.py index cba131e7f..88215b1ea 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/models/use_join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/models/use_join.py @@ -9,7 +9,7 @@ from clan_cli.clan_uri import ClanURI from clan_cli.history.add import add_history from clan_vm_manager.errors.show_error import show_error_dialog -from clan_vm_manager.models.use_vms import VMS, Clans +from clan_vm_manager.models.use_vms import Clans gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") @@ -76,7 +76,7 @@ class Join: def after_join(item: JoinValue, _: Any) -> None: self.discard(item) Clans.use().refresh() - VMS.use().refresh() + # VMS.use().refresh() print("Refreshed list after join") on_join(item) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/models/use_vms.py index 098324d51..fbefec068 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/models/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/models/use_vms.py @@ -107,6 +107,7 @@ class VM(GObject.Object): data: HistoryEntry, ) -> None: super().__init__() + self.KILL_TIMEOUT = 6 # seconds self.data = data self.process = MPProcess("dummy", mp.Process(), Path("./dummy")) self._watcher_id: int = 0 @@ -121,9 +122,8 @@ class VM(GObject.Object): self.log_dir = tempfile.TemporaryDirectory( prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}" ) - self._finalizer = weakref.finalize(self, self.stop) + self._finalizer = weakref.finalize(self, self.kill) self.connect("build_vm", self.build_vm) - uri = ClanURI.from_str( url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr ) @@ -160,25 +160,25 @@ class VM(GObject.Object): log.info(f"Starting VM {self.get_id()}") vm = vms.run.inspect_vm(self.machine) - GLib.idle_add(self.emit, "build_vm", self, True) - self.process = spawn( - on_except=None, - log_dir=Path(str(self.log_dir.name)), - func=vms.run.build_vm, - machine=self.machine, - vm=vm, - ) - self.process.proc.join() + # GLib.idle_add(self.emit, "build_vm", self, True) + # self.process = spawn( + # on_except=None, + # log_dir=Path(str(self.log_dir.name)), + # func=vms.run.build_vm, + # machine=self.machine, + # vm=vm, + # ) + # self.process.proc.join() - GLib.idle_add(self.emit, "build_vm", self, False) + # GLib.idle_add(self.emit, "build_vm", self, False) - if self.process.proc.exitcode != 0: - log.error(f"Failed to build VM {self.get_id()}") - return + # if self.process.proc.exitcode != 0: + # log.error(f"Failed to build VM {self.get_id()}") + # return self.process = spawn( on_except=None, - log_dir=Path(str(self.log_dir.name)), + out_file=Path(str(self.log_dir.name)) / "vm.log", func=vms.run.run_vm, vm=vm, ) @@ -241,7 +241,7 @@ class VM(GObject.Object): if self.is_running(): assert self._stop_timer_init is not None diff = datetime.now() - self._stop_timer_init - if diff.seconds > 10: + if diff.seconds > self.KILL_TIMEOUT: log.error(f"VM {self.get_id()} has not stopped. Killing it") self.process.kill_group() return GLib.SOURCE_CONTINUE @@ -263,12 +263,19 @@ class VM(GObject.Object): if self._stop_watcher_id == 0: raise ClanError("Failed to add stop watcher") - def stop(self) -> None: + def shutdown(self) -> None: if not self.is_running(): return log.info(f"Stopping VM {self.get_id()}") threading.Thread(target=self.__stop).start() + def kill(self) -> None: + if not self.is_running(): + log.warning(f"Tried to kill VM {self.get_id()} is not running") + return + log.info(f"Killing VM {self.get_id()} now") + self.process.kill_group() + def read_whole_log(self) -> str: if not self.process.out_file.exists(): log.error(f"Log file {self.process.out_file} does not exist") @@ -276,28 +283,16 @@ class VM(GObject.Object): return self.process.out_file.read_text() -class VMS: - """ - This is a singleton. - It is initialized with the first call of use() - - Usage: - - VMS.use().get_running_vms() - - VMS.use() can also be called before the data is needed. e.g. to eliminate/reduce waiting time. - - """ - +class VMs: list_store: Gio.ListStore - _instance: "None | VMS" = None + _instance: "None | VMs" = None # Make sure the VMS class is used as a singleton def __init__(self) -> None: raise RuntimeError("Call use() instead") @classmethod - def use(cls: Any) -> "VMS": + def use(cls: Any) -> "VMs": if cls._instance is None: cls._instance = cls.__new__(cls) cls.list_store = Gio.ListStore.new(VM) @@ -327,10 +322,13 @@ class VMS: return list(filter(lambda vm: vm.is_running(), self.list_store)) def kill_all(self) -> None: + log.debug(f"Running vms: {self.get_running_vms()}") for vm in self.get_running_vms(): - vm.stop() + vm.kill() def refresh(self) -> None: + log.error("NEVER FUCKING DO THIS") + return self.list_store.remove_all() for vm in get_saved_vms(): self.list_store.append(vm) @@ -338,7 +336,7 @@ class VMS: def get_saved_vms() -> list[VM]: vm_list = [] - + log.info("=====CREATING NEW VM OBJ====") try: # Execute `clan flakes add ` to democlan for this to work for entry in list_history(): diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index b28f14ce4..ed5296751 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -14,7 +14,7 @@ from clan_vm_manager.models.use_views import Views gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk -from clan_vm_manager.models.use_vms import VM, VMS, ClanGroup, Clans +from clan_vm_manager.models.use_vms import VM, ClanGroup, Clans log = logging.getLogger(__name__) @@ -48,8 +48,8 @@ class ClanList(Gtk.Box): def __init__(self, config: ClanConfig) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL) - app = Gio.Application.get_default() - app.connect("join_request", self.on_join_request) + self.app = Gio.Application.get_default() + self.app.connect("join_request", self.on_join_request) groups = Clans.use() join = Join.use() @@ -123,7 +123,7 @@ class ClanList(Gtk.Box): def on_search_changed(self, entry: Gtk.SearchEntry) -> None: Clans.use().filter_by_name(entry.get_text()) # Disable the shadow if the list is empty - if not VMS.use().list_store.get_n_items(): + if not self.app.vms.list_store.get_n_items(): self.group_list.add_css_class("no-shadow") def render_vm_row(self, boxed_list: Gtk.ListBox, vm: VM) -> Gtk.Widget: @@ -202,7 +202,7 @@ class ClanList(Gtk.Box): def on_edit(self, action: Any, parameter: Any) -> None: target = parameter.get_string() - vm = VMS.use().get_by_id(target) + vm = self.app.vms.get_by_id(target) if not vm: raise ClanError("Something went wrong. Please restart the app.") @@ -220,7 +220,7 @@ class ClanList(Gtk.Box): row.add_css_class("trust") # TODO: figure out how to detect that - exist = VMS.use().get_by_id(item.url.get_id()) + exist = self.app.vms.use().get_by_id(item.url.get_id()) if exist: sub = row.get_subtitle() row.set_subtitle( @@ -292,7 +292,7 @@ class ClanList(Gtk.Box): if not row.get_active(): row.set_state(True) - vm.stop() + vm.shutdown() def vm_status_changed(self, switch: Gtk.Switch, vm: VM, _vm: VM) -> None: switch.set_active(vm.is_running())