Skip to content

Commit 21f8a67

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 21f8a67

3 files changed

Lines changed: 74 additions & 55 deletions

File tree

qui/devices/actionable_widgets.py

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

3132
import qubesadmin
3233
import qubesadmin.devices
3334
import qubesadmin.vm
35+
from qubesadmin.utils import async_thread
3436

3537
import gi
3638

3739
gi.require_version("Gtk", "3.0") # isort:skip
3840
from gi.repository import Gtk, GdkPixbuf, GLib # isort:skip
3941

4042
from . import backend
41-
import time
4243

4344

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

249250
async def widget_action(self, *_args):
250-
self.device.attach_to_vm(self.vm)
251+
await async_thread(self.device.attach_to_vm, self.vm)
251252

252253

253254
class DetachWidget(ActionableWidget, SimpleActionWidget):
@@ -259,7 +260,7 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
259260
self.device = device
260261

261262
async def widget_action(self, *_args):
262-
self.device.detach_from_vm(self.vm, False)
263+
await async_thread(self.device.detach_from_vm, self.vm, False)
263264

264265

265266
class DetachWithWidget(ActionableWidget, SimpleActionWidget):
@@ -278,7 +279,7 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
278279
self.device = device
279280

280281
async def widget_action(self, *_args):
281-
self.device.detach_from_vm(self.vm, True)
282+
await async_thread(self.device.detach_from_vm, self.vm, True)
282283

283284

284285
class DetachAndShutdownWidget(ActionableWidget, SimpleActionWidget):
@@ -292,8 +293,8 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
292293
self.device = device
293294

294295
async def widget_action(self, *_args):
295-
self.device.detach_from_vm(self.vm, True)
296-
self.vm.vm_object.shutdown()
296+
await async_thread(self.device.detach_from_vm, self.vm, True)
297+
await async_thread(self.vm.vm_object.shutdown)
297298

298299

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

307308
async def widget_action(self, *_args):
308309
for vm in self.device.attachments:
309-
self.device.detach_from_vm(vm, True)
310-
self.device.attach_to_vm(self.vm)
310+
await async_thread(self.device.detach_from_vm, vm, True)
311+
await async_thread(self.device.attach_to_vm, self.vm)
311312

312313

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

321322
async def widget_action(self, *_args):
322323
new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm)
323-
new_dispvm.start()
324+
await async_thread(new_dispvm.start)
324325

325-
self.device.attach_to_vm(backend.VM(new_dispvm))
326+
await async_thread(self.device.attach_to_vm, backend.VM(new_dispvm))
326327

327328

328329
class DetachAndAttachDisposableWidget(ActionableWidget, VMWithIcon):
@@ -334,11 +335,11 @@ def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"
334335
self.device = device
335336

336337
async def widget_action(self, *_args):
337-
self.device.detach_from_vm(self.vm)
338+
await async_thread(self.device.detach_from_vm, self.vm)
338339
new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm)
339-
new_dispvm.start()
340+
await async_thread(new_dispvm.start)
340341

341-
self.device.attach_to_vm(backend.VM(new_dispvm))
342+
await async_thread(self.device.attach_to_vm, backend.VM(new_dispvm))
342343

343344

344345
class ToggleFeatureItem(ActionableWidget, SimpleActionWidget):
@@ -379,7 +380,7 @@ def __init__(self, usbvm: backend.VM, variant: str = "dark"):
379380
self.usbvm = usbvm
380381

381382
async def widget_action(self, *_args):
382-
self.usbvm.vm_object.start()
383+
await async_thread(self.usbvm.vm_object.start)
383384

384385

385386
#### Configuration-related actions

qui/tray/domains.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import qubesadmin
1919
import qubesadmin.events
2020
from qubesadmin import exc
21+
from qubesadmin.utils import async_thread
2122

2223
import qui.decorators
2324
import qui.utils
@@ -157,7 +158,7 @@ def __init__(self, vm, icon_cache):
157158

