Disconnect from Bluetooth devices before the server stops
The polling threads are daemon threads, so a stop ended the server in the middle of a Bluetooth read. The read got no disconnect, and the adapter kept an open connection or a scan in progress. The server now sets the stop event at exit, closes its socket, and waits at most 20 seconds for the polling threads. A Bluetooth read that sees the stop event cancels its current step, disconnects, and returns. A read that waits for the adapter does not connect after a stop.
This commit is contained in:
+61
-2
@@ -25,6 +25,7 @@ def _poll_once(devices, stop, debug=0):
|
||||
for device in devices.values():
|
||||
device.data = collect_device_data(device, debug)
|
||||
device.polled.set()
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -217,6 +218,39 @@ listener.accept()
|
||||
assert result.stderr.strip() == "terminating"
|
||||
assert not socket_path.exists()
|
||||
|
||||
def test_sigterm_during_a_bluetooth_read_waits_for_the_disconnect(self, tmp_path):
|
||||
"""The server process itself, with a stand-in for the Bluetooth read."""
|
||||
socket_path = tmp_path / "bms.sock"
|
||||
script = """
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from bmspy import server
|
||||
|
||||
def bluetooth_read(config, debug=0, stop=None):
|
||||
if stop.is_set():
|
||||
return False
|
||||
print("read started", flush=True)
|
||||
threading.Timer(0.1, os.kill, args=(os.getpid(), signal.SIGTERM)).start()
|
||||
stop.wait(10)
|
||||
print("disconnected", flush=True)
|
||||
return False
|
||||
|
||||
server.ecoflow_collect_data = bluetooth_read
|
||||
sys.argv = ["bmspy-server", "--socket", sys.argv[1],
|
||||
"-d", "river@02:00:00:00:00:01/R601TEST00000001/test-user"]
|
||||
server.main()
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script, str(socket_path)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
assert result.stdout.splitlines() == ["read started", "disconnected"]
|
||||
assert result.returncode == 1
|
||||
assert result.stderr.strip() == "terminating"
|
||||
assert not socket_path.exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# socket_cleanup
|
||||
@@ -765,7 +799,7 @@ class TestServerMain:
|
||||
patch("bmspy.server.signal.signal"), \
|
||||
patch("bmspy.server.atexit.register"), \
|
||||
patch("bmspy.server.initialise_serial"), \
|
||||
patch("bmspy.server.start_polling"), \
|
||||
patch("bmspy.server.start_polling", return_value=[]), \
|
||||
patch("bmspy.server.os.path.isdir", return_value=True), \
|
||||
patch.object(socket_module.socket, "accept", side_effect=KeyboardInterrupt):
|
||||
from bmspy.server import main as server_main
|
||||
@@ -773,6 +807,31 @@ class TestServerMain:
|
||||
server_main()
|
||||
assert exit_info.value.code == 1
|
||||
|
||||
def test_exit_closes_the_socket_and_stops_polling(self, tmp_path):
|
||||
sock_path = str(tmp_path / "server_stop.sock")
|
||||
threads = [object()]
|
||||
listeners = []
|
||||
original_listen = socket_module.socket.listen
|
||||
|
||||
def listen(self, backlog=1):
|
||||
listeners.append(self)
|
||||
return original_listen(self, backlog)
|
||||
|
||||
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", return_value=threads) as start, \
|
||||
patch("bmspy.server.stop_polling") as stop_polling, \
|
||||
patch("bmspy.server.os.path.isdir", return_value=True), \
|
||||
patch.object(socket_module.socket, "listen", listen), \
|
||||
patch.object(socket_module.socket, "accept", side_effect=KeyboardInterrupt):
|
||||
from bmspy.server import main as server_main
|
||||
with pytest.raises(SystemExit):
|
||||
server_main()
|
||||
stop_polling.assert_called_once_with(threads, start.call_args.args[1])
|
||||
assert listeners[0].fileno() == -1
|
||||
|
||||
def test_socket_read_error_logs_and_continues(self, tmp_path, capsys):
|
||||
"""Test that read_request errors are caught and logged."""
|
||||
sock_path = str(tmp_path / "server_err.sock")
|
||||
@@ -1098,7 +1157,7 @@ class TestServerMain:
|
||||
patch("bmspy.server.signal.signal"), \
|
||||
patch("bmspy.server.atexit.register"), \
|
||||
patch("bmspy.server.initialise_serial"), \
|
||||
patch("bmspy.server.start_polling"), \
|
||||
patch("bmspy.server.start_polling", return_value=[]), \
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user