Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion qubesmanager/qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,18 @@
self.state = {'power': "", 'outdated': ""}
self.updateable = getattr(vm, 'updateable', False)
self.update(True)
self.CPU_usage = None
self.RAM_usage = None

def update_resource_usage(self, RAM_usage, CPU_usage):
self.RAM_usage = RAM_usage
self.CPU_usage = CPU_usage

def update_power_state(self):
try:
self.state['power'] = self.vm.get_power_state()
if self.state["power"] in ["Halted", "Paused", "Suspended"]:
self.update_resource_usage(None, None)
if self.state['power'] == "Halted" and \
self.vm.klass != "AdminVM" and \
manager_utils.get_feature(
Expand All @@ -259,6 +267,7 @@
except exc.QubesDaemonAccessError:
self.state['power'] = ""


self.state['outdated'] = ""
try:
if manager_utils.is_running(self.vm, False):
Expand Down Expand Up @@ -422,6 +431,8 @@
"Label",
"Name",
"State",
"CPU",
"MEM",
"Template",
"NetVM",
"Disk Usage",
Expand Down Expand Up @@ -453,6 +464,12 @@
col_name = self.columns_indices[col]
vm = self.qubes_cache.get_vm(row)

if role == Qt.ItemDataRole.SizeHintRole:
if col_name in ["CPU", "MEM"]:
return QSize(100, 22)
if role == Qt.ItemDataRole.TextAlignmentRole:
if col_name in ["CPU", "MEM", "Disk Usage"]:
return Qt.AlignmentFlag.AlignCenter
if role == Qt.ItemDataRole.DisplayRole:
if col_name == "Name":
return vm.name
Expand All @@ -464,6 +481,14 @@
return vm.template
if col_name == "NetVM":
return vm.netvm
if col_name == "CPU":
if not vm.CPU_usage:
return "-"
return vm.CPU_usage + " %"
if col_name == "MEM":
if not vm.RAM_usage:
return "-"
return str(int(int(vm.RAM_usage) / 1024)) + " MiB"
if col_name == "Disk Usage":
return vm.disk
if col_name == "Internal":
Expand Down Expand Up @@ -508,6 +533,13 @@
if role == Qt.ItemDataRole.UserRole + 1:
if vm.klass == 'AdminVM':
return ""
# Consider allowing dom0 to be sorted for CPU & MEM usage
if col_name == "CPU":
if vm.CPU_usage:
return int(vm.CPU_usage)

Check warning on line 539 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L538-L539

Added lines #L538 - L539 were not covered by tests
if col_name == "MEM":
if vm.RAM_usage:
return int(vm.RAM_usage)

Check warning on line 542 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L541-L542

Added lines #L541 - L542 were not covered by tests
if col_name == "Label":
vmtype, vmcolor = vm.icon.split("-", 1)
try:
Expand Down Expand Up @@ -746,7 +778,14 @@
# suppress saving settings while initializing widgets
settings_loaded = False

def __init__(self, qt_app, qubes_app, dispatcher, _parent=None):
def __init__(
self,
qt_app,
qubes_app,
dispatcher,
stats_dispatcher=None,
_parent=None
):
# pylint: disable=too-many-statements
super().__init__()
self.setupUi(self)
Expand All @@ -772,6 +811,7 @@

self.frame_width = 0
self.frame_height = 0
self.foreground = True

self.init_template_menu()
self.init_network_menu()
Expand Down Expand Up @@ -908,6 +948,9 @@
dispatcher.add_handler('domain-feature-delete:skip-update',
self.on_domain_updates_available)

if stats_dispatcher:
stats_dispatcher.add_handler("vm-stats", self.on_vm_stats)

self.installEventFilter(self)

# It needs to store threads until they finish
Expand All @@ -927,8 +970,10 @@
if event.type() == QEvent.Type.WindowActivate:
self.update_running_size()
self.size_on_disk_timer.setInterval(1000 * 60)
self.foreground = True
elif event.type() == QEvent.Type.WindowDeactivate:
self.size_on_disk_timer.setInterval(1000 * 60 * 5)
self.foreground = False
return False

def scroll_to_top(self):
Expand Down Expand Up @@ -1191,6 +1236,16 @@
self.qubes_cache.get_vm(qid=vm.qid).update(
update_size_on_disk=True, event='disk_size')

def on_vm_stats(self, vm, _event, **kwargs):
if not self.foreground:
return
domain = self.qubes_app.domains[vm]
self.qubes_cache.get_vm(qid=domain.qid).update_resource_usage(
kwargs["memory_kb"],
kwargs["cpu_usage"],
)
self.proxy.invalidate()

def on_domain_added(self, _submitter, _event, vm, **_kwargs):
try:
domain = self.qubes_app.domains[vm]
Expand Down
34 changes: 32 additions & 2 deletions qubesmanager/tests/test_qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import time
from datetime import datetime

from PyQt6.QtCore import Qt, QSettings, QItemSelectionModel
from PyQt6.QtCore import Qt, QSettings, QItemSelectionModel, QEvent
from PyQt6.QtGui import QPixmap, QIcon
from PyQt6.QtWidgets import QMessageBox

Expand Down Expand Up @@ -168,7 +168,12 @@ def test_000_window_loads(qapp, test_qubes_app):

def test_001_model_correctness(qapp, test_qubes_app):
dispatcher = MockDispatcher(test_qubes_app)
qm = qube_manager.VmManagerWindow(qapp, test_qubes_app, dispatcher)
qm = qube_manager.VmManagerWindow(
qapp,
test_qubes_app,
dispatcher,
stats_dispatcher=dispatcher,
)

model = qm.qubes_model

Expand All @@ -177,6 +182,13 @@ def test_001_model_correctness(qapp, test_qubes_app):
# number of domains
assert model.rowCount(None) == len(domains)

# emulating CPU/MEM usage update for dom0
qm.on_vm_stats('dom0', _event=None, memory_kb="67108864", cpu_usage="100")
# emulating skipping of CPU/MEM usage update for personal
qm.eventFilter(_object=None, event=QEvent(QEvent.Type.WindowDeactivate))
qm.on_vm_stats('personal', _event=None, memory_kb="524288", cpu_usage="1")
qm.eventFilter(_object=None, event=QEvent(QEvent.Type.WindowActivate))

# domain data
for row in range(model.rowCount(None)):
# name
Expand Down Expand Up @@ -222,6 +234,24 @@ def test_001_model_correctness(qapp, test_qubes_app):
if getattr(vm_object, 'internal', False):
assert internal_data == "Yes"

# cpu usage
cpu_index = model.index(row, model.columns_indices.index(
"CPU"))
cpu_data = model.data(cpu_index, Qt.ItemDataRole.DisplayRole)
if vm_object.klass == "AdminVM":
assert str(cpu_data) == "100 %"
else:
assert str(cpu_data) == "-"

# mem usage
mem_index = model.index(row, model.columns_indices.index(
"MEM"))
mem_data = model.data(mem_index, Qt.ItemDataRole.DisplayRole)
if vm_object.klass == "AdminVM":
assert str(mem_data) == "65536 MiB"
else:
assert str(mem_data) == "-"

# disk usage
du_index = model.index(row, model.columns_indices.index(
"Disk Usage"))
Expand Down
7 changes: 6 additions & 1 deletion qubesmanager/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,14 @@
loop = qasync.QEventLoop(qt_app)
asyncio.set_event_loop(loop)

stats_dispatcher = events.EventsDispatcher(

Check warning on line 565 in qubesmanager/utils.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/utils.py#L565

Added line #L565 was not covered by tests
qubes_app,
api_method = "admin.vm.Stats"
)
async def setup():
dispatcher = events.EventsDispatcher(qubes_app)

window = window_class(qt_app, qubes_app, dispatcher)
window = window_class(qt_app, qubes_app, dispatcher, stats_dispatcher)

Check warning on line 572 in qubesmanager/utils.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/utils.py#L572

Added line #L572 was not covered by tests

if hasattr(window, "setup_application"):
window.setup_application()
Expand All @@ -575,6 +579,7 @@
await dispatcher.listen_for_events()

try:
loop.create_task(stats_dispatcher.listen_for_events())

Check warning on line 582 in qubesmanager/utils.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/utils.py#L582

Added line #L582 was not covered by tests
loop.run_until_complete(asyncio.ensure_future(setup()))
except asyncio.CancelledError:
pass
Expand Down