Skip to content

Commit ec2e23c

Browse files
committed
Schedule synchronous function to separate thread
Waiting for the server to send the result of the API calls, allows dealing with failures/exceptions: - QubesVM.shutdown -> .shutdown(wait=True) - QubesVM.run_service -> .run_service_for_stdio It also does not block the widget from being opened again even if an action is still running. This allows, for example: - To create disposable qube and attach device to it (which takes 10s), but not block the widget from opening - To detach and shutdown disposable that is hanging, without hanging the widget For qui/tray/domains.py, the "widget.destroy()" is called earlier, cause it's not needed after the response is received. Else it hangs until the "react_to_question" finishes. For: QubesOS/qubes-issues#10648 For: QubesOS/qubes-issues#10651 For: QubesOS/qubes-issues#10835 Requires: QubesOS/qubes-core-admin#807 Requires: QubesOS/qubes-core-admin-client#469
1 parent 1f295d9 commit ec2e23c

3 files changed

Lines changed: 71 additions & 55 deletions

File tree

qui/devices/actionable_widgets.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import asyncio
2727
import functools
2828
import pathlib
29+
import time
2930
from typing import Iterable, Callable, Optional, List
3031

3132
import qubesadmin
@@ -38,7 +39,6 @@
3839
from gi.repository import Gtk, GdkPixbuf, GLib # isort:skip
3940

4041
from . import backend
41-
import time
4242

4343

4444
def load_icon(icon_name: str, backup_name: str, size: int = 24):
@@ -247,7 +247,7 @@ def __init__(self, vm: backend.VM, device: backend.Device):
247247
self.actionable = False
248248

249249
async def widget_action(self, *_args):
250-
self.device.attach_to_vm(self.vm)
250+
await asyncio.to_thread(self.device.attach_to_vm, self.vm)
251251

252252

253253
class DetachWidget(ActionableWidget, SimpleActionWidget):
@@ -259,7 +259,7 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
259259
self.device = device
260260

261261
async def widget_action(self, *_args):
262-
self.device.detach_from_vm(self.vm, False)
262+
await asyncio.to_thread(self.device.detach_from_vm, self.vm, False)
263263

264264

265265
class DetachWithWidget(ActionableWidget, SimpleActionWidget):
@@ -278,7 +278,7 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
278278
self.device = device
279279

280280
async def widget_action(self, *_args):
281-
self.device.detach_from_vm(self.vm, True)
281+
await asyncio.to_thread(self.device.detach_from_vm, self.vm, True)
282282

283283

284284
class DetachAndShutdownWidget(ActionableWidget, SimpleActionWidget):
@@ -292,8 +292,8 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
292292
self.device = device
293293

294294
async def widget_action(self, *_args):
295-
self.device.detach_from_vm(self.vm, True)
296-
self.vm.vm_object.shutdown()
295+
await asyncio.to_thread(self.device.detach_from_vm, self.vm, True)
296+
await asyncio.to_thread(self.vm.vm_object.shutdown)
297297

298298

299299
class DetachAndAttachWidget(ActionableWidget, VMWithIcon):
@@ -306,8 +306,8 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
306306

307307
async def widget_action(self, *_args):
308308
for vm in self.device.attachments:
309-
self.device.detach_from_vm(vm, True)
310-
self.device.attach_to_vm(self.vm)
309+
await asyncio.to_thread(self.device.detach_from_vm, vm, True)
310+
await asyncio.to_thread(self.device.attach_to_vm, self.vm)
311311

312312

313313
class AttachDisposableWidget(ActionableWidget, VMWithIcon):
@@ -320,9 +320,9 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
320320

321321
async def widget_action(self, *_args):
322322
new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm)
323-
new_dispvm.start()
323+
await asyncio.to_thread(new_dispvm.start)
324324

325-
self.device.attach_to_vm(backend.VM(new_dispvm))
325+
await asyncio.to_thread(self.device.attach_to_vm, backend.VM(new_dispvm))
326326

327327

328328
class DetachAndAttachDisposableWidget(ActionableWidget, VMWithIcon):
@@ -334,11 +334,11 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
334334
self.device = device
335335

336336
async def widget_action(self, *_args):
337-
self.device.detach_from_vm(self.vm)
337+
await asyncio.to_thread(self.device.detach_from_vm, self.vm)
338338
new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm)
339-
new_dispvm.start()
339+
await asyncio.to_thread(new_dispvm.start)
340340

341-
self.device.attach_to_vm(backend.VM(new_dispvm))
341+
await asyncio.to_thread(self.device.attach_to_vm, backend.VM(new_dispvm))
342342

343343

