Handle unavailable Python Bluetooth socket connections

This commit is contained in:
firestar5683
2026-10-23 19:04:37 -05:00
parent 2a7ebf190c
commit 50fc142dd7
3 changed files with 77 additions and 2 deletions
@@ -88,7 +88,11 @@ def _libc_connect(proto: int, kind: int, address: str, port: int, timeout: float
def _connect(proto: int, kind: int, address: str, port: int, timeout: float) -> socket.socket:
address = normalize_address(address)
if hasattr(socket, "AF_BLUETOOTH") and hasattr(socket, "BTPROTO_RFCOMM"):
return _native(proto, kind, address, port, timeout)
try:
return _native(proto, kind, address, port, timeout)
except OSError as exc:
if exc.errno != errno.ENOSYS:
raise
return _libc_connect(proto, kind, address, port, timeout)
@@ -0,0 +1,71 @@
import errno
import socket
import unittest
from unittest.mock import Mock, patch
from openpilot.starpilot.system.android_auto import bt_sockets
class TestBluetoothSocketFallback(unittest.TestCase):
ADDRESS = '50:5A:65:8B:8B:82'
def routes(self):
return ((bt_sockets.connect_rfcomm, 1), (bt_sockets.connect_l2cap, 1))
def capabilities(self):
return patch.multiple(socket, AF_BLUETOOTH=31, BTPROTO_RFCOMM=3, create=True)
def test_native_success_does_not_retry(self):
with self.capabilities(), patch.object(bt_sockets, '_native') as native, patch.object(bt_sockets, '_libc_connect') as libc:
for connect, port in self.routes():
self.assertIs(connect(self.ADDRESS, port, 4), native.return_value)
self.assertEqual(native.call_count, 2)
libc.assert_not_called()
def test_unimplemented_connect_uses_same_protocol_and_timeout(self):
with self.capabilities(), patch.object(bt_sockets, '_native', side_effect=OSError(errno.ENOSYS, 'Function not implemented')), \
patch.object(bt_sockets, '_libc_connect') as libc:
for connect, port in self.routes():
self.assertIs(connect(self.ADDRESS.lower(), port, 4), libc.return_value)
self.assertEqual(libc.call_args_list[0].args, (3, socket.SOCK_STREAM, self.ADDRESS, 1, 4))
self.assertEqual(libc.call_args_list[1].args, (0, socket.SOCK_SEQPACKET, self.ADDRESS, 1, 4))
def test_real_connection_failures_do_not_retry(self):
for code in (errno.EHOSTDOWN, errno.ECONNREFUSED, errno.ETIMEDOUT, errno.EPERM, errno.EAFNOSUPPORT):
with self.subTest(errno=code), self.capabilities(), \
patch.object(bt_sockets, '_native', side_effect=OSError(code, 'connection failed')), \
patch.object(bt_sockets, '_libc_connect') as libc:
for connect, port in self.routes():
with self.assertRaises(OSError) as caught:
connect(self.ADDRESS, port)
self.assertEqual(caught.exception.errno, code)
libc.assert_not_called()
def test_fallback_failure_propagates_without_loop(self):
with self.capabilities(), patch.object(bt_sockets, '_native', side_effect=OSError(errno.ENOSYS, 'unimplemented')) as native, \
patch.object(bt_sockets, '_libc_connect', side_effect=OSError(errno.EHOSTDOWN, 'host down')) as libc:
with self.assertRaises(OSError) as caught:
bt_sockets.connect_rfcomm(self.ADDRESS, 1)
self.assertEqual(caught.exception.errno, errno.EHOSTDOWN)
native.assert_called_once()
libc.assert_called_once()
def test_native_failure_closes_original_descriptor(self):
original = Mock()
original.connect.side_effect = OSError(errno.ENOSYS, 'unimplemented')
with self.capabilities(), patch.object(socket, 'socket', return_value=original), patch.object(bt_sockets, '_libc_connect') as libc:
self.assertIs(bt_sockets.connect_rfcomm(self.ADDRESS, 1), libc.return_value)
original.close.assert_called_once()
def test_missing_python_capability_uses_libc(self):
with patch.object(bt_sockets, 'socket') as python_socket, patch.object(bt_sockets, '_libc_connect') as libc:
del python_socket.AF_BLUETOOTH
self.assertIs(bt_sockets.connect_rfcomm(self.ADDRESS, 1), libc.return_value)
def test_invalid_inputs_never_open_socket(self):
with patch.object(bt_sockets, '_native') as native, patch.object(bt_sockets, '_libc_connect') as libc:
for address, channel in (('bad', 1), (self.ADDRESS, 0), (self.ADDRESS, 31)):
with self.assertRaises(ValueError):
bt_sockets.connect_rfcomm(address, channel)
native.assert_not_called()
libc.assert_not_called()
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "49536cbf72bd1f27ce146a2f062d911757d40c87e5a2247d67d9bae44600cc40", "schema_version": 1}
{"protected_inventory_sha256": "4653c7b94bf628c49e1d16cf6a4c617f3342dcdeffbed7142631858a1f666807", "schema_version": 1}