23
23
get_fullscreen_window_hack ,
24
24
) # isort:skip
25
25
26
- from typing import Set , List , Dict , Optional , Any
26
+ from typing import Set , List , Dict , Optional
27
27
import asyncio
28
28
import sys
29
29
import time
@@ -113,6 +113,8 @@ def __init__(self, app_name, qapp, dispatcher):
113
113
self .dispvm_templates : Set [backend .VM ] = set ()
114
114
self .parent_ports_to_hide = []
115
115
self .sysusb : backend .VM | None = None
116
+ self .dev_update_queue : Set = set ()
117
+ self .vm_update_queue : Set = set ()
116
118
117
119
self .dispatcher : qubesadmin .events .EventsDispatcher = dispatcher
118
120
self .qapp : qubesadmin .Qubes = qapp
@@ -132,7 +134,17 @@ def __init__(self, app_name, qapp, dispatcher):
132
134
"device-detach:" + devclass , self .device_detached
133
135
)
134
136
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
136
148
)
137
149
138
150
self .dispatcher .add_handler ("domain-shutdown" , self .vm_shutdown )
@@ -167,73 +179,55 @@ def __init__(self, app_name, qapp, dispatcher):
167
179
"<b>Qubes Devices</b>\n View and manage devices."
168
180
)
169
181
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 ()
199
200
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 ))
207
208
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
214
216
215
- dev_to_remove = []
217
+ async def update_parents (self , vm ):
218
+ await asyncio .sleep (0.3 )
216
219
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
+ )
237
231
238
232
def initialize_vm_data (self ):
239
233
for vm in self .qapp .domains :
@@ -250,6 +244,61 @@ def initialize_vm_data(self):
250
244
# we don't have access to VM state
251
245
pass
252
246
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
+
253
302
def initialize_dev_data (self ):
254
303
# list all devices
255
304
for domain in self .qapp .domains :
@@ -289,6 +338,23 @@ def initialize_dev_data(self):
289
338
# we have no permission to access VM's devices
290
339
continue
291
340
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
+
292
358
def update_single_feature (self , _vm , _event , feature , value = None , oldvalue = None ):
293
359
if not value :
294
360
new = set ()
@@ -448,8 +514,14 @@ def vm_shutdown(self, vm, _event, **_kwargs):
448
514
self .vms .discard (wrapped_vm )
449
515
self .dispvm_templates .discard (wrapped_vm )
450
516
517
+ devs_to_remove = []
451
518
for dev in self .devices .values ():
519
+ if dev .backend_domain == wrapped_vm :
520
+ devs_to_remove .append (dev .port )
521
+ continue
452
522
dev .attachments .discard (wrapped_vm )
523
+ for dev_port in devs_to_remove :
524
+ self .device_removed (vm , None , port = dev_port )
453
525
454
526
def vm_dispvm_template_change (self , vm , _event , ** _kwargs ):
455
527
"""Is template for dispvms property changed"""
0 commit comments