fix(meshcore): fix thread safety in _set_state/connect, add missing tests

- Lock-protect `get_state` and `_set_state` to prevent data race
  between Flask and asyncio daemon threads
- Atomically check-and-set CONNECTING guard in `connect()` to close
  TOCTOU window between concurrent Flask threads
- Push status events outside the lock in both `_set_state` and
  `connect()` to avoid potential deadlock
- Add TestMeshcoreContact, TestMeshcoreClientStateMachine tests
  covering to_dict keys, queue push on state change, message append
  and 500-item cap (9 -> 13 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-05-11 11:12:11 +01:00
parent 6807ee6878
commit 80bbdb2c09
2 changed files with 92 additions and 8 deletions
+21 -8
View File
@@ -263,10 +263,13 @@ class MeshcoreClient:
def get_state(self) -> ConnectionState:
"""Return the current connection state."""
return self._state
with self._lock:
return self._state
def _set_state(self, state: ConnectionState, **extra) -> None:
self._state = state
with self._lock:
self._state = state
# Push the status event OUTSIDE the lock (avoids deadlock; _push is queue-based)
payload: dict = {"state": state.value}
payload.update(extra)
self._push({"type": "status", "data": payload})
@@ -291,16 +294,26 @@ class MeshcoreClient:
def connect(self, config: ConnectionConfig) -> None:
"""Start background AsyncWorker with the given connection config."""
if self._state == ConnectionState.CONNECTING:
return
with self._lock:
if self._state == ConnectionState.CONNECTING:
return
self._state = ConnectionState.CONNECTING
# Push status event outside the lock
self._push({"type": "status", "data": {"state": ConnectionState.CONNECTING.value}})
if isinstance(config, BLEConfig) and _is_docker():
self._set_state(
ConnectionState.ERROR,
message="BLE unavailable in Docker. Run meshcore-proxy on the host and connect via TCP.",
with self._lock:
self._state = ConnectionState.ERROR
self._push(
{
"type": "status",
"data": {
"state": ConnectionState.ERROR.value,
"message": "BLE unavailable in Docker. Run meshcore-proxy on the host and connect via TCP.",
},
}
)
return
self._config = config
self._set_state(ConnectionState.CONNECTING)
from utils.meshcore_client import AsyncWorker # imported lazily (Task 3)
self._worker = AsyncWorker(config, self)