344344
class ToggleFeatureItem(ActionableWidget, SimpleActionWidget):
@@ -379,7 +379,7 @@ def __init__(self, usbvm: backend.VM, variant: str = "dark"):
379379
self.usbvm = usbvm
380380

381381
async def widget_action(self, *_args):
382-
self.usbvm.vm_object.start()
382+
await asyncio.to_thread(self.usbvm.vm_object.start)
383383

384384

385385
#### Configuration-related actions

qui/tray/domains.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def __init__(self, vm, icon_cache):
157157

158158
async def perform_action(self):
159159
try:
160-
self.vm.pause()
160+
await asyncio.to_thread(self.vm.pause)
161161
except exc.QubesException as ex:
162162
show_error(
163163
_("Error pausing qube"),
@@ -178,7 +178,7 @@ def __init__(self, vm, icon_cache):
178178

179179
async def perform_action(self):
180180
try:
181-
self.vm.unpause()
181+
await asyncio.to_thread(self.vm.unpause)
182182
except exc.QubesException as ex:
183183
show_error(
184184
_("Error unpausing qube"),
@@ -215,7 +215,7 @@ def set_force(self, force):
215215

216216
async def perform_action(self):
217217
try:
218-
self.vm.shutdown(force=self.force)
218+
await asyncio.to_thread(self.vm.shutdown, force=self.force, wait=True)
219219
except exc.QubesException as ex:
220220
if self.force:
221221
show_error(
@@ -271,19 +271,22 @@ async def perform_action(self):
271271
GLib.idle_add(dialog.show)
272272

273273
def react_to_question(self, widget, response, action):
274+
asyncio.create_task(self.react_to_question_async(widget, response, action))
275+
276+
async def react_to_question_async(self, widget, response, action):
274277
if response not in (Gtk.ResponseType.OK, Gtk.ResponseType.YES):
275278
widget.destroy()
276279
return
277280
try:
278281
if action == "force":
279-
self.vm.shutdown(force=True)
282+
await asyncio.to_thread(self.vm.shutdown, force=True, wait=True)
280283
elif action == "timeout":
281284
if response == Gtk.ResponseType.YES:
282-
self.vm.kill()
285+
await asyncio.to_thread(self.vm.kill)
283286
elif response == Gtk.ResponseType.OK:
284-
self.vm.shutdown(force=False)
287+
await asyncio.to_thread(self.vm.shutdown, force=False, wait=True)
285288
elif action == "kill" and response == Gtk.ResponseType.OK:
286-
self.vm.kill()
289+
await asyncio.to_thread(self.vm.kill)
287290
except exc.QubesException as ex:
288291
show_error(
289292
_("Error shutting down qube"),
@@ -320,7 +323,7 @@ def set_force(self, force):
320323

321324
async def perform_action(self, *_args, **_kwargs):
322325
try:
323-
self.vm.shutdown(force=self.force)
326+
await asyncio.to_thread(self.vm.shutdown, force=self.force, wait=True)
324327
except exc.QubesException as ex:
325328
if self.force:
326329
# we already tried forcing it, let's just give up
@@ -351,12 +354,7 @@ async def perform_action(self, *_args, **_kwargs):
351354
if self.give_up:
352355
return
353356
await asyncio.sleep(1)
354-
proc = await asyncio.create_subprocess_exec(
355-
"qvm-start", self.vm.name, stderr=asyncio.subprocess.PIPE
356-
)
357-
_stdout, stderr = await proc.communicate()
358-
if proc.returncode != 0:
359-
raise exc.QubesException(stderr)
357+
await asyncio.to_thread(self.vm.start)
360358
except exc.QubesException as ex:
361359
show_error(
362360
_("Error restarting qube"),
@@ -367,9 +365,12 @@ async def perform_action(self, *_args, **_kwargs):
367365
)
368366

369367
def react_to_question(self, widget, response):
368+
asyncio.create_task(self.react_to_question_async(widget, response))
369+
370+
async def react_to_question_async(self, widget, response):
370371
if response == Gtk.ResponseType.OK:
371372
try:
372-
self.vm.shutdown(force=True)
373+
await asyncio.to_thread(self.vm.shutdown, force=True, wait=True)
373374
except exc.QubesException as ex:
374375
show_error(
375376
_("Error shutting down qube"),
@@ -392,13 +393,13 @@ def __init__(self, vm, icon_cache):
392393

393394
async def perform_action(self, *_args, **_kwargs):
394395
try:
395-
self.vm.kill()
396+
await asyncio.to_thread(self.vm.kill)
396397
except exc.QubesException as ex:
397398
show_error(
398399
_("Error shutting down qube"),
399400
_(
400-
"The following error occurred while attempting to shut"
401-
"down qube {0}:\n{1}"
401+
"The following error occurred while attempting to kill"
402+
"qube {0}:\n{1}"
402403
).format(self.vm.name, str(ex)),
403404
)
404405

@@ -459,7 +460,11 @@ async def perform_action(self):
459460
if self.as_root:
460461
service_args["user"] = "root"
461462
try:
462-
self.vm.run_service("qubes.StartApp+qubes-run-terminal", **service_args)
463+
await asyncio.to_thread(
464+
self.vm.run_service_for_stdio,
465+
"qubes.StartApp+qubes-run-terminal",
466+
**service_args,
467+
)
463468
except exc.QubesException as ex:
464469
show_error(
465470
_("Error starting terminal"),
@@ -508,7 +513,9 @@ def __init__(self, vm, icon_cache):
508513

509514
async def perform_action(self):
510515
try:
511-
self.vm.run_service("qubes.StartApp+qubes-open-file-manager")
516+
await asyncio.to_thread(
517+
self.vm.run_service_for_stdio, "qubes.StartApp+qubes-open-file-manager"
518+
)
512519
except exc.QubesException as ex:
513520
show_error(
514521
_("Error opening file manager"),

qui/updater/summary_page.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from typing import Optional, Any
3232

3333
import qubesadmin
34-
from qubesadmin.events.utils import wait_for_domain_shutdown
3534

3635
from qubes_config.widgets.gtk_utils import (
3736
load_icon,
@@ -308,25 +307,26 @@ def shutdown_domains(self, to_shutdown):
308307
"""
309308
Try to shut down vms and wait to finish.
310309
"""
311-
wait_for = []
312-
for vm in to_shutdown:
313-
try:
314-
vm.shutdown(force=True)
315-
wait_for.append(vm)
316-
self.log.info("Shutdown %s", vm.name)
317-
except qubesadmin.exc.QubesVMError as err:
318-
self.err += vm.name + " cannot shutdown: " + str(err) + "\n"
319-
self.log.error("Cannot shutdown %s because %s", vm.name, str(err))
320-
self.status = RestartStatus.ERROR_TMPL_DOWN
321310
try:
322311
loop = asyncio.get_event_loop()
323312
except RuntimeError:
324313
# changes between GLib versions and python versions mean that the above
325314
# can fail on some dom0/gui domain configurations
326315
loop = asyncio.new_event_loop()
327-
loop.run_until_complete(wait_for_domain_shutdown(wait_for))
328-
329-
return wait_for
316+
tasks = [asyncio.to_thread(vm.shutdown, force=True, wait=True) for vm in to_shutdown]
317+
results = loop.run_until_complete(
318+
asyncio.gather(*tasks, return_exceptions=True)
319+
)
320+
done = []
321+
for vm, res in zip(to_shutdown, results):
322+
if not isinstance(res, BaseException):
323+
self.log.info("Shutdown %s", vm.name)
324+
done.append(vm)
325+
continue
326+
self.err += vm.name + " cannot shutdown: " + str(res) + "\n"
327+
self.log.error("Cannot shutdown %s because %s", vm.name, str(res))
328+
self.status = RestartStatus.ERROR_TMPL_DOWN
329+
return done
330330

331331
def restart_vms(self, to_restart):
332332
"""
@@ -335,14 +335,23 @@ def restart_vms(self, to_restart):
335335
shutdowns = self.shutdown_domains(to_restart)
336336

337337
# restart shutdown qubes
338-
for vm in shutdowns:
339-
try:
340-
vm.start()
338+
try:
339+
loop = asyncio.get_event_loop()
340+
except RuntimeError:
341+
# changes between GLib versions and python versions mean that the above
342+
# can fail on some dom0/gui domain configurations
343+
loop = asyncio.new_event_loop()
344+
tasks = [asyncio.to_thread(vm.start) for vm in shutdowns]
345+
results = loop.run_until_complete(
346+
asyncio.gather(*tasks, return_exceptions=True)
347+
)
348+
for vm, res in zip(shutdowns, results):
349+
if not isinstance(res, qubesadmin.exc.QubesVMError):
341350
self.log.info("Restart %s", vm.name)
342-
except qubesadmin.exc.QubesVMError as err:
343-
self.err += vm.name + " cannot start: " + str(err) + "\n"
344-
self.log.error("Cannot start %s because %s", vm.name, str(err))
345-
self.status = RestartStatus.ERROR_APP_DOWN
351+
continue
352+
self.err += vm.name + " cannot start: " + str(res) + "\n"
353+
self.log.error("Cannot start %s because %s", vm.name, str(res))
354+
self.status = RestartStatus.ERROR_APP_DOWN
346355

347356
def _show_status_dialog(self, show_only_error: bool):
348357
if self.status == RestartStatus.OK and not show_only_error:

0 commit comments

Comments
 (0)