From ffc1a8213593f395d9d9a682bb85109486664efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Wed, 3 Jun 2026 17:14:45 +0200 Subject: [PATCH 1/2] Initialize asyncio event loop before using it Python 3.14 (in Fedora 43) throws RunetimeError if event loop is not initialized before using it. Note most uses of get_event_loop() (especially in tests) are okay, since event loop already exists at that point, even if it isn't running at that moment. There is also caveat about using asyncio.run() - it resets event loop at the end, which breaks any further use of asyncio. It's okay to use it in main function of a standalone tool, but it's not okay in utility function or tests. And finally, register libvirt event loop only after creating asyncio one. Fixes: QubesOS/qubes-issues#10188 Fixes: QubesOS/qubes-issues#10820 --- doc/qubes-events.rst | 6 +++--- qubes/backup.py | 2 +- qubes/tests/__init__.py | 17 +++++++++++------ qubes/tools/qubes_create.py | 2 +- qubes/tools/qubesd.py | 3 ++- qubes/tools/qubesd_query.py | 3 ++- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/doc/qubes-events.rst b/doc/qubes-events.rst index d341c2912..0e564d516 100644 --- a/doc/qubes-events.rst +++ b/doc/qubes-events.rst @@ -177,8 +177,7 @@ handler (a coroutine) for synchronous event (the one fired with o = MyClass() o.events_enabled = True - loop = asyncio.get_event_loop() - loop.run_until_complete(o.fire_event_async('event1')) + asyncio.run(o.fire_event_async('event1')) Asynchronous event handlers can also return value - but only a collection, not yield individual values (because of python limitation): @@ -207,7 +206,8 @@ yield individual values (because of python limitation): o = MyClass() o.events_enabled = True - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) # returns ['sync result', 'result1', 'result2', 'result3', 'result4'], # possibly not in order effects = loop.run_until_complete(o.fire_event_async('event1')) diff --git a/qubes/backup.py b/qubes/backup.py index c67a5c2ab..a80d1c586 100644 --- a/qubes/backup.py +++ b/qubes/backup.py @@ -257,7 +257,7 @@ class Backup: >>> } >>> backup_op = Backup(app, vms, exclude_vms, **options) >>> print(backup_op.get_backup_summary()) - >>> asyncio.get_event_loop().run_until_complete(backup_op.backup_do()) + >>> asyncio.run(backup_op.backup_do()) See attributes of this object for all available options. diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index 46c385dbe..524ba00d0 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -494,11 +494,6 @@ def __init__(self, methodName="runTest"): self._success = True - global libvirt_event_impl - - if in_dom0 and not libvirt_event_impl: - libvirt_event_impl = libvirtaio.virEventRegisterAsyncIOImpl() - def set_result(self, success): self._success = success @@ -513,7 +508,17 @@ def setUp(self): super().setUp() self.addCleanup(self.cleanup_gc) - self.loop = asyncio.get_event_loop() + try: + self.loop = asyncio.get_event_loop() + except RuntimeError: + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + global libvirt_event_impl + + if in_dom0 and not libvirt_event_impl: + libvirt_event_impl = libvirtaio.virEventRegisterAsyncIOImpl() + self.addCleanup(self.cleanup_loop) self.kernel_validator_original = qubes.app.validate_kernel diff --git a/qubes/tools/qubes_create.py b/qubes/tools/qubes_create.py index b3853a5b5..39e20b154 100644 --- a/qubes/tools/qubes_create.py +++ b/qubes/tools/qubes_create.py @@ -40,7 +40,7 @@ def main(args=None): """ args = parser.parse_args(args) - asyncio.get_event_loop().run_until_complete( + asyncio.run( qubes.Qubes.create_empty_store( args.app, offline_mode=args.offline_mode ).setup_pools() diff --git a/qubes/tools/qubesd.py b/qubes/tools/qubesd.py index 8ff3d5c09..eaa89b6f8 100644 --- a/qubes/tools/qubesd.py +++ b/qubes/tools/qubesd.py @@ -53,7 +53,8 @@ def sighandler(loop, signame, servers, app): def main(args=None): - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) libvirtaio.virEventRegisterAsyncIOImpl(loop=loop) try: args = parser.parse_args(args) diff --git a/qubes/tools/qubesd_query.py b/qubes/tools/qubesd_query.py index 9a5e3c3e0..cc4d416d1 100644 --- a/qubes/tools/qubesd_query.py +++ b/qubes/tools/qubesd_query.py @@ -112,7 +112,8 @@ async def qubesd_client(socket, payload, *args): # pylint: disable=too-many-return-statements def main(args=None): args = parser.parse_args(args) - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) max_payload_size = 1024 if args.single_line else MAX_PAYLOAD_SIZE if args.max_bytes is not None: From 125cb5ac7269f30713fc076d4e11e4ab6c609760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 30 May 2026 13:37:27 +0200 Subject: [PATCH 2/2] tests: cancel all remaining tasks between tests execution If old loop still has some tasks, bad things happen (mostly timeouts waiting for no longer existing objects). While theoretically all tasks should be completed by the time test ends, ensure that by cancelling them all (and logging a warning if there were any). --- qubes/tests/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index 524ba00d0..534bd31bb 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -598,6 +598,27 @@ def cleanup_loop(self): self.loop.stop() self.loop.run_forever() + # and finally, cancel remaining tasks + to_cancel = asyncio.all_tasks(self.loop) + if to_cancel: + for task in to_cancel: + self.log.warning("Leftover task: %r", task) + task.cancel() + + self.loop.run_until_complete( + asyncio.gather(*to_cancel, return_exceptions=True) + ) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + self.log.warning( + "Unhandled exception during test %r shutdown: %r", + task, + task.exception(), + ) + # Check there are no Tasks left. assert not self.loop._ready assert not self.loop._scheduled