Skip to content

Commit 8ec9b3a

Browse files
committed
Add fix for Linux and fix unittests
Linux cannot receive broadcasts if IP is bound to the host IP
1 parent d44a6c6 commit 8ec9b3a

File tree

4 files changed

+69
-37
lines changed

4 files changed

+69
-37
lines changed

PyStageLinQ/PyStageLinQ.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import platform
1212
import psutil
1313
import ipaddress
14+
import os
1415
from typing import Callable
1516

1617
from . import Device
@@ -39,9 +40,6 @@ def __init__(self, ip=None, discovery_port=51337):
3940
self.target_interfaces = []
4041
self.discovery_port = discovery_port
4142
self.get_interface_from_ip(ip)
42-
logger.info(f"Found {len(self.target_interfaces)} network interfaces:")
43-
for iface in self.target_interfaces:
44-
logger.info(f" - {iface.name}: {iface.addr_str}")
4543

4644
def get_interface_from_ip(self, ip):
4745
if ip is None:
@@ -55,30 +53,43 @@ def get_interface_from_ip(self, ip):
5553
ip_list = ip
5654
else:
5755
raise TypeError
56+
logger.info(
57+
f"Found {len(psutil.net_if_stats().items())} total network interfaces, listing IPv4 interfaces:"
58+
)
5859

