Skip to content

Commit a5e3a97

Browse files
committed
fix(sat): improve Docker error reporting, socket fallback, and payload field name
- Show actual DockerError messages in CLI instead of generic "Docker Desktop is required" message, so users can see the real failure cause - Add fallback to known Docker Desktop socket paths on macOS when docker.from_env() fails - Fix payload field name from "payload" to "payload_b64" to match sdr-docker API response format
1 parent 1622815 commit a5e3a97

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

src/hubblenetwork/cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,8 +2860,8 @@ def sat_scan(
28602860
# Fail fast: verify Docker is available before printing anything.
28612861
try:
28622862
sat_mod.ensure_docker_available()
2863-
except sat_mod.DockerError:
2864-
msg = _docker_err_msg()
2863+
except sat_mod.DockerError as exc:
2864+
msg = str(exc) or _docker_err_msg()
28652865
if printer.suppress_info_messages:
28662866
click.echo(json.dumps({"error": msg}))
28672867
else:
@@ -2892,9 +2892,9 @@ def _on_interrupt(sig, frame):
28922892
printer.print_row(pkt)
28932893
if count is not None and printer.packet_count >= count:
28942894
break
2895-
except sat_mod.DockerError:
2895+
except sat_mod.DockerError as exc:
28962896
error_occurred = True
2897-
msg = _docker_err_msg()
2897+
msg = str(exc) or _docker_err_msg()
28982898
if printer.suppress_info_messages:
28992899
click.echo(json.dumps({"error": msg}))
29002900
else:

src/hubblenetwork/sat.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import json
1414
import logging
1515
import time
16+
from pathlib import Path
1617
from typing import Generator, List, Optional, Set, Tuple
1718

1819
import httpx
@@ -37,10 +38,21 @@ def _packets_url(port: int = API_PORT) -> str:
3738
# ---------------------------------------------------------------------------
3839

3940

41+
_DOCKER_DESKTOP_SOCKETS = [
42+
Path.home() / ".docker/run/docker.sock",
43+
Path.home() / "Library/Containers/com.docker.docker/Data/docker-cli.sock",
44+
]
45+
46+
4047
def _get_client():
4148
"""Return a Docker client from the environment.
4249
43-
Raises ``DockerError`` if the Docker SDK is not installed.
50+
Tries ``docker.from_env()`` first (which honours ``DOCKER_HOST`` and the
51+
default ``/var/run/docker.sock``). If that fails, probes well-known
52+
Docker Desktop socket paths on macOS before giving up.
53+
54+
Raises ``DockerError`` if the Docker SDK is not installed or no
55+
reachable daemon is found.
4456
"""
4557
try:
4658
import docker
@@ -52,7 +64,24 @@ def _get_client():
5264
try:
5365
return docker.from_env()
5466
except docker.errors.DockerException:
55-
raise DockerError("Docker is not available")
67+
pass
68+
69+
# Fallback: try known Docker Desktop socket paths.
70+
for sock in _DOCKER_DESKTOP_SOCKETS:
71+
if sock.exists():
72+
try:
73+
client = docker.DockerClient(base_url=f"unix://{sock}")
74+
client.ping()
75+
return client
76+
except Exception:
77+
continue
78+
79+
raise DockerError(
80+
"Docker is not available. If Docker Desktop is running, enable "
81+
"'Allow the default Docker socket to be used' in Docker Desktop "
82+
"settings, or set the DOCKER_HOST environment variable "
83+
"(e.g. export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock)."
84+
)
5685

5786

5887
def ensure_docker_available() -> None:
@@ -140,7 +169,7 @@ def _parse_jsonl(text: str) -> List[SatellitePacket]:
140169
continue
141170
try:
142171
obj = json.loads(line)
143-
payload_b64 = obj.get("payload", "")
172+
payload_b64 = obj.get("payload_b64", "")
144173
payload = base64.b64decode(payload_b64) if payload_b64 else b""
145174
packets.append(
146175
SatellitePacket(

tests/test_sat.py

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import base64
66
import json
7+
from pathlib import Path
78
from unittest.mock import MagicMock, patch
89

910
import pytest
@@ -65,10 +66,10 @@ def test_frozen(self):
6566
SAMPLE_JSONL = (
6667
'{"device_id": "0xBB2973BD", "seq_num": 153, "device_type": "silabs", '
6768
'"timestamp": 1774289859.339, "rssi_dB": -42.3, "channel_num": 2, '
68-
f'"freq_offset_hz": 21654.5, "payload": "{_PAYLOAD_B64}"}}\n'
69+
f'"freq_offset_hz": 21654.5, "payload_b64": "{_PAYLOAD_B64}"}}\n'
6970
'{"device_id": "0xBB2973BD", "seq_num": 154, "device_type": "silabs", '
7071
'"timestamp": 1774289863.860, "rssi_dB": -42.9, "channel_num": 15, '
71-
f'"freq_offset_hz": 21588.0, "payload": "{_PAYLOAD_B64}"}}\n'
72+
f'"freq_offset_hz": 21588.0, "payload_b64": "{_PAYLOAD_B64}"}}\n'
7273
)
7374

7475

@@ -136,6 +137,96 @@ def test_key_tuple(self):
136137
# ---------------------------------------------------------------------------
137138

138139

140+
class TestGetClient:
141+
@patch("docker.from_env")
142+
def test_from_env_succeeds(self, mock_from_env):
143+
mock_client = MagicMock()
144+
mock_from_env.return_value = mock_client
145+
assert sat._get_client() is mock_client
146+
147+
@patch("hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [])
148+
@patch("docker.from_env")
149+
def test_from_env_fails_no_fallbacks(self, mock_from_env):
150+
import docker
151+
152+
mock_from_env.side_effect = docker.errors.DockerException("no sock")
153+
with pytest.raises(DockerError, match="Docker is not available"):
154+
sat._get_client()
155+
156+
@patch("docker.DockerClient")
157+
@patch("docker.from_env")
158+
def test_fallback_to_desktop_socket(self, mock_from_env, mock_client_cls, tmp_path):
159+
import docker
160+
161+
mock_from_env.side_effect = docker.errors.DockerException("no sock")
162+
163+
# Create a fake socket file so the path.exists() check passes.
164+
fake_sock = tmp_path / "docker.sock"
165+
fake_sock.touch()
166+
167+
mock_client = MagicMock()
168+
mock_client_cls.return_value = mock_client
169+
170+
with patch("hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [fake_sock]):
171+
result = sat._get_client()
172+
173+
mock_client_cls.assert_called_once_with(base_url=f"unix://{fake_sock}")
174+
mock_client.ping.assert_called_once()
175+
assert result is mock_client
176+
177+
@patch("docker.DockerClient")
178+
@patch("docker.from_env")
179+
def test_fallback_skips_nonexistent_sockets(
180+
self, mock_from_env, mock_client_cls, tmp_path
181+
):
182+
import docker
183+
184+
mock_from_env.side_effect = docker.errors.DockerException("no sock")
185+
186+
missing = tmp_path / "missing.sock"
187+
existing = tmp_path / "docker.sock"
188+
existing.touch()
189+
190+
mock_client = MagicMock()
191+
mock_client_cls.return_value = mock_client
192+
193+
with patch(
194+
"hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [missing, existing]
195+
):
196+
result = sat._get_client()
197+
198+
# Only the existing socket should be tried.
199+
mock_client_cls.assert_called_once_with(base_url=f"unix://{existing}")
200+
assert result is mock_client
201+
202+
@patch("docker.DockerClient")
203+
@patch("docker.from_env")
204+
def test_fallback_skips_socket_that_fails_ping(
205+
self, mock_from_env, mock_client_cls, tmp_path
206+
):
207+
import docker
208+
209+
mock_from_env.side_effect = docker.errors.DockerException("no sock")
210+
211+
bad_sock = tmp_path / "bad.sock"
212+
bad_sock.touch()
213+
good_sock = tmp_path / "good.sock"
214+
good_sock.touch()
215+
216+
bad_client = MagicMock()
217+
bad_client.ping.side_effect = Exception("connection refused")
218+
good_client = MagicMock()
219+
220+
mock_client_cls.side_effect = [bad_client, good_client]
221+
222+
with patch(
223+
"hubblenetwork.sat._DOCKER_DESKTOP_SOCKETS", [bad_sock, good_sock]
224+
):
225+
result = sat._get_client()
226+
227+
assert result is good_client
228+
229+
139230
class TestDockerHelpers:
140231
@patch("hubblenetwork.sat._get_client")
141232
def test_ensure_docker_available_success(self, mock_get_client):
@@ -254,4 +345,4 @@ def test_docker_not_available(self, mock_sat, runner):
254345

255346
result = runner.invoke(cli, ["sat", "scan", "--timeout", "1"])
256347
assert result.exit_code != 0
257-
assert "Docker" in result.output
348+
assert "Docker is not installed" in result.output

0 commit comments

Comments
 (0)