Merge pull request 'clan_vm_manager: --debug enables debug mode in clan_cli too' (#840) from Qubasa-add_spinner into main
All checks were successful
checks-impure / test (push) Successful in 1m43s
checks / test (push) Successful in 2m46s

This commit is contained in:
clan-bot 2024-02-14 08:43:14 +00:00
commit 1cc6e74297
9 changed files with 128 additions and 138 deletions

View File

@ -1,9 +1,9 @@
import json
import logging
from os import path
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from tempfile import NamedTemporaryFile
from time import sleep
from clan_cli.dirs import vm_state_dir
from qemu.qmp import QEMUMonitorProtocol
@ -16,6 +16,27 @@ from ..ssh import Host, parse_deployment_address
log = logging.getLogger(__name__)
class VMAttr:
def __init__(self, state_dir: Path) -> None:
self._qmp_socket: Path = state_dir / "qmp.sock"
self._qga_socket: Path = state_dir / "qga.sock"
self._qmp: QEMUMonitorProtocol | None = None
@contextmanager
def qmp(self) -> Generator[QEMUMonitorProtocol, None, None]:
if self._qmp is None:
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")
self._qmp = QEMUMonitorProtocol(str(rpath))
self._qmp.connect()
try:
yield self._qmp
finally:
self._qmp.close()
class Machine:
def __init__(
self,
@ -36,14 +57,10 @@ class Machine:
self.build_cache: dict[str, Path] = {}
self._deployment_info: None | dict[str, str] = deployment_info
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.name)
self.qmp_socket: Path = state_dir / "qmp.sock"
self.qga_socket: Path = state_dir / "qga.sock"
log.debug(f"qmp_socket: {self.qmp_socket}")
self._qmp = QEMUMonitorProtocol(path.realpath(self.qmp_socket))
self._qmp_connected = False
self.vm: VMAttr = VMAttr(state_dir)
def __str__(self) -> str:
return f"Machine(name={self.name}, flake={self.flake})"
@ -60,28 +77,6 @@ class Machine:
)
return self._deployment_info
def qmp_connect(self) -> None:
if not self._qmp_connected:
tries = 100
for num in range(tries):
try:
# the socket file link might be outdated, therefore re-init the qmp object
self._qmp = QEMUMonitorProtocol(path.realpath(self.qmp_socket))
self._qmp.connect()
self._qmp_connected = True
log.debug("QMP Connected")
return
except FileNotFoundError:
if num < 99:
sleep(0.1)
continue
else:
raise
def qmp_command(self, command: str) -> dict:
self.qmp_connect()
return self._qmp.command(command)
@property
def target_host_address(self) -> str:
# deploymentAddress is deprecated.

View File

