Keep the server running if a reply fails
If a client disconnected before its reply, send_response raised OSError and the server ended. The request loop now logs the error and waits for the next client. An error from accept() still ends the server.
This commit is contained in:
@@ -476,6 +476,14 @@ def main():
|
|||||||
debug,
|
debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
if connection is None:
|
||||||
|
# accept() failed, so the listening socket is not usable.
|
||||||
|
raise
|
||||||
|
# A client that disconnects before its reply must not end the
|
||||||
|
# server for every other client.
|
||||||
|
debugger("socket ERROR: {}".format(e))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
if connection:
|
if connection:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|||||||
@@ -1006,6 +1006,52 @@ class TestServerMain:
|
|||||||
assert "invalid request from" in capsys.readouterr().out
|
assert "invalid request from" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_that_disconnects_before_the_reply_does_not_end_the_server(self, tmp_path, capsys):
|
||||||
|
"""The reply to a closed connection fails. The next client must still get an answer."""
|
||||||
|
sock_path = str(tmp_path / "server_gone.sock")
|
||||||
|
ready, client_closed = threading.Event(), threading.Event()
|
||||||
|
|
||||||
|
def send_after_close(*args, **kwargs):
|
||||||
|
# Hold the reply until the client is gone, so that the send fails each time.
|
||||||
|
client_closed.wait(timeout=5)
|
||||||
|
return send_response(*args, **kwargs)
|
||||||
|
|
||||||
|
t = self._make_server_thread(sock_path, ready, threading.Event())
|
||||||
|
with patch("bmspy.server.send_response", send_after_close):
|
||||||
|
t.start()
|
||||||
|
ready.wait(timeout=2)
|
||||||
|
try:
|
||||||
|
sock = socket_module.socket(socket_module.AF_UNIX, socket_module.SOCK_STREAM)
|
||||||
|
sock.connect(sock_path)
|
||||||
|
payload = json.dumps({"command": "GET", "client": "gone"}).encode()
|
||||||
|
sock.sendall(struct.pack("!I", len(payload)) + payload)
|
||||||
|
sock.close()
|
||||||
|
client_closed.set()
|
||||||
|
|
||||||
|
response = self._send_command(sock_path, {"command": "REGISTER", "client": "test"})
|
||||||
|
assert response == {"status": "REGISTERED", "client": "test"}
|
||||||
|
assert t.is_alive()
|
||||||
|
finally:
|
||||||
|
client_closed.set()
|
||||||
|
self._stop_server(sock_path, t)
|
||||||
|
assert "socket ERROR: unable to encode response" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_failed_accept_still_ends_the_server(self, tmp_path):
|
||||||
|
"""A listening socket that fails must not become an endless loop."""
|
||||||
|
sock_path = str(tmp_path / "server_accept.sock")
|
||||||
|
with patch("sys.argv", ["bmspy-server", "--socket", sock_path, "--device", "/dev/ttyUSB0"]), \
|
||||||
|
patch("bmspy.server.signal.signal"), \
|
||||||
|
patch("bmspy.server.atexit.register"), \
|
||||||
|
patch("bmspy.server.initialise_serial"), \
|
||||||
|
patch("bmspy.server.start_polling"), \
|
||||||
|
patch("bmspy.server.os.path.isdir", return_value=True), \
|
||||||
|
patch.object(socket_module.socket, "accept", side_effect=OSError("accept failed")) as accept:
|
||||||
|
from bmspy.server import main as server_main
|
||||||
|
with pytest.raises(OSError, match="accept failed"):
|
||||||
|
server_main()
|
||||||
|
assert accept.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
class TestSendResponseDebug:
|
class TestSendResponseDebug:
|
||||||
def test_debug_3_logs_sending(self, capsys):
|
def test_debug_3_logs_sending(self, capsys):
|
||||||
srv, cli = socket_module.socketpair()
|
srv, cli = socket_module.socketpair()
|
||||||
|
|||||||
Reference in New Issue
Block a user