diff --git a/AGENTS.md b/AGENTS.md index d23fecd..bdfa140 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,6 +304,29 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all real captures contain a 372 ATR), so end-to-end proof needs a re-captured `.sr` from the failing phone; the unit tests cover the loop-escape logic. +### v1.7.1: accept EOFError as clean end-of-stream (newer libsigrokdecode) + +Newer libsigrokdecode signals end-of-capture from `wait()` by raising +`EOFError("samples exhausted")`; the 0.5.3 build on the Linux dev box instead +returns `None` / raises `SystemError`, so this only showed up on Windows (a +newer libsigrokdecode). The framework explicitly accepts `EOFError` out of +`decode()` as normal termination (`instance.c`: "Termination with an +EOFError exception is accepted"), but our `decode()` caught it in the generic +`except Exception` handler, printing a traceback, logging `DECODE ERROR`, and +resyncing -- retrying up to 50 times and flooding the log (looked like a +crash) at the end of every capture. + +- `decode()` and the five `except SystemError: raise` guards now use + `except (SystemError, EOFError): raise`, so the end-of-stream signal + propagates and libsigrokdecode terminates cleanly. The critical guard is + `_measure_clock_period()`, whose `try` wraps a `wait()` loop. +- 2 new unit tests assert `decode()` re-raises `EOFError` and `SystemError`. +- 67/67 unit tests. VERSION -> 1.7.1. + +Deployment note: the Windows decoder is a git clone, so `git pull` there +(and deleting `__pycache__`) picks this up; the EOF error cannot be +reproduced against Linux 0.5.3. + ### v1.7.0: spurious end-of-decode hardening + start.sh session resilience A live capture could end without Ctrl+C (the session simply stopped). The @@ -607,7 +630,7 @@ official gsmtap.h reserves — a simtrace2-sniff convention. ### Versioning -The decoder version is defined in `pd.py` as `VERSION = '1.7.0'`. +The decoder version is defined in `pd.py` as `VERSION = '1.7.1'`. The version is printed to the log on decoder startup. ### Testing after decoder changes diff --git a/pd.py b/pd.py index bb033aa..19a0a03 100644 --- a/pd.py +++ b/pd.py @@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender, GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT, GSMTAP_FLAG_BAD_FCS) -VERSION = '1.7.0' +VERSION = '1.7.1' @@ -807,7 +807,7 @@ class Decoder(srd.Decoder): wes = max(self.es, wss + 1) self.put(wss, wes, self.out_ann, [Ann.ANN_WARN, ["noisy signal"]]) - except SystemError: + except (SystemError, EOFError): raise except Exception: pass @@ -856,7 +856,7 @@ class Decoder(srd.Decoder): self._samples_per_clock = period self.log("clock period (samples):", period) return True - except SystemError: + except (SystemError, EOFError): raise except Exception: pass @@ -1235,7 +1235,7 @@ class Decoder(srd.Decoder): try: self.put(self.ss, self.samplenum, self.out_ann, [Ann.ANN_WARN, ["I/O stuck low?"]]) - except SystemError: + except (SystemError, EOFError): raise except Exception: pass @@ -1346,7 +1346,7 @@ class Decoder(srd.Decoder): try: self.put(self.ss, self.samplenum, self.out_ann, [Ann.ANN_WARN, ["I/O stuck low?"]]) - except SystemError: + except (SystemError, EOFError): raise except Exception: pass @@ -1520,7 +1520,7 @@ class Decoder(srd.Decoder): try: self.put(self.ss, self.samplenum, self.out_ann, [Ann.ANN_WARN, ["I/O stuck low?"]]) - except SystemError: + except (SystemError, EOFError): raise except Exception: pass @@ -2586,14 +2586,17 @@ class Decoder(srd.Decoder): while True: # Crash protection: garbage from a noisy live signal must # never kill the whole sigrok-cli process. Log, warn and - # resync. SystemError from wait() is the end-of-sample- - # stream signal and must propagate for a clean exit; - # KeyboardInterrupt (Ctrl+C) likewise passes through. + # resync. End-of-sample-stream must propagate for a clean + # exit: older libsigrokdecode raises SystemError from + # put()/wait(), newer versions raise EOFError('samples + # exhausted') from wait() -- libsigrokdecode accepts either + # as normal decode() termination. KeyboardInterrupt + # (Ctrl+C) likewise passes through. try: if (not self.decode_step()): break failures = 0 - except SystemError: + except (SystemError, EOFError): raise except Exception as e: traceback.print_exc(file=sys.stderr) @@ -2603,7 +2606,7 @@ class Decoder(srd.Decoder): wes = max(self.es, wss + 1) self.put(wss, wes, self.out_ann, [Ann.ANN_WARN, ["decode error, resynced"]]) - except SystemError: + except (SystemError, EOFError): raise except Exception: pass diff --git a/tests/test_gsmtap.py b/tests/test_gsmtap.py index 24451a7..0804829 100644 --- a/tests/test_gsmtap.py +++ b/tests/test_gsmtap.py @@ -530,5 +530,37 @@ class TestAtEof(unittest.TestCase): self.assertTrue(self.inst._at_eof()) +class TestEndOfStream(unittest.TestCase): + """decode() must let end-of-stream exceptions propagate. + + Newer libsigrokdecode signals end-of-capture from wait() with + EOFError('samples exhausted') (older versions use SystemError). + libsigrokdecode accepts either as normal decode() termination, so + swallowing them turns a clean end into a flood of DECODE ERROR retries. + """ + + def setUp(self): + _, pd = _load_iso7816() + self.pd = pd + + def _decode_raising(self, exc_cls): + inst = object.__new__(self.pd.Decoder) + inst.write_pcap_header = lambda: None + inst.finish = lambda: None + + def step(): + raise exc_cls('samples exhausted') + inst.decode_step = step + return inst + + def test_eoferror_propagates(self): + with self.assertRaises(EOFError): + self._decode_raising(EOFError).decode() + + def test_systemerror_propagates(self): + with self.assertRaises(SystemError): + self._decode_raising(SystemError).decode() + + if __name__ == '__main__': unittest.main()