diff --git a/meta/recipes-devtools/qemu/qemu.inc b/meta/recipes-devtools/qemu/qemu.inc index 748a32215e..54644dd924 100644 --- a/meta/recipes-devtools/qemu/qemu.inc +++ b/meta/recipes-devtools/qemu/qemu.inc @@ -43,6 +43,8 @@ SRC_URI = "https://download.qemu.org/${BPN}-${PV}.tar.xz \ file://qemu-guest-agent.udev \ file://CVE-2024-8354.patch \ file://CVE-2025-12464.patch \ + file://0001-python-backport-Remove-deprecated-get_event_loop-cal.patch \ + file://0002-python-backport-avoid-creating-additional-event-loop.patch \ " UPSTREAM_CHECK_REGEX = "qemu-(?P\d+(\.\d+)+)\.tar" diff --git a/meta/recipes-devtools/qemu/qemu/0001-python-backport-Remove-deprecated-get_event_loop-cal.patch b/meta/recipes-devtools/qemu/qemu/0001-python-backport-Remove-deprecated-get_event_loop-cal.patch new file mode 100644 index 0000000000..7a564513c0 --- /dev/null +++ b/meta/recipes-devtools/qemu/qemu/0001-python-backport-Remove-deprecated-get_event_loop-cal.patch @@ -0,0 +1,92 @@ +From 120d060528d02e24b68ac06b44de34fb206b4319 Mon Sep 17 00:00:00 2001 +From: John Snow +Date: Tue, 13 Aug 2024 09:35:30 -0400 +Subject: [PATCH] python: backport 'Remove deprecated get_event_loop calls' +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This method was deprecated in 3.12 because it ordinarily should not be +used from coroutines; if there is not a currently running event loop, +this automatically creates a new event loop - which is usually not what +you want from code that would ever run in the bottom half. + +In our case, we do want this behavior in two places: + +(1) The synchronous shim, for convenience: this allows fully sync +programs to use QEMUMonitorProtocol() without needing to set up an event +loop beforehand. This is intentional to fully box in the async +complexities into the legacy sync shim. + +(2) The qmp_tui shell; instead of relying on asyncio.run to create and +run an asyncio program, we need to be able to pass the current asyncio +loop to urwid setup functions. For convenience, again, we create one if +one is not present to simplify the creation of the TUI appliance. + +The remaining user of get_event_loop() was in fact one of the erroneous +users that should not have been using this function: if there's no +running event loop inside of a coroutine, you're in big trouble :) + +Signed-off-by: John Snow +cherry picked from commit python-qemu-qmp@aa1ff9907603a3033296027e1bd021133df86ef1 +Signed-off-by: John Snow +Reviewed-by: Daniel P. Berrangé +Upstream-Status: Backport [https://gitlab.com/qemu-project/qemu/-/commit/5d99044d09db0fa8c2b3294e301927118f9effc9] +Signed-off-by: Yoann Congal +--- + python/qemu/qmp/legacy.py | 9 ++++++++- + python/qemu/qmp/qmp_tui.py | 7 ++++++- + python/tests/protocol.py | 2 +- + 3 files changed, 15 insertions(+), 3 deletions(-) + +diff --git a/python/qemu/qmp/legacy.py b/python/qemu/qmp/legacy.py +index 22a2b5616ef..ea9b8032c3b 100644 +--- a/python/qemu/qmp/legacy.py ++++ b/python/qemu/qmp/legacy.py +@@ -86,7 +86,14 @@ def __init__(self, + "server argument should be False when passing a socket") + + self._qmp = QMPClient(nickname) +- self._aloop = asyncio.get_event_loop() ++ ++ try: ++ self._aloop = asyncio.get_running_loop() ++ except RuntimeError: ++ # No running loop; since this is a sync shim likely to be ++ # used in fully sync programs, create one if neccessary. ++ self._aloop = asyncio.get_event_loop_policy().get_event_loop() ++ + self._address = address + self._timeout: Optional[float] = None + +diff --git a/python/qemu/qmp/qmp_tui.py b/python/qemu/qmp/qmp_tui.py +index 2d9ebbd20bc..d11b9fc547b 100644 +--- a/python/qemu/qmp/qmp_tui.py ++++ b/python/qemu/qmp/qmp_tui.py +@@ -377,7 +377,12 @@ def run(self, debug: bool = False) -> None: + screen = urwid.raw_display.Screen() + screen.set_terminal_properties(256) + +- self.aloop = asyncio.get_event_loop() ++ try: ++ self.aloop = asyncio.get_running_loop() ++ except RuntimeError: ++ # No running asyncio event loop. Create one if necessary. ++ self.aloop = asyncio.get_event_loop_policy().get_event_loop() ++ + self.aloop.set_debug(debug) + + # Gracefully handle SIGTERM and SIGINT signals +diff --git a/python/tests/protocol.py b/python/tests/protocol.py +index 56c4d441f9c..8dcef573b6c 100644 +--- a/python/tests/protocol.py ++++ b/python/tests/protocol.py +@@ -228,7 +228,7 @@ def async_test(async_test_method): + Decorator; adds SetUp and TearDown to async tests. + """ + async def _wrapper(self, *args, **kwargs): +- loop = asyncio.get_event_loop() ++ loop = asyncio.get_running_loop() + loop.set_debug(True) + + await self._asyncSetUp() diff --git a/meta/recipes-devtools/qemu/qemu/0002-python-backport-avoid-creating-additional-event-loop.patch b/meta/recipes-devtools/qemu/qemu/0002-python-backport-avoid-creating-additional-event-loop.patch new file mode 100644 index 0000000000..d893c10c42 --- /dev/null +++ b/meta/recipes-devtools/qemu/qemu/0002-python-backport-avoid-creating-additional-event-loop.patch @@ -0,0 +1,199 @@ +From f25eb62190a6fa170db24584fe6225cd0dcd64ad Mon Sep 17 00:00:00 2001 +From: John Snow +Date: Wed, 3 Sep 2025 01:06:30 -0400 +Subject: [PATCH] python: backport 'avoid creating additional event loops per + thread' +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This commit is two backports squashed into one to avoid regressions. + +python: *really* remove get_event_loop + +A prior commit, aa1ff990, switched away from using get_event_loop *by +default*, but this is not good enough to avoid deprecation warnings as +`asyncio.get_event_loop_policy().get_event_loop()` is *also* +deprecated. Replace this mechanism with explicit calls to +asyncio.get_new_loop() and revise the cleanup mechanisms in __del__ to +match. + +python: avoid creating additional event loops per thread + +"Too hasty by far!", commit 21ce2ee4 attempted to avoid deprecated +behavior altogether by calling new_event_loop() directly if there was no +loop currently running, but this has the unfortunate side effect of +potentially creating multiple event loops per thread if tests +instantiate multiple QMP connections in a single thread. This behavior +is apparently not well-defined and causes problems in some, but not all, +combinations of Python interpreter version and platform environment. + +Partially revert to Daniel Berrange's original patch, which calls +get_event_loop and simply suppresses the deprecation warning in +Python<=3.13. This time, however, additionally register new loops +created with new_event_loop() so that future calls to get_event_loop() +will return the loop already created. + +Reported-by: Richard W.M. Jones +Reported-by: Daniel P. Berrangé +Signed-off-by: John Snow +cherry picked from commit python-qemu-qmp@21ce2ee4f2df87efe84a27b9c5112487f4670622 +cherry picked from commit python-qemu-qmp@c08fb82b38212956ccffc03fc6d015c3979f42fe +Signed-off-by: John Snow +Reviewed-by: Daniel P. Berrangé +Upstream-Status: Backport [https://gitlab.com/qemu-project/qemu/-/commit/85f223e5b031eb8ab63fbca314a4fb296a3a2632] +Signed-off-by: Yoann Congal +--- + python/qemu/qmp/legacy.py | 46 +++++++++++++++++++++++--------------- + python/qemu/qmp/qmp_tui.py | 10 ++------- + python/qemu/qmp/util.py | 27 ++++++++++++++++++++++ + 3 files changed, 57 insertions(+), 26 deletions(-) + +diff --git a/python/qemu/qmp/legacy.py b/python/qemu/qmp/legacy.py +index ea9b8032c3b..c732212c048 100644 +--- a/python/qemu/qmp/legacy.py ++++ b/python/qemu/qmp/legacy.py +@@ -38,6 +38,7 @@ + from .error import QMPError + from .protocol import Runstate, SocketAddrT + from .qmp_client import QMPClient ++from .util import get_or_create_event_loop + + + #: QMPMessage is an entire QMP message of any kind. +@@ -86,17 +87,13 @@ def __init__(self, + "server argument should be False when passing a socket") + + self._qmp = QMPClient(nickname) +- +- try: +- self._aloop = asyncio.get_running_loop() +- except RuntimeError: +- # No running loop; since this is a sync shim likely to be +- # used in fully sync programs, create one if neccessary. +- self._aloop = asyncio.get_event_loop_policy().get_event_loop() +- + self._address = address + self._timeout: Optional[float] = None + ++ # This is a sync shim intended for use in fully synchronous ++ # programs. Create and set an event loop if necessary. ++ self._aloop = get_or_create_event_loop() ++ + if server: + assert not isinstance(self._address, socket.socket) + self._sync(self._qmp.start_server(self._address)) +@@ -310,17 +307,30 @@ def send_fd_scm(self, fd: int) -> None: + self._qmp.send_fd_scm(fd) + + def __del__(self) -> None: +- if self._qmp.runstate == Runstate.IDLE: +- return ++ if self._qmp.runstate != Runstate.IDLE: ++ self._qmp.logger.warning( ++ "QEMUMonitorProtocol object garbage collected without a prior " ++ "call to close()" ++ ) + + if not self._aloop.is_running(): +- self.close() +- else: +- # Garbage collection ran while the event loop was running. +- # Nothing we can do about it now, but if we don't raise our +- # own error, the user will be treated to a lot of traceback +- # they might not understand. ++ if self._qmp.runstate != Runstate.IDLE: ++ # If the user neglected to close the QMP session and we ++ # are not currently running in an asyncio context, we ++ # have the opportunity to close the QMP session. If we ++ # do not do this, the error messages presented over ++ # dangling async resources may not make any sense to the ++ # user. ++ self.close() ++ ++ if self._qmp.runstate != Runstate.IDLE: ++ # If QMP is still not quiesced, it means that the garbage ++ # collector ran from a context within the event loop and we ++ # are simply too late to take any corrective action. Raise ++ # our own error to give meaningful feedback to the user in ++ # order to prevent pages of asyncio stacktrace jargon. + raise QMPError( +- "QEMUMonitorProtocol.close()" +- " was not called before object was garbage collected" ++ "QEMUMonitorProtocol.close() was not called before object was " ++ "garbage collected, and could not be closed due to GC running " ++ "in the event loop" + ) +diff --git a/python/qemu/qmp/qmp_tui.py b/python/qemu/qmp/qmp_tui.py +index d11b9fc547b..76e540931c7 100644 +--- a/python/qemu/qmp/qmp_tui.py ++++ b/python/qemu/qmp/qmp_tui.py +@@ -40,7 +40,7 @@ + from .message import DeserializationError, Message, UnexpectedTypeError + from .protocol import ConnectError, Runstate + from .qmp_client import ExecInterruptedError, QMPClient +-from .util import create_task, pretty_traceback ++from .util import get_or_create_event_loop, create_task, pretty_traceback + + + # The name of the signal that is used to update the history list +@@ -376,13 +376,7 @@ def run(self, debug: bool = False) -> None: + """ + screen = urwid.raw_display.Screen() + screen.set_terminal_properties(256) +- +- try: +- self.aloop = asyncio.get_running_loop() +- except RuntimeError: +- # No running asyncio event loop. Create one if necessary. +- self.aloop = asyncio.get_event_loop_policy().get_event_loop() +- ++ self.aloop = get_or_create_event_loop() + self.aloop.set_debug(debug) + + # Gracefully handle SIGTERM and SIGINT signals +diff --git a/python/qemu/qmp/util.py b/python/qemu/qmp/util.py +index ca6225e9cda..213f09c6528 100644 +--- a/python/qemu/qmp/util.py ++++ b/python/qemu/qmp/util.py +@@ -20,6 +20,7 @@ + TypeVar, + cast, + ) ++import warnings + + + T = TypeVar('T') +@@ -30,6 +31,32 @@ + # -------------------------- + + ++def get_or_create_event_loop() -> asyncio.AbstractEventLoop: ++ """ ++ Return this thread's current event loop, or create a new one. ++ ++ This function behaves similarly to asyncio.get_event_loop() in ++ Python<=3.13, where if there is no event loop currently associated ++ with the current context, it will create and register one. It should ++ generally not be used in any asyncio-native applications. ++ """ ++ try: ++ with warnings.catch_warnings(): ++ # Python <= 3.13 will trigger deprecation warnings if no ++ # event loop is set, but will create and set a new loop. ++ warnings.simplefilter("ignore") ++ loop = asyncio.get_event_loop() ++ except RuntimeError: ++ # Python 3.14+: No event loop set for this thread, ++ # create and set one. ++ loop = asyncio.new_event_loop() ++ # Set this loop as the current thread's loop, to be returned ++ # by calls to get_event_loop() in the future. ++ asyncio.set_event_loop(loop) ++ ++ return loop ++ ++ + async def flush(writer: asyncio.StreamWriter) -> None: + """ + Utility function to ensure a StreamWriter is *fully* drained.