diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py
index 492ab285..0bcc1ac1 100644
--- a/qui/devices/actionable_widgets.py
+++ b/qui/devices/actionable_widgets.py
@@ -361,20 +361,20 @@ async def widget_action(self, *_args):
)
-#### Start sys-usb
+#### Start sys-usb (or other custom USBVMs)
-class StartSysUsb(ActionableWidget, SimpleActionWidget):
- def __init__(self, sysusb: backend.VM, variant: str = "dark"):
+class StartUSBVM(ActionableWidget, SimpleActionWidget):
+ def __init__(self, usbvm: backend.VM, variant: str = "dark"):
super().__init__(
- icon_name=sysusb.icon_name,
- text="List USB Devices " "(start sys-usb)",
+ icon_name=usbvm.icon_name,
+ text="List USB Devices " "(start {})".format(usbvm.name),
variant=variant,
)
- self.sysusb = sysusb
+ self.usbvm = usbvm
async def widget_action(self, *_args):
- self.sysusb.vm_object.start()
+ self.usbvm.vm_object.start()
#### Configuration-related actions
diff --git a/qui/devices/backend.py b/qui/devices/backend.py
index 38f98643..9c78c784 100644
--- a/qui/devices/backend.py
+++ b/qui/devices/backend.py
@@ -87,6 +87,19 @@ def is_attachable(self) -> bool:
"""
return self.vm_class != "AdminVM" and self._vm.is_running()
+ @property
+ def is_usbvm(self) -> bool:
+ """
+ Does the VM have a PCI USB controller attached to it?
+ We do not need to cache this since it is checked only at Qui Devices
+ initialization as well a PCI assignment/un-assignment
+ """
+ for dev in self._vm.devices["pci"].get_assigned_devices():
+ for interface in dev.device.interfaces:
+ if interface.category == DeviceCategory.PCI_USB:
+ return True
+ return False
+
@property
def vm_object(self):
"""
diff --git a/qui/devices/device_widget.py b/qui/devices/device_widget.py
index 018c98fb..2d375944 100644
--- a/qui/devices/device_widget.py
+++ b/qui/devices/device_widget.py
@@ -112,7 +112,8 @@ def __init__(self, app_name, qapp, dispatcher):
self.vms: Set[backend.VM] = set()
self.dispvm_templates: Set[backend.VM] = set()
self.parent_ports_to_hide = []
- self.sysusb: backend.VM | None = None
+ self.active_usbvms: Set[backend.VM] = set()
+ self.dormant_usbvms: Set[backend.VM] = set()
self.dev_update_queue: Set = set()
self.vm_update_queue: Set = set()
@@ -144,6 +145,10 @@ def __init__(self, app_name, qapp, dispatcher):
"device-unassign:" + devclass, self.device_unassigned
)
+ self.dispatcher.add_handler("device-assign:pci", self.pci_action)
+ self.dispatcher.add_handler("device-unassign:pci", self.pci_action)
+ self.dispatcher.add_handler("domain-pre-delete", self.remove_domain_item)
+
self.dispatcher.add_handler("domain-shutdown", self.vm_shutdown)
self.dispatcher.add_handler("domain-start-failed", self.vm_shutdown)
self.dispatcher.add_handler("domain-start", self.vm_start)
@@ -242,9 +247,12 @@ def initialize_vm_data(self):
self.vms.add(wrapped_vm)
if wrapped_vm.is_dispvm_template:
self.dispvm_templates.add(wrapped_vm)
- if vm.name == "sys-usb":
- self.sysusb = wrapped_vm
- self.sysusb.is_running = vm.is_running()
+ # Keep track of USBVMs
+ if wrapped_vm.is_usbvm:
+ if vm.is_running():
+ self.active_usbvms.add(wrapped_vm)
+ else:
+ self.dormant_usbvms.add(wrapped_vm)
except qubesadmin.exc.QubesException:
# we don't have access to VM state
pass
@@ -360,6 +368,24 @@ def device_unassigned(self, vm, _event, device, **_kwargs):
# assigned. Cheers!
return
+ def pci_action(self, vm, _event, **_kwargs):
+ # We assume PCI controllers could be assigned only when qube is shutdown
+ wrapped_vm = backend.VM(vm)
+ if wrapped_vm.is_usbvm:
+ if wrapped_vm not in self.dormant_usbvms:
+ self.dormant_usbvms.add(wrapped_vm)
+ elif wrapped_vm in self.dormant_usbvms:
+ self.dormant_usbvms.discard(wrapped_vm)
+
+ def remove_domain_item(self, _submitter, _event, vm, **_kwargs):
+ # In a perfect world, core should trigger `device-unassign:pci` event
+ # for PCI devices attached to an HVM before actually removing it and
+ # this method should not be necessary. But we are not certain :/
+ for wrapped_vm in self.dormant_usbvms:
+ if wrapped_vm.name == vm:
+ self.dormant_usbvms.discard(wrapped_vm)
+ break
+
def update_single_feature(self, _vm, _event, feature, value=None, oldvalue=None):
if not value:
new = set()
@@ -533,8 +559,9 @@ def vm_start(self, vm, _event, **_kwargs):
internal, attachable = False, False
if attachable and not internal:
self.vms.add(wrapped_vm)
- if wrapped_vm == self.sysusb:
- self.sysusb.is_running = True
+ if wrapped_vm in self.dormant_usbvms:
+ self.dormant_usbvms.discard(wrapped_vm)
+ self.active_usbvms.add(wrapped_vm)
for devclass in DEV_TYPES:
try:
@@ -548,8 +575,9 @@ def vm_start(self, vm, _event, **_kwargs):
def vm_shutdown(self, vm, _event, **_kwargs):
wrapped_vm = backend.VM(vm)
- if wrapped_vm == self.sysusb:
- self.sysusb.is_running = False
+ if wrapped_vm in self.active_usbvms:
+ self.active_usbvms.discard(wrapped_vm)
+ self.dormant_usbvms.add(wrapped_vm)
self.vms.discard(wrapped_vm)
self.dispvm_templates.discard(wrapped_vm)
@@ -633,13 +661,13 @@ def show_menu(self, _unused, _event):
menu_items.append(device_item)
- if not self.sysusb.is_running:
- sysusb_item = actionable_widgets.generate_wrapper_widget(
+ for wrapped_vm in self.dormant_usbvms:
+ usbvm_item = actionable_widgets.generate_wrapper_widget(
Gtk.MenuItem,
"activate",
- actionable_widgets.StartSysUsb(self.sysusb, theme),
+ actionable_widgets.StartUSBVM(wrapped_vm, theme),
)
- menu_items.append(sysusb_item)
+ menu_items.append(usbvm_item)
for item in menu_items:
tray_menu.add(item)