diff --git a/nixosModules/clanCore/vm.nix b/nixosModules/clanCore/vm.nix index fd0b438ea..ee9d88a54 100644 --- a/nixosModules/clanCore/vm.nix +++ b/nixosModules/clanCore/vm.nix @@ -17,8 +17,13 @@ let imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ./serial.nix + ./waypipe.nix ]; + clan.services.waypipe = { + inherit (config.clan.core.vm.inspect.waypipe) enable command; + }; + # required for issuing shell commands via qga services.qemuGuest.enable = true; @@ -149,12 +154,19 @@ in ''; }; - waypipe = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to use waypipe for native wayland passthrough, or not. - ''; + waypipe = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to use waypipe for native wayland passthrough, or not. + ''; + }; + command = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Commands that waypipe should run"; + }; }; }; # All important VM config variables needed by the vm runner @@ -193,13 +205,22 @@ in whether to enable graphics for the vm ''; }; - waypipe = lib.mkOption { - type = lib.types.bool; - internal = true; - readOnly = true; - description = '' - whether to enable native wayland window passthrough with waypipe for the vm - ''; + + waypipe = { + enable = lib.mkOption { + type = lib.types.bool; + internal = true; + readOnly = true; + description = '' + Whether to use waypipe for native wayland passthrough, or not. + ''; + }; + command = lib.mkOption { + type = lib.types.listOf lib.types.str; + internal = true; + readOnly = true; + description = "Commands that waypipe should run"; + }; }; machine_icon = lib.mkOption { type = lib.types.nullOr lib.types.path; @@ -245,7 +266,12 @@ in initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}"; toplevel = vmConfig.config.system.build.toplevel; regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; }); - inherit (config.clan.virtualisation) memorySize cores graphics; + inherit (config.clan.virtualisation) + memorySize + cores + graphics + waypipe + ; } ); }; diff --git a/nixosModules/waypipe.nix b/nixosModules/clanCore/waypipe.nix similarity index 95% rename from nixosModules/waypipe.nix rename to nixosModules/clanCore/waypipe.nix index 4e9f80a5a..b6c3b11b9 100644 --- a/nixosModules/waypipe.nix +++ b/nixosModules/clanCore/waypipe.nix @@ -39,8 +39,6 @@ # General default settings fonts.enableDefaultPackages = lib.mkDefault true; hardware.opengl.enable = lib.mkDefault true; - # Assume it is run inside a clan context - clan.virtualisation.waypipe = lib.mkDefault true; # User account services.getty.autologinUser = lib.mkDefault config.clan.services.waypipe.user; diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py index 8aa35fe3c..910703bd4 100644 --- a/pkgs/clan-app/clan_app/app.py +++ b/pkgs/clan-app/clan_app/app.py @@ -34,7 +34,7 @@ class MainApplication(Adw.Application): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__( - application_id="org.clan.clan-app", + application_id="org.clan.app", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, ) diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 427e9da54..1dc0acb50 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -246,6 +246,7 @@ class Machine: @attr: the attribute to get """ + log.warning(f"Extra config: {extra_config}") if attr in self._build_cache and not refresh and extra_config is None: return self._build_cache[attr] diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index 71f017bf0..0107d94f7 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -8,6 +8,12 @@ from ..completions import add_dynamic_completer, complete_machines from ..machines.machines import Machine +@dataclass +class WaypipeConfig: + enable: bool + command: list[str] + + @dataclass class VmConfig: machine_name: str @@ -24,6 +30,8 @@ class VmConfig: def __post_init__(self) -> None: if isinstance(self.flake_url, str): self.flake_url = FlakeId(self.flake_url) + if isinstance(self.waypipe, dict): + self.waypipe = WaypipeConfig(**self.waypipe) def inspect_vm(machine: Machine) -> VmConfig: diff --git a/pkgs/clan-vm-manager/clan-vm-manager.code-workspace b/pkgs/clan-vm-manager/clan-vm-manager.code-workspace index 1a108d5ce..2bd645fa0 100644 --- a/pkgs/clan-vm-manager/clan-vm-manager.code-workspace +++ b/pkgs/clan-vm-manager/clan-vm-manager.code-workspace @@ -14,6 +14,9 @@ }, { "path": "../../lib/build-clan" + }, + { + "path": "../../../democlan" } ], "settings": { diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg deleted file mode 100644 index f2840c42c..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg deleted file mode 100644 index 53d4f30c2..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/cybernet_no_text.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg deleted file mode 100644 index 2c8241c6a..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/firestorm.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg deleted file mode 100644 index 72f8e503c..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/penguin.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg deleted file mode 100644 index f3fa915bf..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg deleted file mode 100644 index 5c251a0d0..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/placeholder2.jpeg and /dev/null differ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg b/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg deleted file mode 100644 index 904182b57..000000000 Binary files a/pkgs/clan-vm-manager/clan_vm_manager/assets/zenith.jpeg and /dev/null differ 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 05c237ff6..fba07acc9 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -158,9 +158,15 @@ class VMObject(GObject.Object): flake=uri.flake, ) assert self.machine is not None - state_dir = vm_state_dir( - flake_url=str(self.machine.flake.url), vm_name=self.machine.name - ) + + if self.machine.flake.is_local(): + state_dir = vm_state_dir( + flake_url=str(self.machine.flake.path), vm_name=self.machine.name + ) + else: + state_dir = vm_state_dir( + flake_url=self.machine.flake.url, vm_name=self.machine.name + ) self.qmp_wrap = QMPWrapper(state_dir) assert self.machine is not None yield self.machine diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py b/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py deleted file mode 100644 index 4d8e9cd64..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py +++ /dev/null @@ -1,156 +0,0 @@ -import dataclasses -import json -import logging -import sys -import threading -from collections.abc import Callable -from pathlib import Path -from threading import Lock -from typing import Any - -import gi -from clan_cli.api import API - -gi.require_version("WebKit", "6.0") - -from gi.repository import GLib, WebKit - -site_index: Path = ( - Path(sys.argv[0]).absolute() - / Path("../..") - / Path("clan_vm_manager/.webui/index.html") -).resolve() - -log = logging.getLogger(__name__) - - -def dataclass_to_dict(obj: Any) -> Any: - """ - Utility function to convert dataclasses to dictionaries - It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries - - It does NOT convert member functions. - """ - if dataclasses.is_dataclass(obj): - return {k: dataclass_to_dict(v) for k, v in dataclasses.asdict(obj).items()} - elif isinstance(obj, list | tuple): - return [dataclass_to_dict(item) for item in obj] - elif isinstance(obj, dict): - return {k: dataclass_to_dict(v) for k, v in obj.items()} - else: - return obj - - -class WebView: - def __init__(self, methods: dict[str, Callable]) -> None: - self.method_registry: dict[str, Callable] = methods - - self.webview = WebKit.WebView() - - settings = self.webview.get_settings() - # settings. - settings.set_property("enable-developer-extras", True) - self.webview.set_settings(settings) - - self.manager = self.webview.get_user_content_manager() - # Can be called with: window.webkit.messageHandlers.gtk.postMessage("...") - # Important: it seems postMessage must be given some payload, otherwise it won't trigger the event - self.manager.register_script_message_handler("gtk") - self.manager.connect("script-message-received", self.on_message_received) - - self.webview.load_uri(f"file://{site_index}") - - # global mutex lock to ensure functions run sequentially - self.mutex_lock = Lock() - self.queue_size = 0 - - def on_message_received( - self, user_content_manager: WebKit.UserContentManager, message: Any - ) -> None: - payload = json.loads(message.to_json(0)) - method_name = payload["method"] - handler_fn = self.method_registry[method_name] - - log.debug(f"Received message: {payload}") - log.debug(f"Queue size: {self.queue_size} (Wait)") - - def threaded_wrapper() -> bool: - """ - Ensures only one function is executed at a time - - Wait until there is no other function acquiring the global lock. - - Starts a thread with the potentially long running API function within. - """ - if not self.mutex_lock.locked(): - thread = threading.Thread( - target=self.threaded_handler, - args=( - handler_fn, - payload.get("data"), - method_name, - ), - ) - thread.start() - return GLib.SOURCE_REMOVE - - return GLib.SOURCE_CONTINUE - - GLib.idle_add( - threaded_wrapper, - ) - self.queue_size += 1 - - def threaded_handler( - self, - handler_fn: Callable[ - ..., - Any, - ], - data: dict[str, Any] | None, - method_name: str, - ) -> None: - with self.mutex_lock: - log.debug("Executing... ", method_name) - log.debug(f"{data}") - if data is None: - result = handler_fn() - else: - reconciled_arguments = {} - for k, v in data.items(): - # Some functions expect to be called with dataclass instances - # But the js api returns dictionaries. - # Introspect the function and create the expected dataclass from dict dynamically - # Depending on the introspected argument_type - arg_type = API.get_method_argtype(method_name, k) - if dataclasses.is_dataclass(arg_type): - reconciled_arguments[k] = arg_type(**v) - else: - reconciled_arguments[k] = v - - result = handler_fn(**reconciled_arguments) - - serialized = json.dumps(dataclass_to_dict(result)) - - # Use idle_add to queue the response call to js on the main GTK thread - GLib.idle_add(self.return_data_to_js, method_name, serialized) - self.queue_size -= 1 - log.debug(f"Done: Remaining queue size: {self.queue_size}") - - def return_data_to_js(self, method_name: str, serialized: str) -> bool: - # This function must be run on the main GTK thread to interact with the webview - # result = method_fn(data) # takes very long - # serialized = result - self.webview.evaluate_javascript( - f""" - window.clan.{method_name}(`{serialized}`); - """, - -1, - None, - None, - None, - ) - return GLib.SOURCE_REMOVE - - def get_webview(self) -> WebKit.WebView: - return self.webview diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 92dcf2798..f8d0ec6a1 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -2,7 +2,6 @@ import logging import threading import gi -from clan_cli.api import API from clan_cli.history.list import list_history from clan_vm_manager.components.interfaces import ClanConfig @@ -12,7 +11,6 @@ from clan_vm_manager.singletons.use_vms import ClanStore from clan_vm_manager.views.details import Details from clan_vm_manager.views.list import ClanList from clan_vm_manager.views.logs import Logs -from clan_vm_manager.views.webview import WebView gi.require_version("Adw", "1") @@ -61,9 +59,6 @@ class MainWindow(Adw.ApplicationWindow): stack_view.add_named(Details(), "details") stack_view.add_named(Logs(), "logs") - webview = WebView(methods=API._registry) - stack_view.add_named(webview.get_webview(), "webview") - stack_view.set_visible_child_name(config.initial_view) view.set_content(scroll) diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 6f9acc541..5852e6325 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -18,7 +18,6 @@ runCommand, setuptools, webkitgtk_6_0, - webview-ui, wrapGAppsHook, }: let @@ -142,12 +141,6 @@ python3.pkgs.buildPythonApplication rec { passthru.runtimeDependencies = runtimeDependencies; passthru.testDependencies = testDependencies; - # TODO: place webui in lib/python3.11/site-packages/clan_vm_manager - postInstall = '' - mkdir -p $out/clan_vm_manager/.webui - cp -r ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/* $out/clan_vm_manager/.webui - ''; - # Don't leak python packages into a devshell. # It can be very confusing if you `nix run` than load the cli from the devshell instead. postFixup = '' diff --git a/pkgs/clan-vm-manager/flake-module.nix b/pkgs/clan-vm-manager/flake-module.nix index f374a95f5..f0b172db8 100644 --- a/pkgs/clan-vm-manager/flake-module.nix +++ b/pkgs/clan-vm-manager/flake-module.nix @@ -13,10 +13,10 @@ else { devShells.clan-vm-manager = pkgs.callPackage ./shell.nix { - inherit (config.packages) clan-vm-manager webview-ui; + inherit (config.packages) clan-vm-manager; }; packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix { - inherit (config.packages) clan-cli webview-ui; + inherit (config.packages) clan-cli; }; checks = config.packages.clan-vm-manager.tests; diff --git a/pkgs/clan-vm-manager/install-desktop.sh b/pkgs/clan-vm-manager/install-desktop.sh index 445839786..afa1c387e 100755 --- a/pkgs/clan-vm-manager/install-desktop.sh +++ b/pkgs/clan-vm-manager/install-desktop.sh @@ -1,23 +1,22 @@ #!/usr/bin/env bash -CLAN=$(nix build .#clan-vm-manager --print-out-paths) if ! command -v xdg-mime &> /dev/null; then echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed." fi +ALREADY_INSTALLED=$(nix profile list --json | jq 'has("elements") and (.elements | has("clan-vm-manager"))') + +if [ "$ALREADY_INSTALLED" = "true" ]; then + echo "Upgrading installed clan-vm-manager" + nix profile upgrade clan-vm-manager +else + nix profile install .#clan-vm-manager +fi + + # install desktop file set -eou pipefail DESKTOP_FILE_NAME=org.clan.vm-manager.desktop -DESKTOP_DST=~/.local/share/applications/"$DESKTOP_FILE_NAME" -DESKTOP_SRC="$CLAN/share/applications/$DESKTOP_FILE_NAME" -UI_BIN="$CLAN/bin/clan-vm-manager" -cp -f "$DESKTOP_SRC" "$DESKTOP_DST" -sleep 2 -sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" "$DESKTOP_DST" xdg-mime default "$DESKTOP_FILE_NAME" x-scheme-handler/clan -echo "==== Validating desktop file installation ====" -set -x -desktop-file-validate "$DESKTOP_DST" -set +xeou pipefail diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index 4de1f494c..1b00bd998 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -10,7 +10,7 @@ python3, gtk4, libadwaita, - webview-ui, + }: let @@ -52,11 +52,5 @@ mkShell { # Add clan-cli to the python path so that we can import it without building it in nix first export PYTHONPATH="$GIT_ROOT/pkgs/clan-cli":"$PYTHONPATH" - - # Add the webview-ui to the .webui directory - rm -rf ./clan_vm_manager/.webui/* - mkdir -p ./clan_vm_manager/.webui - cp -a ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/* ./clan_vm_manager/.webui - chmod -R +w ./clan_vm_manager/.webui ''; }