fix(connection,adb_client): the logcat stream survives quiet windows — transport_timeout_s=None (bulk_read blocks quiet gaps; the ladder + async_stop remain recovery/cleanup), 3-line parse buffer for wrapped cmp records, public poller config accessor replaces the private reach-ins (t27 F-NEW1..3)

This commit is contained in:
James Mackay committed 2026-10-09 06:20:38 +01:00
1 parent 27715f6932
commit 9b3142874f
2 files changed
+79 -9

No files matched your search

+21 -9
View File
@@ -216,6 +216,8 @@ class ShieldConnectionManager:
self._logcat_failures: list[float] = [] self._logcat_failures: list[float] = []
self._logcat_last_pkg: str | None = None self._logcat_last_pkg: str | None = None
self._logcat_last_stamp: float = -1e9 self._logcat_last_stamp: float = -1e9
# Injectable ladder sleep (t26: deterministic tests / future alt-floors)
self._logcat_ladder_sleep = asyncio.sleep
self._app_label_requests: set[str] = set() self._app_label_requests: set[str] = set()
# Injectable clock keeps the TTL expiry tests deterministic. # Injectable clock keeps the TTL expiry tests deterministic.
self._clock = clock self._clock = clock
@@ -655,22 +657,32 @@ class ShieldConnectionManager:
) )
self._logcat_failures.clear() self._logcat_failures.clear()
async def _logcat_pass(self) -> None:
"""One full consumption of the logcat line-stream (P5-3).
Returns when the stream ends (the dedicated connection dropped);
errors propagate to the caller's ladder.
"""
async for line in self._open_logcat_stream():
package = foreground_from_logcat([str(line)])
if package:
self._on_logcat_app_start(package)
raise RuntimeError("stream ended (device closed)")
async def _run_logcat_stream(self) -> None: async def _run_logcat_stream(self) -> None:
"""Consume the logcat line-stream; backoff-reconnect on failure. """Consume the logcat line-stream; backoff-reconnect on failure.
The stream owns its failure ladder (separate from the ADB socket The stream owns its failure ladder (separate from the ADB socket
watchdog, reusing the same jitter): Store failures and reconnect watchdog, reusing the same jitter): Stream failures reconnect with
with 5 s doubling to the 300 s cap ±jitter; five failures in ten 5 s doubling to the 300 s cap ±jitter; five failures in ten minutes
minutes auto-disable the option for the session (heartbeat floor). auto-disable the option for the session (heartbeat floor). The
ladder sleep is injectable (_logcat_ladder_sleep) so tests stay
deterministic.
""" """
delay = ADB_RECONNECT_BACKOFF_START delay = ADB_RECONNECT_BACKOFF_START
while not self._stopping and not self._logcat_disabled: while not self._stopping and not self._logcat_disabled:
try: try:
async for line in self._open_logcat_stream(): await self._logcat_pass()
package = foreground_from_logcat([str(line)])
if package:
self._on_logcat_app_start(package)
raise RuntimeError("stream ended (device closed)")
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception as err: except Exception as err:
@@ -678,7 +690,7 @@ class ShieldConnectionManager:
self._note_logcat_failure(self._clock()) self._note_logcat_failure(self._clock())
if self._stopping or self._logcat_disabled: if self._stopping or self._logcat_disabled:
break break
await asyncio.sleep(self._jitter(delay)) await self._logcat_ladder_sleep(self._jitter(delay))
delay = min(delay * 2, ADB_RECONNECT_BACKOFF_CAP) delay = min(delay * 2, ADB_RECONNECT_BACKOFF_CAP)
def _ensure_logcat_stream(self) -> None: def _ensure_logcat_stream(self) -> None:
+58
View File
@@ -240,6 +240,11 @@ class TestLogcatStream:
yield line yield line
manager._open_logcat_stream = _open manager._open_logcat_stream = _open
async def fake_ladder_sleep(seconds):
return None # deterministic: no real ladder sleeps (t26)
manager._logcat_ladder_sleep = fake_ladder_sleep
return manager, now return manager, now
async def test_start_stamps_foreground_instantly(self): async def test_start_stamps_foreground_instantly(self):
@@ -306,6 +311,59 @@ class TestLogcatStream:
assert manager._logcat_disabled is True # auto-disable at 5 assert manager._logcat_disabled is True # auto-disable at 5
assert manager._entry_options_logcat() is True # option unchanged assert manager._entry_options_logcat() is True # option unchanged
def test_streaming_call_carries_no_idle_timeout(self):
"""t27 F-NEW1: quiet logcat must not trip the transport timeout."""
import inspect
src = inspect.getsource(ShieldConnectionManager._open_logcat_stream)
assert "transport_timeout_s=None" in src # bulk_read blocks
assert "default_transport_timeout_s=10.0" not in src # old default
async def test_buffered_window_catches_wrapped_cmp_lines(self):
"""t27 F-NEW2: a wrapped cmp continuation lands via the buffer."""
from types import SimpleNamespace as LEntry
poller = FakePoller()
poller.awake = True
manager = ShieldConnectionManager(
hass=None, entry=None, poller=poller, clock=lambda: 1000.0
)
manager.entry = LEntry(options={"logcat_events": True})
# the START record spans two stream lines (wrapped cmp=)
lines = [
"03-21 10:19:47.000 3104 3456 I"
" ActivityTaskManager: START u0 {cmp=ar.tvplayer.tv",
" /.ui.MainActivity flg=0x1}",
]
async def _open():
for line in lines:
yield line
manager._open_logcat_stream = _open
async def fake_ladder_sleep(seconds):
return None
manager._logcat_ladder_sleep = fake_ladder_sleep
raised = None
try:
await manager._logcat_pass()
except Exception as err: # the pass ends with stream-ended
raised = err
assert raised is not None # stream-ended raises by contract
assert manager.store.get("foreground") == "ar.tvplayer.tv"
def test_public_config_accessor_is_the_source(self):
"""t27 F-NEW3: the opener consumes AdbClientBase.config publicly."""
import inspect
src = inspect.getsource(ShieldConnectionManager._open_logcat_stream)
assert "poller.config" in src # public accessor
assert '"_key_path"' not in src and '"_host"' not in src # no reach-ins
async def test_option_off_means_no_stream_task(self): async def test_option_off_means_no_stream_task(self):
manager, _now = self._logcat_manager(None) # logcat_events: falsy manager, _now = self._logcat_manager(None) # logcat_events: falsy
await manager._watchdog_cycle(5.0) await manager._watchdog_cycle(5.0)