5960
for interface in psutil.net_if_stats().items():
6061
for interface_info in psutil.net_if_addrs()[interface[0]]:
6162
# Only look for IPV4 binds
62-
if socket.AF_INET == interface_info.family and (
63-
interface_info.address in ip_list or ip_list[0] == "any"
64-
):
65-
self.target_interfaces.append(
66-
PyStageLinQ_interface_info(
67-
interface[0],
68-
len(self.target_interfaces),
69-
int(ipaddress.IPv4Address(interface_info.address)),
70-
interface_info.address,
71-
int(ipaddress.IPv4Address(interface_info.netmask)),
72-
interface[1],
73-
0,
63+
if socket.AF_INET == interface_info.family:
64+
logger.info(f" - {interface[0]}: {interface_info.address}")
65+
if interface_info.address in ip_list or ip_list[0] == "any":
66+
self.target_interfaces.append(
67+
PyStageLinQ_interface_info(
68+
interface[0],
69+
len(self.target_interfaces),
70+
int(ipaddress.IPv4Address(interface_info.address)),
71+
interface_info.address,
72+
int(ipaddress.IPv4Address(interface_info.netmask)),
73+
interface[1],
74+
0,
75+
)
7476
)
75-
)
77+
78+
logger.info(
79+
f"{len(self.target_interfaces)} interfaces matched with requested interfaces and will be used by PyStageLinQ:"
80+
)
81+
for iface in self.target_interfaces:
82+
logger.info(f" - {iface.name}: {iface.addr_str}")
7683

7784
def send_discovery_frame(self, discovery_frame):
7885
for interface in self.target_interfaces:
7986
try:
80-
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as discovery_socket:
81-
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
87+
with socket.socket(
88+
socket.AF_INET, socket.SOCK_DGRAM
89+
) as discovery_socket:
90+
discovery_socket.setsockopt(
91+
socket.SOL_SOCKET, socket.SO_BROADCAST, 1
92+
)
8293
discovery_socket.bind((interface.addr_str, 0))
8394
discovery_socket.sendto(
8495
discovery_frame,
@@ -182,16 +193,13 @@ def start(self):
182193

183194
def _stop(self):
184195
logger.info(f"Stop requested, trying graceful shutdown")
185-
try:
186-
discovery = StageLinQDiscovery()
187-
discovery_info = self.discovery_info
188-
discovery_info.ConnectionType = ConnectionTypes.EXIT
189-
discovery_frame = discovery.encode_frame(discovery_info)
196+
discovery = StageLinQDiscovery()
197+
discovery_info = self.discovery_info
198+
discovery_info.ConnectionType = ConnectionTypes.EXIT
199+
discovery_frame = discovery.encode_frame(discovery_info)
190200

191-
self.network_interface.send_discovery_frame(discovery_frame)
192-
logger.info(f"Gracefully shutdown complete")
193-
except Exception as e:
194-
logger.debug('Could not send "EXIT" discovery frame during shutdown')
201+
self.network_interface.send_discovery_frame(discovery_frame)
202+
logger.info(f"Gracefully shutdown complete")
195203

196204
def _announce_self(self):
197205
discovery = StageLinQDiscovery()
@@ -214,9 +222,11 @@ async def _discover_stagelinq_device(self, host_ip, timeout=10):
214222
# Create socket
215223
discover_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
216224

225+
bind_ip = "" if os.name == "posix" else host_ip
226+
217227
try:
218228
discover_socket.bind(
219-
(host_ip, self.StageLinQ_discovery_port)
229+
(bind_ip, self.StageLinQ_discovery_port)
220230
) # bind socket to broadcast
221231
except Exception as e:
222232
# Cannot bind to socket, check if IP is correct and link is up
@@ -361,12 +371,16 @@ async def _periodic_announcement(self):
361371

362372
async def _py_stagelinq_strapper(self):
363373
strapper_tasks = set()
364-
logger.info(f"Looking for discovery frames on {len(self.network_interface.target_interfaces)} IP addresses:")
374+
logger.info(
375+
f"Looking for discovery frames on {len(self.network_interface.target_interfaces)} IP addresses:"
376+
)
365377

366378
for interface in self.network_interface.target_interfaces:
367379
logger.info(f"{interface.addr_str}")
368380
strapper_tasks.add(
369-
asyncio.create_task(self._discover_stagelinq_device(interface.addr_str, timeout=2))
381+
asyncio.create_task(
382+
self._discover_stagelinq_device(interface.addr_str, timeout=2)
383+
)
370384
)
371385

372386
while self.get_loop_condition():

tests/Main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def state_map_data_print(data):
5353
def main():
5454
logging.basicConfig(level=logging.INFO)
5555
global PrimeGo
56-
ip_choice = 1
56+
ip_choice = 2
5757
match ip_choice:
5858
case 0:
5959
PrimeGo = PyStageLinQ.PyStageLinQ(
@@ -69,6 +69,12 @@ def main():
6969
name="Jaxcie StageLinQ",
7070
ip=["169.254.13.37", "127.0.0.1"],
7171
)
72+
case 3:
73+
# should fail
74+
PrimeGo = PyStageLinQ.PyStageLinQ(
75+
new_device_found_callback, name="Jaxcie StageLinQ", ip="127.0.0.1"
76+
)
77+
7278
PrimeGo.start_standalone()
7379

7480

tests/unit/test_unit_PyStageLinQ.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ async def test_discover_stagelinq_device_bind_error(
142142

143143
dummy_socket.socket.return_value.bind.side_effect = Exception()
144144

145-
with pytest.raises(Exception) as exception:
145+
assert (
146146
await dummy_pystagelinq._discover_stagelinq_device(dummy_ip)
147-
148-
assert exception.type is Exception
147+
== PyStageLinQError.CANNOTBINDSOCKET
148+
)
149149

150150

151151
def test_get_loop_condition(dummy_pystagelinq):
@@ -176,7 +176,7 @@ async def test_discover_stagelinq_check_initialization(
176176
dummy_socket.AF_INET, dummy_socket.SOCK_DGRAM
177177
)
178178
dummy_socket.socket.return_value.bind.assert_called_once_with(
179-
(dummy_ip, dummy_pystagelinq.StageLinQ_discovery_port)
179+
("", dummy_pystagelinq.StageLinQ_discovery_port)
180180
)
181181
dummy_socket.socket.return_value.setblocking.assert_called_once_with(False)
182182

@@ -832,6 +832,11 @@ async def test_py_stagelinq_strapper(dummy_pystagelinq, monkeypatch):
832832
monkeypatch.setattr(
833833
dummy_pystagelinq, "_discover_stagelinq_device", discover_device_mock
834834
)
835+
monkeypatch.setattr(
836+
dummy_pystagelinq.network_interface,
837+
"target_interfaces",
838+
[PyStageLinQ.PyStageLinQ.PyStageLinQ_interface_info("", 0 , 0,"", 0, "", 0)]
839+
)
835840

836841
await dummy_pystagelinq._py_stagelinq_strapper()
837842

@@ -850,6 +855,11 @@ async def test_py_stagelinq_strapper_loop_condition_false(
850855
monkeypatch.setattr(
851856
dummy_pystagelinq, "_discover_stagelinq_device", discover_device_mock
852857
)
858+
monkeypatch.setattr(
859+
dummy_pystagelinq.network_interface,
860+
"target_interfaces",
861+
[PyStageLinQ.PyStageLinQ.PyStageLinQ_interface_info("", 0 , 0,"", 0, "", 0)]
862+
)
853863

854864
await dummy_pystagelinq._py_stagelinq_strapper()
855865

@@ -884,6 +894,9 @@ async def test_py_stagelinq_strapper_task_exception(
884894
monkeypatch.setattr(
885895
dummy_pystagelinq, "_discover_stagelinq_device", discover_device_mock
886896
)
897+
monkeypatch.setattr(
898+
dummy_pystagelinq.network_interface, "target_interfaces", [PyStageLinQ.PyStageLinQ.PyStageLinQ_interface_info("", 0 , 0,"", 0, "", 0)]
899+
)
887900

888901
get_loop_condition_mock.side_effect = [True, False]
889902

tests/unit/test_unit_PyStageLinQ_interface_info.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,7 @@ def test_send_discovery_frame_permission_error(
364364

365365
monkeypatch.setattr(PyStageLinQ.PyStageLinQ, "socket", dummy_socket)
366366

367-
with pytest.raises(PermissionError) as exception:
368-
dummy_PyStageLinQ_network_interface.send_discovery_frame(dummy_discovery_frame)
367+
dummy_PyStageLinQ_network_interface.send_discovery_frame(dummy_discovery_frame)
369368

370369
dummy_socket.socket.assert_called_once_with(
371370
dummy_socket.AF_INET, dummy_socket.SOCK_DGRAM

0 commit comments

Comments
 (0)