158159
async def perform_action(self):
159160
try:
160-
self.vm.pause()
161+
await async_thread(self.vm.pause)
161162
except exc.QubesException as ex:
162163
show_error(
163164
_("Error pausing qube"),
@@ -178,7 +179,7 @@ def __init__(self, vm, icon_cache):
178179

179180
async def perform_action(self):
180181
try:
181-
self.vm.unpause()
182+
await async_thread(self.vm.unpause)
182183
except exc.QubesException as ex:
183184
show_error(
184185
_("Error unpausing qube"),
@@ -215,7 +216,7 @@ def set_force(self, force):
215216

216217
async def perform_action(self):
217218
try:
218-
self.vm.shutdown(force=self.force)
219+
await async_thread(self.vm.shutdown, force=self.force, wait=True)
219220
except exc.QubesException as ex:
220221
if self.force:
221222
show_error(
@@ -271,19 +272,22 @@ async def perform_action(self):
271272
GLib.idle_add(dialog.show)
272273

273274
def react_to_question(self, widget, response, action):
275+
asyncio.create_task(self.react_to_question_async(widget, response, action))
276+
277+
async def react_to_question_async(self, widget, response, action):
274278
if response not in (Gtk.ResponseType.OK, Gtk.ResponseType.YES):
275279
widget.destroy()
276280
return
277281
try:
278282
if action == "force":
279-
self.vm.shutdown(force=True)
283+
await async_thread(self.vm.shutdown, force=True, wait=True)
280284
elif action == "timeout":
281285
if response == Gtk.ResponseType.YES:
282-
self.vm.kill()
286+
await async_thread(self.vm.kill)
283287
elif response == Gtk.ResponseType.OK:
284-
self.vm.shutdown(force=False)
288+
await async_thread(self.vm.shutdown, force=False, wait=True)
285289
elif action == "kill" and response == Gtk.ResponseType.OK:
286-
self.vm.kill()
290+
await async_thread(self.vm.kill)
287291
except exc.QubesException as ex:
288292
show_error(
289293
_("Error shutting down qube"),
@@ -320,7 +324,7 @@ def set_force(self, force):
320324

321325
async def perform_action(self, *_args, **_kwargs):
322326
try:
323-
self.vm.shutdown(force=self.force)
327+
await async_thread(self.vm.shutdown, force=self.force, wait=True)
324328
except exc.QubesException as ex:
325329
if self.force:
326330
# we already tried forcing it, let's just give up
@@ -351,12 +355,7 @@ async def perform_action(self, *_args, **_kwargs):
351355
if self.give_up:
352356
return
353357
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)
358+
await async_thread(self.vm.start)
360359
except exc.QubesException as ex:
361360
show_error(
362361
_("Error restarting qube"),
@@ -367,9 +366,12 @@ async def perform_action(self, *_args, **_kwargs):
367366
)
368367

369368
def react_to_question(self, widget, response):
369+
asyncio.create_task(self.react_to_question_async(widget, response))
370+
371+
async def react_to_question_async(self, widget, response):
370372
if response == Gtk.ResponseType.OK:
371373
try:
372-
self.vm.shutdown(force=True)
374+
await async_thread(self.vm.shutdown, force=True, wait=True)
373375
except exc.QubesException as ex:
374376
show_error(
375377
_("Error shutting down qube"),
@@ -392,13 +394,13 @@ def __init__(self, vm, icon_cache):
392394

393395
async def perform_action(self, *_args, **_kwargs):
394396
try:
395-
self.vm.kill()
397+
await async_thread(self.vm.kill)
396398
except exc.QubesException as ex:
397399
show_error(
398400
_("Error shutting down qube"),
399401
_(
400-
"The following error occurred while attempting to shut"
401-
"down qube {0}:\n{1}"
402+
"The following error occurred while attempting to kill"
403+
"qube {0}:\n{1}"
402404
).format(self.vm.name, str(ex)),
403405
)
404406

@@ -459,7 +461,11 @@ async def perform_action(self):
459461
if self.as_root:
460462
service_args["user"] = "root"
461463
try:
462-
self.vm.run_service("qubes.StartApp+qubes-run-terminal", **service_args)
464+
await async_thread(
465+
self.vm.run_service_for_stdio,
466+
"qubes.StartApp+qubes-run-terminal",
467+
**service_args,
468+
)
463469
except exc.QubesException as ex:
464470
show_error(
465471
_("Error starting terminal"),
@@ -508,7 +514,9 @@ def __init__(self, vm, icon_cache):
508514

509515
async def perform_action(self):
510516
try:
511-
self.vm.run_service("qubes.StartApp+qubes-open-file-manager")
517+
await async_thread(
518+
self.vm.run_service_for_stdio, "qubes.StartApp+qubes-open-file-manager"
519+
)
512520
except exc.QubesException as ex:
513521
show_error(
514522
_("Error opening file manager"),

qui/updater/summary_page.py

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

3333
import qubesadmin
34-
from qubesadmin.events.utils import wait_for_domain_shutdown
34+
from qubesadmin.utils import async_thread
3535

3636
from qubes_config.widgets.gtk_utils import (
3737
load_icon,
@@ -308,25 +308,26 @@ def shutdown_domains(self, to_shutdown):
308308
"""
309309
Try to shut down vms and wait to finish.
310310
"""
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
321311
try:
322312
loop = asyncio.get_event_loop()
323313
except RuntimeError:
324314
# changes between GLib versions and python versions mean that the above
325315
# can fail on some dom0/gui domain configurations
326316
loop = asyncio.new_event_loop()
327-
loop.run_until_complete(wait_for_domain_shutdown(wait_for))
328-
329-
return wait_for
317+
tasks = [async_thread(vm.shutdown, force=True, wait=True) for vm in to_shutdown]
318+
results = loop.run_until_complete(
319+
asyncio.gather(*tasks, return_exceptions=True)
320+
)
321+
done = []
322+
for vm, res in zip(to_shutdown, results):
323+
if not isinstance(res, qubesadmin.exc.QubesVMError):
324+
self.log.info("Shutdown %s", vm.name)
325+
done.append(vm)
326+
continue
327+
self.err += vm.name + " cannot shutdown: " + str(res) + "\n"
328+
self.log.error("Cannot shutdown %s because %s", vm.name, str(res))
329+
self.status = RestartStatus.ERROR_TMPL_DOWN
330+
return done
330331

331332
def restart_vms(self, to_restart):
332333
"""
@@ -335,14 +336,23 @@ def restart_vms(self, to_restart):
335336
shutdowns = self.shutdown_domains(to_restart)
336337

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

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

0 commit comments

Comments
 (0)