@ -154,7 +154,9 @@ def qemu_command(
# TODO move this to the Machines class
def build_vm(machine: Machine, vm: VmConfig, nix_options: list[str]) -> dict[str, str]:
def build_vm(
machine: Machine, vm: VmConfig, nix_options: list[str] = []
) -> dict[str, str]:
config = nix_config()
system = config["system"]

View File

@ -40,17 +40,25 @@ def run_vm_in_thread(machine_name: str) -> None:
# wait for qmp socket to exist
def wait_vm_up(state_dir: Path) -> None:
socket_file = state_dir / "qga.sock"
timeout: float = 300
while True:
if timeout <= 0:
raise TimeoutError(f"qga socket {socket_file} not found")
if socket_file.exists():
break
sleep(0.1)
timeout -= 0.1
# wait for vm to be down by checking if qga socket is down
def wait_vm_down(state_dir: Path) -> None:
socket_file = state_dir / "qga.sock"
timeout: float = 300
while socket_file.exists():
if timeout <= 0:
raise TimeoutError(f"qga socket {socket_file} still exists")
sleep(0.1)
timeout -= 0.1
# wait for vm to be up then connect and return qmp instance

View File

@ -1,88 +1,35 @@
## Developing GTK3 Applications
## Developing GTK4 Applications
Here we will document on how to develop GTK3 application UI in python. First we want to setup
an example code base to look into. In this case gnome-music.
## Setup gnome-music as code reference
gnome-music does not use glade
Clone gnome-music and check out the tag v40.0
[gnome-music](https://github.com/GNOME/gnome-music/tree/40.0)
## Demos
Adw has a demo application showing all widgets. You can run it by executing:
```bash
git clone git@github.com:GNOME/gnome-music.git && cd gnome-music && git checkout 40.0
adwaita-1-demo
```
GTK4 has a demo application showing all widgets. You can run it by executing:
```bash
gtk4-widget-factory
```
Checkout nixpkgs version `468cb5980b56d348979488a74a9b5de638400160` for the correct gnome-music devshell then execute:
```bash
nix develop /home/username/Projects/nixpkgs#gnome.gnome-music
```
Look into the file `gnome-music.in` which bootstraps the application.
## Setup gnu-cash as reference
Gnucash uses glade with complex UI
Setup gnucash
```bash
git clone git@github.com:Gnucash/gnucash.git
git checkout ed4921271c863c7f6e0c800e206b25ac6e9ba4da
cd nixpkgs
git checkout 015739d7bffa7da4e923978040a2f7cba6af3270
nix develop /home/username/Projects/nixpkgs#gnucash
mkdir build && cd build
cmake ..
cd ..
make
```
- The use the GTK Builder instead of templates.
## Look into virt-manager it uses python + spice-gtk
Look into `virtManager/details/viewers.py` to see how spice-gtk is being used
```bash
git clone https://github.com/virt-manager/virt-manager
```
### Glade
Make sure to check the 'composit' box in glade in the GtkApplicationWindow to be able to
import the glade file through GTK template
## Links
- [Adw PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Adw-1)
- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0)
- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html)
- [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html)
- Another python glade project [syncthing-gtk](https://github.com/kozec/syncthing-gtk)
- Other python glade project [linuxcnc](https://github.com/podarok/linuxcnc/tree/master)
- Install [Glade UI Toolbuilder](https://gitlab.gnome.org/GNOME/glade)
- To understand GTK3 Components look into the [Python GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/search.html?q=ApplicationWindow&check_keywords=yes&area=default)
- https://web.archive.org/web/20100706201447/http://www.pygtk.org/pygtk2reference/ (GTK2 Reference, many methods still exist in gtk3)
-
- Also look into [PyGObject](https://pygobject.readthedocs.io/en/latest/guide/gtk_template.html) to know more about threading and async etc.
- [GI Python API](https://lazka.github.io/pgi-docs/#Gtk-3.0)
- https://developer.gnome.org/documentation/tutorials/application.html
- [GTK3 Python] https://github.com/sam-m888/python-gtk3-tutorial/tree/master
- https://gnome.pages.gitlab.gnome.org/libhandy/doc/1.8/index.html
- https://github.com/geigi/cozy
- https://github.com/lutris/lutris/blob/2e9bd115febe08694f5d42dabcf9da36a1065f1d/lutris/gui/widgets/cellrenderers.py#L92
## Debugging Style and Layout
You can append `--debug` flag to enable debug logging printed into the console.
```bash
# Enable the debugger
# Enable the GTK debugger
gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true
# Start the application with the debugger attached
GTK_DEBUG=interactive ./bin/clan-vm-manager
GTK_DEBUG=interactive ./bin/clan-vm-manager --debug
```

View File

@ -56,6 +56,7 @@ class MainApplication(Adw.Application):
if "debug" in options:
setup_logging("DEBUG", root_log_name=__name__.split(".")[0])
setup_logging("DEBUG", root_log_name="clan_cli")
else:
setup_logging("INFO", root_log_name=__name__.split(".")[0])
log.debug("Debug logging enabled")

View File

@ -12,8 +12,8 @@ avatar {
}
.trust {
padding-top: 25px;
padding-bottom: 25px;
padding-top: 25px;
padding-bottom: 25px;
}
.join-list {
@ -22,11 +22,16 @@ avatar {
}
.progress-bar {
margin-right: 25px;
min-width: 200px;
}
.group-list {
background-color: inherit;
}
.group-list > .activatable:hover {
background-color: unset;
background-color: unset;
}
.group-list > row {

View File

@ -23,7 +23,7 @@ import multiprocessing as mp
import threading
from clan_cli.machines.machines import Machine
from gi.repository import Gio, GLib, GObject
from gi.repository import Gio, GLib, GObject, Gtk
log = logging.getLogger(__name__)
@ -114,10 +114,15 @@ class VM(GObject.Object):
self._stop_timer_init: datetime | None = None
self._logs_id: int = 0
self._log_file: IO[str] | None = None
self.progress_bar: Gtk.ProgressBar = Gtk.ProgressBar()
self.progress_bar.hide()
self.progress_bar.set_hexpand(True) # Horizontally expand
self.prog_bar_id: int = 0
self.log_dir = tempfile.TemporaryDirectory(
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
)
self._finalizer = weakref.finalize(self, self.stop)
self.connect("build_vm", self.build_vm)
uri = ClanURI.from_str(
url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr
@ -134,14 +139,43 @@ class VM(GObject.Object):
flake=url, # type: ignore
)
def _pulse_progress_bar(self) -> bool:
self.progress_bar.pulse()
return GLib.SOURCE_CONTINUE
def build_vm(self, vm: "VM", _vm: "VM", building: bool) -> None:
if building:
log.info("Building VM")
self.progress_bar.show()
self.prog_bar_id = GLib.timeout_add(100, self._pulse_progress_bar)
if self.prog_bar_id == 0:
raise ClanError("Couldn't spawn a progess bar task")
else:
self.progress_bar.hide()
if not GLib.Source.remove(self.prog_bar_id):
log.error("Failed to remove progress bar task")
log.info("VM built")
def __start(self) -> None:
log.info(f"Starting VM {self.get_id()}")
vm = vms.run.inspect_vm(self.machine)
GLib.idle_add(self.emit, "build_vm", self, True)
vms.run.build_vm(self.machine, vm, [])
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)
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)),
@ -160,8 +194,6 @@ class VM(GObject.Object):
if self._watcher_id == 0:
raise ClanError("Failed to add watcher")
self.machine.qmp_connect()
def start(self) -> None:
if self.is_running():
log.warn("VM is already running")
@ -220,7 +252,12 @@ class VM(GObject.Object):
def __stop(self) -> None:
log.info(f"Stopping VM {self.get_id()}")
self.machine.qmp_command("system_powerdown")
try:
with self.machine.vm.qmp() as qmp:
qmp.command("system_powerdown")
except ClanError as e:
log.debug(e)
self._stop_timer_init = datetime.now()
self._stop_watcher_id = GLib.timeout_add(100, self.__shutdown_watchdog)
if self._stop_watcher_id == 0:

View File

@ -54,6 +54,7 @@ class ClanList(Gtk.Box):
groups = Clans.use()
join = Join.use()
self.log_label: Gtk.Label = Gtk.Label()
self.__init_machines = history.add.list_history()
self.join_boxed_list = create_boxed_list(
model=join.list_store, render_row=self.render_join_row
@ -126,25 +127,13 @@ class ClanList(Gtk.Box):
self.group_list.add_css_class("no-shadow")
def render_vm_row(self, boxed_list: Gtk.ListBox, vm: VM) -> Gtk.Widget:
# Remove no-shadow class if attached
if boxed_list.has_css_class("no-shadow"):
boxed_list.remove_css_class("no-shadow")
flake = vm.data.flake
row = Adw.ActionRow()
# Title
row.set_title(flake.flake_attr)
row.set_title_lines(1)
row.set_title_selectable(True)
# Subtitle
if flake.vm.machine_description:
row.set_subtitle(flake.vm.machine_description)
else:
row.set_subtitle(flake.clan_name)
row.set_subtitle_lines(1)
# Avatar
# ====== Display Avatar ======
avatar = Adw.Avatar()
machine_icon = flake.vm.machine_icon
@ -159,7 +148,26 @@ class ClanList(Gtk.Box):
avatar.set_size(50)
row.add_prefix(avatar)
# Switch
# ====== Display Name And Url =====
row.set_title(flake.flake_attr)
row.set_title_lines(1)
row.set_title_selectable(True)
if flake.vm.machine_description:
row.set_subtitle(flake.vm.machine_description)
else:
row.set_subtitle(flake.clan_name)
row.set_subtitle_lines(1)
# ==== Display build progress bar ====
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER)
box.append(vm.progress_bar)
box.set_homogeneous(False)
row.add_suffix(box) # This allows children to have different sizes
# ==== Action buttons ====
switch = Gtk.Switch()
switch_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@ -169,10 +177,6 @@ class ClanList(Gtk.Box):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
box.set_valign(Gtk.Align.CENTER)
# suffix_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
# suffix.set_halign(Gtk.Align.CENTER)
# suffix_box.append(switch)
open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s"))
open_action.connect("activate", self.on_edit)
@ -190,7 +194,6 @@ class ClanList(Gtk.Box):
switch.connect("notify::active", partial(self.on_row_toggle, vm))
vm.connect("vm_status_changed", partial(self.vm_status_changed, switch))
vm.connect("build_vm", self.build_vm)
# suffix.append(box)
row.add_suffix(box)
@ -251,9 +254,6 @@ class ClanList(Gtk.Box):
def show_error_dialog(self, error: str) -> None:
p = Views.use().main_window
# app = Gio.Application.get_default()
# p = Gtk.Application.get_active_window(app)
dialog = Adw.MessageDialog(heading="Error")
dialog.add_response("ok", "ok")
dialog.set_body(error)
@ -294,12 +294,6 @@ class ClanList(Gtk.Box):
row.set_state(True)
vm.stop()
def build_vm(self, vm: VM, _vm: VM, building: bool) -> None:
if building:
log.info("Building VM")
else:
log.info("VM built")
def vm_status_changed(self, switch: Gtk.Switch, vm: VM, _vm: VM) -> None:
switch.set_active(vm.is_running())
switch.set_state(vm.is_running())

View File

@ -1,4 +1,4 @@
{ lib, stdenv, clan-vm-manager, libadwaita, clan-cli, mkShell, ruff, desktop-file-utils, xdg-utils, mypy, python3Packages }:
{ lib, stdenv, clan-vm-manager, gtk4, libadwaita, clan-cli, mkShell, ruff, desktop-file-utils, xdg-utils, mypy, python3Packages }:
mkShell {
inherit (clan-vm-manager) propagatedBuildInputs buildInputs;
@ -11,6 +11,7 @@ mkShell {
desktop-file-utils
mypy
python3Packages.ipdb
gtk4.dev
libadwaita.devdoc # has the demo called 'adwaita-1-demo'
] ++ clan-vm-manager.nativeBuildInputs;