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)