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.
Reported-by: Richard W.M. Jones <rjo...@redhat.com> Reported-by: Daniel P. Berrangé <berra...@redhat.com> Signed-off-by: John Snow <js...@redhat.com> cherry picked from commit 21ce2ee4f2df87efe84a27b9c5112487f4670622 Signed-off-by: John Snow <js...@redhat.com> --- python/qemu/qmp/legacy.py | 47 +++++++++++++++++++++++++++----------- python/qemu/qmp/qmp_tui.py | 10 ++++++-- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/python/qemu/qmp/legacy.py b/python/qemu/qmp/legacy.py index 735d42971e9..775b1fdd3b3 100644 --- a/python/qemu/qmp/legacy.py +++ b/python/qemu/qmp/legacy.py @@ -86,13 +86,15 @@ def __init__(self, "server argument should be False when passing a socket") self._qmp = QMPClient(nickname) + self._created_loop = False 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() + # No running loop; since this is a sync shim likely to be used + # in sync programs without any event loop at all, create one. + self._aloop = asyncio.new_event_loop() + self._created_loop = True self._address = address self._timeout: Optional[float] = None @@ -313,17 +315,36 @@ 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 we created our own loop (and we are not running inside + # of it), we must close it to avoid warnings and error + # messages upon program exit. + if self._created_loop: + self._aloop.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 12bdc17c99e..d5526338f22 100644 --- a/python/qemu/qmp/qmp_tui.py +++ b/python/qemu/qmp/qmp_tui.py @@ -161,6 +161,7 @@ def __init__(self, address: Union[str, Tuple[str, int]], num_retries: int, self.retry_delay = retry_delay if retry_delay else 2 self.retry: bool = False self.exiting: bool = False + self._created_loop = False super().__init__() def add_to_history(self, msg: str, level: Optional[str] = None) -> None: @@ -391,8 +392,9 @@ def run(self, debug: bool = False) -> None: 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() + # No running asyncio event loop. Create one. + self.aloop = asyncio.new_event_loop() + self._created_loop = True self.aloop.set_debug(debug) @@ -416,6 +418,10 @@ def run(self, debug: bool = False) -> None: logging.error('%s\n%s\n', str(err), pretty_traceback()) raise err + def __del__(self) -> None: + if self._created_loop and self.aloop: + self.aloop.close() + class StatusBar(urwid.Text): """ -- 2.50.1