@@ -113,6 +113,8 @@ def __init__(self, app_name, qapp, dispatcher):
113113 self .dispvm_templates : Set [backend .VM ] = set ()
114114 self .parent_ports_to_hide = []
115115 self .sysusb : backend .VM | None = None
116+ self .dev_update_queue : Set = set ()
117+ self .vm_update_queue : Set = set ()
116118
117119 self .dispatcher : qubesadmin .events .EventsDispatcher = dispatcher
118120 self .qapp : qubesadmin .Qubes = qapp
@@ -132,7 +134,17 @@ def __init__(self, app_name, qapp, dispatcher):
132134 "device-detach:" + devclass , self .device_detached
133135 )
134136 self .dispatcher .add_handler (
135- "device-list-change:" + devclass , self .device_list_update
137+ "device-list-change:" + devclass , self ._update_queue
138+ )
139+ self .dispatcher .add_handler ("device-added:" + devclass , self .device_added )
140+ self .dispatcher .add_handler (
141+ "device-removed:" + devclass , self .device_removed
142+ )
143+ self .dispatcher .add_handler (
144+ "device-assign:" + devclass , self .device_assigned
145+ )
146+ self .dispatcher .add_handler (
147+ "device-unassign:" + devclass , self .device_unassigned
136148 )
137149
138150 self .dispatcher .add_handler ("domain-shutdown" , self .vm_shutdown )
@@ -167,73 +179,55 @@ def __init__(self, app_name, qapp, dispatcher):
167179 "<b>Qubes Devices</b>\n View and manage devices."
168180 )
169181
170- def device_list_update (self , vm , event , ** _kwargs ):
171- devclass = event .split (":" )[1 ]
172- changed_devices : Dict [str , Any ] = {}
173- # create list of all current devices from the changed VM
174- try :
175- for device in vm .devices [devclass ]:
176- changed_devices [backend .Device .id_from_device (device )] = device
177- except qubesadmin .exc .QubesException :
178- changed_devices = {} # VM was removed
179-
180- microphone = self .devices .get ("dom0:mic:dom0:mic::m000000" , None )
181-
182- for dev_id , device in changed_devices .items ():
183- if dev_id not in self .devices :
184- dev = backend .Device (device , self )
185- dev .connection_timestamp = time .monotonic ()
186- self .devices [dev_id ] = dev
187- if dev .parent :
188- for potential_parent in self .devices .values ():
189- if potential_parent .port == dev .parent :
190- potential_parent .has_children = True
191-
192- # connect with mic
193- mic_feature = vm .features .get (
194- backend .FEATURE_ATTACH_WITH_MIC , ""
195- ).split (" " )
196- if dev_id in mic_feature :
197- microphone .devices_to_attach_with_me .append (dev )
198- dev .devices_to_attach_with_me = [microphone ]
182+ def _update_queue (self , vm , device , ** _kwargs ):
183+ """Handle certain operations that should not be done too often."""
184+ # update children
185+ if vm not in self .vm_update_queue :
186+ self .vm_update_queue .add (vm )
187+ asyncio .create_task (self .update_parents (vm ))
188+ if device not in self .dev_update_queue :
189+ self .dev_update_queue .add (device )
190+ asyncio .create_task (self .update_assignments ())
191+
192+ async def update_assignments (self ):
193+ """Scan vm list for new assignments"""
194+ await asyncio .sleep (0.3 )
195+
196+ if not self .dev_update_queue :
197+ return
198+ devs = self .dev_update_queue .copy ()
199+ self .dev_update_queue .clear ()
199200
200- # hide children
201- child_feature = vm . features . get (
202- backend . FEATURE_HIDE_CHILDREN , ""
203- ). split ( " " )
204- if dev_id in child_feature :
205- self .parent_ports_to_hide . append ( dev . port )
206- dev . show_children = False
201+ for domain in self . qapp . domains :
202+ for devclass in DEV_TYPES :
203+ try :
204+ for device in domain . devices [ devclass ]. get_attached_devices ():
205+ dev = backend . Device . id_from_device ( device )
206+ if dev in devs and dev in self .devices :
207+ self . devices [ dev ]. attachments . add ( backend . VM ( domain ))
207208
208- self .emit_notification (
209- _ ("Device available" ),
210- _ ("Device {} is available." ).format (dev .description ),
211- Gio .NotificationPriority .NORMAL ,
212- notification_id = dev .notification_id ,
213- )
209+ for device in domain .devices [devclass ].get_assigned_devices ():
210+ dev = backend .Device .id_from_device (device )
211+ if dev in devs and dev in self .devices :
212+ self .devices [dev ].assignments .add (backend .VM (domain ))
213+ except qubesadmin .exc .QubesException :
214+ # we have no permission to access VM's devices
215+ continue
214216
215- dev_to_remove = []
217+ async def update_parents (self , vm ):
218+ await asyncio .sleep (0.3 )
216219
217- for dev_id , dev in self .devices .items ():
218- if dev .backend_domain != vm or dev .device_class != devclass :
219- continue
220- if dev_id not in changed_devices :
221- dev_to_remove .append ((dev_id , dev ))
222-
223- for dev_id , dev in dev_to_remove :
224- self .emit_notification (
225- _ ("Device removed" ),
226- _ ("Device {} has been removed." ).format (dev .description ),
227- Gio .NotificationPriority .NORMAL ,
228- notification_id = dev .notification_id ,
229- )
230- if dev in microphone .devices_to_attach_with_me :
231- microphone .devices_to_attach_with_me .remove (dev )
232- if dev .port in self .parent_ports_to_hide :
233- self .parent_ports_to_hide .remove (dev .port )
234- del self .devices [dev_id ]
235-
236- self .hide_child_devices ()
220+ if vm not in self .vm_update_queue :
221+ return
222+ self .vm_update_queue .remove (vm )
223+
224+ self .update_single_feature (
225+ None ,
226+ None ,
227+ backend .FEATURE_HIDE_CHILDREN ,
228+ value = vm .features .get (backend .FEATURE_HIDE_CHILDREN , "" ),
229+ oldvalue = "" ,
230+ )
237231
238232 def initialize_vm_data (self ):
239233 for vm in self .qapp .domains :
@@ -250,6 +244,61 @@ def initialize_vm_data(self):
250244 # we don't have access to VM state
251245 pass
252246
247+ def device_added (self , vm , _event , device ):
248+ dev_id = backend .Device .id_from_device (device )
249+ dev = backend .Device (device , self )
250+ dev .connection_timestamp = time .monotonic ()
251+ self .devices [dev_id ] = dev
252+
253+ if dev .parent :
254+ for potential_parent in self .devices .values ():
255+ if potential_parent .port == dev .parent :
256+ potential_parent .has_children = True
257+ break
258+
259+ # connect with mic
260+ mic_feature = vm .features .get (backend .FEATURE_ATTACH_WITH_MIC , "" ).split (" " )
261+ if dev_id in mic_feature :
262+ microphone = self .devices .get ("dom0:mic:dom0:mic::m000000" , None )
263+ microphone .devices_to_attach_with_me .append (dev )
264+ dev .devices_to_attach_with_me = [microphone ]
265+
266+ self .emit_notification (
267+ _ ("Device available" ),
268+ _ ("Device {} is available." ).format (dev .description ),
269+ Gio .NotificationPriority .NORMAL ,
270+ notification_id = dev .notification_id ,
271+ )
272+
273+ self ._update_queue (vm , dev_id )
274+
275+ def device_removed (self , vm , _event , port ):
276+ for potential_dev_id , potential_dev in self .devices .items ():
277+ if (
278+ potential_dev .backend_domain .name != vm .name
279+ or potential_dev .port != str (port )
280+ ):
281+ continue
282+ dev , dev_id = potential_dev , potential_dev_id
283+ break
284+ else :
285+ # we never knew the device anyway
286+ return
287+
288+ microphone = self .devices .get ("dom0:mic:dom0:mic::m000000" , None )
289+
290+ self .emit_notification (
291+ _ ("Device removed" ),
292+ _ ("Device {} has been removed." ).format (dev .description ),
293+ Gio .NotificationPriority .NORMAL ,
294+ notification_id = dev .notification_id ,
295+ )
296+ if dev in microphone .devices_to_attach_with_me :
297+ microphone .devices_to_attach_with_me .remove (dev )
298+ if dev .port in self .parent_ports_to_hide :
299+ self .parent_ports_to_hide .remove (dev .port )
300+ del self .devices [dev_id ]
301+
253302 def initialize_dev_data (self ):
254303 # list all devices
255304 for domain in self .qapp .domains :
@@ -289,6 +338,23 @@ def initialize_dev_data(self):
289338 # we have no permission to access VM's devices
290339 continue
291340
341+ def device_assigned (self , vm , _event , device , ** _kwargs ):
342+ dev_id = backend .Device .id_from_device (device )
343+ if dev_id not in self .devices :
344+ return
345+ self .devices [dev_id ].assignments .add (backend .VM (vm ))
346+
347+ def device_unassigned (self , vm , _event , device , ** _kwargs ):
348+ dev_id = backend .Device .id_from_device (device )
349+ if dev_id not in self .devices :
350+ return
351+ try :
352+ self .devices [dev_id ].assignments .remove (backend .VM (vm ))
353+ except KeyError :
354+ # it's ok, somehow we got an unassign for a device we didn't store as
355+ # assigned. Cheers!
356+ return
357+
292358 def update_single_feature (self , _vm , _event , feature , value = None , oldvalue = None ):
293359 if not value :
294360 new = set ()
@@ -448,8 +514,14 @@ def vm_shutdown(self, vm, _event, **_kwargs):
448514 self .vms .discard (wrapped_vm )
449515 self .dispvm_templates .discard (wrapped_vm )
450516
517+ devs_to_remove = []
451518 for dev in self .devices .values ():
519+ if dev .backend_domain == wrapped_vm :
520+ devs_to_remove .append (dev .port )
521+ continue
452522 dev .attachments .discard (wrapped_vm )
523+ for dev_port in devs_to_remove :
524+ self .device_removed (vm , None , port = dev_port )
453525
454526 def vm_dispvm_template_change (self , vm , _event , ** _kwargs ):
455527 """Is template for dispvms property changed"""
0 commit comments