From 0ba4e76b23fed77d09be7f56da783ab3f0b2d497 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 25 Feb 2022 15:59:40 -0500 Subject: python/aqmp: rename 'accept()' to 'start_server_and_accept()' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, I had a method named "accept()" that under-the-hood calls bind(2), listen(2) *and* accept(2). I meant this as a simplification and counterpart to the one-shot "connect()" method. This is confusing to readers who expect accept() to mean *just* accept(2). Since I need to split apart the "accept()" method into multiple methods anyway (one of which strongly resembling accept(2)), it feels pertinent to rename this method *now*. Rename this all-in-one method "start_server_and_accept()" instead. Signed-off-by: John Snow Acked-by: Kevin Wolf Reviewed-by: Daniel P. Berrangé Message-id: 20220225205948.3693480-3-jsnow@redhat.com Signed-off-by: John Snow --- python/qemu/aqmp/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'python/qemu/aqmp/legacy.py') diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py index 6baa5f3409..dca1e76ed4 100644 --- a/python/qemu/aqmp/legacy.py +++ b/python/qemu/aqmp/legacy.py @@ -91,7 +91,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): self._aqmp.negotiate = True self._sync( - self._aqmp.accept(self._address), + self._aqmp.start_server_and_accept(self._address), timeout ) -- cgit 1.4.1 From 673856f9d889dc50b6a1a7964df960c4f00c7c93 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 25 Feb 2022 15:59:47 -0500 Subject: python/aqmp: fix race condition in legacy.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit legacy.py provides a synchronous model. iotests frequently uses this paradigm: - create QMP client object - start QEMU process - await connection from QEMU process In the switch from sync to async QMP, the QMP client object stopped calling bind() and listen() during the QMP object creation step, which creates a race condition if the QEMU process dials in too quickly. With refactoring out of the way, restore the former behavior of calling bind() and listen() during __init__() to fix this race condition. Signed-off-by: John Snow Acked-by: Kevin Wolf Reviewed-by: Daniel P. Berrangé Message-id: 20220225205948.3693480-10-jsnow@redhat.com [Expanded commit message. --js] Signed-off-by: John Snow --- python/qemu/aqmp/legacy.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'python/qemu/aqmp/legacy.py') diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py index dca1e76ed4..cb50e60564 100644 --- a/python/qemu/aqmp/legacy.py +++ b/python/qemu/aqmp/legacy.py @@ -57,7 +57,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): self._timeout: Optional[float] = None if server: - self._aqmp._bind_hack(address) # pylint: disable=protected-access + self._sync(self._aqmp.start_server(address)) _T = TypeVar('_T') @@ -90,10 +90,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): self._aqmp.await_greeting = True self._aqmp.negotiate = True - self._sync( - self._aqmp.start_server_and_accept(self._address), - timeout - ) + self._sync(self._aqmp.accept(), timeout) ret = self._get_greeting() assert ret is not None -- cgit 1.4.1 From 4c1fe7003c9b373acb0791b4356e2285a10365c0 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 25 Feb 2022 15:59:48 -0500 Subject: python/aqmp: drop _bind_hack() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _bind_hack() was a quick fix to allow async QMP to call bind(2) prior to calling listen(2) and accept(2). This wasn't sufficient to fully address the race condition present in synchronous clients. With the race condition in legacy.py fixed (see the previous commit), there are no longer any users of _bind_hack(). Drop it. Fixes: b0b662bb2b3 Signed-off-by: John Snow Acked-by: Kevin Wolf Reviewed-by: Daniel P. Berrangé Message-id: 20220225205948.3693480-11-jsnow@redhat.com [Expanded commit message. --js] Signed-off-by: John Snow --- python/qemu/aqmp/legacy.py | 2 +- python/qemu/aqmp/protocol.py | 41 +++-------------------------------------- 2 files changed, 4 insertions(+), 39 deletions(-) (limited to 'python/qemu/aqmp/legacy.py') diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py index cb50e60564..46026e9fdc 100644 --- a/python/qemu/aqmp/legacy.py +++ b/python/qemu/aqmp/legacy.py @@ -57,7 +57,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): self._timeout: Optional[float] = None if server: - self._sync(self._aqmp.start_server(address)) + self._sync(self._aqmp.start_server(self._address)) _T = TypeVar('_T') diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py index 2ecba14555..36fae57f27 100644 --- a/python/qemu/aqmp/protocol.py +++ b/python/qemu/aqmp/protocol.py @@ -18,7 +18,6 @@ from asyncio import StreamReader, StreamWriter from enum import Enum from functools import wraps import logging -import socket from ssl import SSLContext from typing import ( Any, @@ -242,9 +241,6 @@ class AsyncProtocol(Generic[T]): self._runstate = Runstate.IDLE self._runstate_changed: Optional[asyncio.Event] = None - # Workaround for bind() - self._sock: Optional[socket.socket] = None - # Server state for start_server() and _incoming() self._server: Optional[asyncio.AbstractServer] = None self._accepted: Optional[asyncio.Event] = None @@ -535,34 +531,6 @@ class AsyncProtocol(Generic[T]): self._reader, self._writer = (reader, writer) self._accepted.set() - def _bind_hack(self, address: Union[str, Tuple[str, int]]) -> None: - """ - Used to create a socket in advance of accept(). - - This is a workaround to ensure that we can guarantee timing of - precisely when a socket exists to avoid a connection attempt - bouncing off of nothing. - - Python 3.7+ adds a feature to separate the server creation and - listening phases instead, and should be used instead of this - hack. - """ - if isinstance(address, tuple): - family = socket.AF_INET - else: - family = socket.AF_UNIX - - sock = socket.socket(family, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - try: - sock.bind(address) - except: - sock.close() - raise - - self._sock = sock - @upper_half async def _do_start_server(self, address: SocketAddrT, ssl: Optional[SSLContext] = None) -> None: @@ -589,21 +557,19 @@ class AsyncProtocol(Generic[T]): if isinstance(address, tuple): coro = asyncio.start_server( self._incoming, - host=None if self._sock else address[0], - port=None if self._sock else address[1], + host=address[0], + port=address[1], ssl=ssl, backlog=1, limit=self._limit, - sock=self._sock, ) else: coro = asyncio.start_unix_server( self._incoming, - path=None if self._sock else address, + path=address, ssl=ssl, backlog=1, limit=self._limit, - sock=self._sock, ) # Allow runstate watchers to witness 'CONNECTING' state; some @@ -630,7 +596,6 @@ class AsyncProtocol(Generic[T]): await self._accepted.wait() assert self._server is None self._accepted = None - self._sock = None self.logger.debug("Connection accepted.") -- cgit 1.4.1