diff --git a/bmspy/server.py b/bmspy/server.py index 42ca4ff..b2c0242 100755 --- a/bmspy/server.py +++ b/bmspy/server.py @@ -476,6 +476,14 @@ def main(): 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: if connection: connection.close() diff --git a/tests/test_server.py b/tests/test_server.py index 421be6f..f7443c5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1006,6 +1006,52 @@ class TestServerMain: 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: def test_debug_3_logs_sending(self, capsys): srv, cli = socket_module.socketpair()