@@ -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<pver>\d+(\.\d+)+)\.tar"
new file mode 100644
@@ -0,0 +1,92 @@
+From 120d060528d02e24b68ac06b44de34fb206b4319 Mon Sep 17 00:00:00 2001
+From: John Snow <jsnow@redhat.com>
+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 <jsnow@redhat.com>
+cherry picked from commit python-qemu-qmp@aa1ff9907603a3033296027e1bd021133df86ef1
+Signed-off-by: John Snow <jsnow@redhat.com>
+Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
+Upstream-Status: Backport [https://gitlab.com/qemu-project/qemu/-/commit/5d99044d09db0fa8c2b3294e301927118f9effc9]
+Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
+---
+ 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()
new file mode 100644
@@ -0,0 +1,199 @@
+From f25eb62190a6fa170db24584fe6225cd0dcd64ad Mon Sep 17 00:00:00 2001
+From: John Snow <jsnow@redhat.com>
+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 <rjones@redhat.com>
+Reported-by: Daniel P. Berrangé <berrange@redhat.com>
+Signed-off-by: John Snow <jsnow@redhat.com>
+cherry picked from commit python-qemu-qmp@21ce2ee4f2df87efe84a27b9c5112487f4670622
+cherry picked from commit python-qemu-qmp@c08fb82b38212956ccffc03fc6d015c3979f42fe
+Signed-off-by: John Snow <jsnow@redhat.com>
+Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
+Upstream-Status: Backport [https://gitlab.com/qemu-project/qemu/-/commit/85f223e5b031eb8ab63fbca314a4fb296a3a2632]
+Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
+---
+ 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.