Merge pull request 'clan-vm-manager: Fix waypipe regression in nix code' (#1793) from Qubasa/clan-core:Qubasa-main into main
@@ -17,8 +17,13 @@ let
|
|||||||
imports = [
|
imports = [
|
||||||
(modulesPath + "/virtualisation/qemu-vm.nix")
|
(modulesPath + "/virtualisation/qemu-vm.nix")
|
||||||
./serial.nix
|
./serial.nix
|
||||||
|
./waypipe.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
clan.services.waypipe = {
|
||||||
|
inherit (config.clan.core.vm.inspect.waypipe) enable command;
|
||||||
|
};
|
||||||
|
|
||||||
# required for issuing shell commands via qga
|
# required for issuing shell commands via qga
|
||||||
services.qemuGuest.enable = true;
|
services.qemuGuest.enable = true;
|
||||||
|
|
||||||
@@ -149,12 +154,19 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
waypipe = lib.mkOption {
|
waypipe = {
|
||||||
type = lib.types.bool;
|
enable = lib.mkOption {
|
||||||
default = false;
|
type = lib.types.bool;
|
||||||
description = ''
|
default = false;
|
||||||
Whether to use waypipe for native wayland passthrough, or not.
|
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
|
# All important VM config variables needed by the vm runner
|
||||||
@@ -193,13 +205,22 @@ in
|
|||||||
whether to enable graphics for the vm
|
whether to enable graphics for the vm
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
waypipe = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
waypipe = {
|
||||||
internal = true;
|
enable = lib.mkOption {
|
||||||
readOnly = true;
|
type = lib.types.bool;
|
||||||
description = ''
|
internal = true;
|
||||||
whether to enable native wayland window passthrough with waypipe for the vm
|
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 {
|
machine_icon = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
@@ -245,7 +266,12 @@ in
|
|||||||
initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}";
|
initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}";
|
||||||
toplevel = vmConfig.config.system.build.toplevel;
|
toplevel = vmConfig.config.system.build.toplevel;
|
||||||
regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; });
|
regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; });
|
||||||
inherit (config.clan.virtualisation) memorySize cores graphics;
|
inherit (config.clan.virtualisation)
|
||||||
|
memorySize
|
||||||
|
cores
|
||||||
|
graphics
|
||||||
|
waypipe
|
||||||
|
;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,8 +39,6 @@
|
|||||||
# General default settings
|
# General default settings
|
||||||
fonts.enableDefaultPackages = lib.mkDefault true;
|
fonts.enableDefaultPackages = lib.mkDefault true;
|
||||||
hardware.opengl.enable = 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
|
# User account
|
||||||
services.getty.autologinUser = lib.mkDefault config.clan.services.waypipe.user;
|
services.getty.autologinUser = lib.mkDefault config.clan.services.waypipe.user;
|
||||||
@@ -34,7 +34,7 @@ class MainApplication(Adw.Application):
|
|||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
application_id="org.clan.clan-app",
|
application_id="org.clan.app",
|
||||||
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
|
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ class Machine:
|
|||||||
@attr: the attribute to get
|
@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:
|
if attr in self._build_cache and not refresh and extra_config is None:
|
||||||
return self._build_cache[attr]
|
return self._build_cache[attr]
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ from ..completions import add_dynamic_completer, complete_machines
|
|||||||
from ..machines.machines import Machine
|
from ..machines.machines import Machine
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WaypipeConfig:
|
||||||
|
enable: bool
|
||||||
|
command: list[str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VmConfig:
|
class VmConfig:
|
||||||
machine_name: str
|
machine_name: str
|
||||||
@@ -24,6 +30,8 @@ class VmConfig:
|
|||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if isinstance(self.flake_url, str):
|
if isinstance(self.flake_url, str):
|
||||||
self.flake_url = FlakeId(self.flake_url)
|
self.flake_url = FlakeId(self.flake_url)
|
||||||
|
if isinstance(self.waypipe, dict):
|
||||||
|
self.waypipe = WaypipeConfig(**self.waypipe)
|
||||||
|
|
||||||
|
|
||||||
def inspect_vm(machine: Machine) -> VmConfig:
|
def inspect_vm(machine: Machine) -> VmConfig:
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../lib/build-clan"
|
"path": "../../lib/build-clan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../democlan"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 152 KiB |
@@ -158,9 +158,15 @@ class VMObject(GObject.Object):
|
|||||||
flake=uri.flake,
|
flake=uri.flake,
|
||||||
)
|
)
|
||||||
assert self.machine is not None
|
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)
|
self.qmp_wrap = QMPWrapper(state_dir)
|
||||||
assert self.machine is not None
|
assert self.machine is not None
|
||||||
yield self.machine
|
yield self.machine
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -2,7 +2,6 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli.api import API
|
|
||||||
from clan_cli.history.list import list_history
|
from clan_cli.history.list import list_history
|
||||||
|
|
||||||
from clan_vm_manager.components.interfaces import ClanConfig
|
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.details import Details
|
||||||
from clan_vm_manager.views.list import ClanList
|
from clan_vm_manager.views.list import ClanList
|
||||||
from clan_vm_manager.views.logs import Logs
|
from clan_vm_manager.views.logs import Logs
|
||||||
from clan_vm_manager.views.webview import WebView
|
|
||||||
|
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
|
|
||||||
@@ -61,9 +59,6 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
stack_view.add_named(Details(), "details")
|
stack_view.add_named(Details(), "details")
|
||||||
stack_view.add_named(Logs(), "logs")
|
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)
|
stack_view.set_visible_child_name(config.initial_view)
|
||||||
|
|
||||||
view.set_content(scroll)
|
view.set_content(scroll)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
runCommand,
|
runCommand,
|
||||||
setuptools,
|
setuptools,
|
||||||
webkitgtk_6_0,
|
webkitgtk_6_0,
|
||||||
webview-ui,
|
|
||||||
wrapGAppsHook,
|
wrapGAppsHook,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -142,12 +141,6 @@ python3.pkgs.buildPythonApplication rec {
|
|||||||
passthru.runtimeDependencies = runtimeDependencies;
|
passthru.runtimeDependencies = runtimeDependencies;
|
||||||
passthru.testDependencies = testDependencies;
|
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.
|
# 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.
|
# It can be very confusing if you `nix run` than load the cli from the devshell instead.
|
||||||
postFixup = ''
|
postFixup = ''
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
devShells.clan-vm-manager = pkgs.callPackage ./shell.nix {
|
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 {
|
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;
|
checks = config.packages.clan-vm-manager.tests;
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
CLAN=$(nix build .#clan-vm-manager --print-out-paths)
|
|
||||||
|
|
||||||
if ! command -v xdg-mime &> /dev/null; then
|
if ! command -v xdg-mime &> /dev/null; then
|
||||||
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
|
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
|
||||||
fi
|
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
|
# install desktop file
|
||||||
set -eou pipefail
|
set -eou pipefail
|
||||||
DESKTOP_FILE_NAME=org.clan.vm-manager.desktop
|
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
|
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
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
python3,
|
python3,
|
||||||
gtk4,
|
gtk4,
|
||||||
libadwaita,
|
libadwaita,
|
||||||
webview-ui,
|
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
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
|
# 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"
|
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
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||