diff mbox series

[scarthgap,55/66] qemu: backport patches to support python 3.14

Message ID 28bab00b35af8bbe3455c8266e4c792fa2367c5d.1777064068.git.yoann.congal@smile.fr
State New
Headers show
Series [scarthgap,01/66] spdx30_tasks: fix condition in create_spdx | expand

Commit Message

Yoann Congal April 24, 2026, 8:55 p.m. UTC
From: Yoann Congal <yoann.congal@smile.fr>

We use QEMU QMP python module to drive qemu in testimage. QMP uses
asyncIO and the method to get the event loop changed.

Backport the patches handling the depreciation to fix the error:
  ERROR: core-image-minimal-1.0-r0 do_testimage: Error executing a python function in exec_func_python() autogenerated:

  The stack trace of python calls that resulted in this exception/failure was:
  File: 'exec_func_python() autogenerated', lineno: 2, function: <module>
   *** 0002:do_testimage(d)
  ...
  File: '.../openembedded-core/meta/lib/oeqa/utils/qemurunner.py', lineno: 332, function: launch
       0331:                from qmp.legacy import QEMUMonitorProtocol
   *** 0332:                self.qmp = QEMUMonitorProtocol(os.path.basename(qmp_port))
  File: '.../build-ubuntu2604/tmp-glibc/work/qemux86_64-oe-linux/core-image-minimal/1.0/recipe-sysroot-native/usr/lib/qemu-python/qmp/legacy.py', lineno: 89, function: __init__
   *** 0089:        self._aloop = asyncio.get_event_loop()
  File: '/usr/lib/python3.14/asyncio/events.py', lineno: 715, function: get_event_loop
       0711:
       0712:        Returns an instance of EventLoop or raises an exception.
       0713:        """
       0714:        if self._local._loop is None:
   *** 0715:            raise RuntimeError('There is no current event loop in thread %r.'
       0716:                               % threading.current_thread().name)
       0717:
       0718:        return self._local._loop
  Exception: RuntimeError: There is no current event loop in thread 'MainThread'.

Both patches are in Qemu 10.2 (OE Core master version)

Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
---
 meta/recipes-devtools/qemu/qemu.inc           |   2 +
 ...Remove-deprecated-get_event_loop-cal.patch |  92 ++++++++
 ...avoid-creating-additional-event-loop.patch | 199 ++++++++++++++++++
 3 files changed, 293 insertions(+)
 create mode 100644 meta/recipes-devtools/qemu/qemu/0001-python-backport-Remove-deprecated-get_event_loop-cal.patch
 create mode 100644 meta/recipes-devtools/qemu/qemu/0002-python-backport-avoid-creating-additional-event-loop.patch
diff mbox series

Patch

diff --git a/meta/recipes-devtools/qemu/qemu.inc b/meta/recipes-devtools/qemu/qemu.inc
index 748a32215e0..54644dd9241 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<pver>\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 00000000000..7a564513c03
--- /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 <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()
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 00000000000..d893c10c420
--- /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 <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.