Fix repeatable sonpy SIGABRT crashes; add crash detection and diagnostics - #5
Merged
Merged
Conversation
CED's sonpy is a compiled C++ library that, on certain files/channels,
fails an internal assertion and calls abort() (SIGABRT) instead of
raising a catchable Python error. This kills the reader process
mid-stream, which the host (e.g. MATLAB) could previously see as
silently truncated/empty data -- especially on Apple Silicon, where the
'arch -x86_64' wrapper does not reliably propagate a signal death as a
non-zero exit status.
Two complementary changes:
Detection (MATLAB layer). invoke_binary now captures stderr separately
and requires the CLI's completion sentinel ("sonpipe: wrote N ...");
if it is missing, or N does not match the bytes captured, it raises
sonpipe:crash / sonpipe:truncated naming the exact command instead of
returning short data. invoke_text captures stderr to its own file so a
warning or crash text can no longer corrupt the JSON handed to
jsondecode, and surfaces it on failure.
Diagnosis (Python layer). New opt-in breadcrumb logging (debuglog),
enabled via the SONPIPE_LOG environment variable, writes one line
immediately before and after every call into sonpy, flushed with an
unbuffered os.write + fsync so the line survives an abort(). Every sonpy
boundary in sonfile.py (SonFile open, ReadInts, ReadFloats, ReadEvents,
marker reads) and the CLI entry point are instrumented, so the last line
in the log names the exact sonpy call and arguments that crashed.
Adds tests/test_debuglog.py (including a real subprocess abort() that
confirms the pre-call breadcrumb survives) and documents the workflow in
the README Troubleshooting section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
Follow-up to the crash-diagnosis work. A breadcrumb log from a real crash showed the abort happening outside the previously instrumented read/open calls -- i.e. in the metadata accessors or, most likely, when sonpy releases the file handle at interpreter shutdown (a teardown-order assertion that fires after a read has already succeeded). Two changes: - Instrument the remaining sonpy boundaries in sonfile.py: GetTimeBase, MaxChannels, ChannelType, ChannelDivide, GetMaxTime, GetFileVersion, AppID and the _num/_text accessors (ChannelMaxTime, GetChannelScale, GetChannelOffset, GetIdealRate, GetChannelTitle/Units/Comment). The log now covers every call into sonpy, so a metadata crash is pinpointed the same way a read crash is. - Add SmrxFile.close() plus context-manager support, and have every CLI command close the file explicitly (with output flushed first) instead of leaving it to garbage collection. Closing while the interpreter is healthy makes the release step visible in the log and, in practice, avoids the shutdown-order abort() class entirely. A new "done" breadcrumb in main() marks a clean finish, so the log's last line distinguishes a completed command from a mid-call crash. Adds close()/context-manager tests and expands the README Troubleshooting section to explain reading the log tail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
A breadcrumb log from a real crash showed the command running fully to completion -- read succeeds, file handle released, main() returns rc=0, 'done' logged -- and sonpy then aborting (SIGABRT) during Python's *interpreter shutdown*, in a static/atexit destructor that runs after all output has already been delivered. That abort cannot be caught from Python and does no harm to the result, but it still produces a crash report and (through the Apple-Silicon arch wrapper) an alarming failure. Add a process entry point sonpipe.cli:run() that runs main(), flushes stdout/stderr, and then calls os._exit() -- terminating immediately without running Python finalizers or C++ static destructors, so the teardown code where sonpy aborts never executes. Every stream is flushed first, so no data is lost. main() stays a pure, importable function (no os._exit), so tests and in-process callers are unaffected. Wire both entry points to run(): the console script (pyproject [project.scripts]) and python -m sonpipe (__main__.py). The MATLAB-test fakecli.py now also calls run() so the double exercises the same exit path. A 'hard_exit' breadcrumb marks the bypass in the log. Adds unit + end-to-end subprocess tests for run() and documents the workaround in the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
Add an epilog to the argument parser describing the SONPIPE_LOG environment variable (breadcrumb logging for diagnosing a hard sonpy crash), so it is discoverable from `sonpipe --help` / `python -m sonpipe --help`, not only in the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
Breadcrumb logs from a crashing file showed two signatures: (1) a read returns all its samples (`<- ReadInts size=200001`) and then aborts on the very next call into sonpy -- the post-read GetChannelScale/ GetChannelOffset lookups used for scaling; and (2) metadata opens abort in the first sonpy call after SonFile. Address the read path (the common, recoverable case): - read_waveform now resolves scale/offset BEFORE the sample read and accepts them from the caller. cmd_read passes the values already fetched in channel_info, so after ReadInts/ReadFloats the code makes *no* further call into sonpy -- a good read can no longer be turned into a crash by a follow-up sonpy call on a problematic file. - _wave_tick_range clamps nmax to the samples actually available from tfrom to the channel end, so a request that runs past the last sample (e.g. NDR's fixed-size chunking near EOF) is never handed to sonpy as an over-read -- another known way to trip its internal assertions. - Instrument the last un-logged sonpy call (GetOpenError) so a metadata crash is named in the log like every other call. Adds tests for the clamp and for scaling with caller-supplied scale/offset (verifying sonpy is not consulted after the read). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
test_breadcrumb_survives_an_abort asserted returncode == -SIGABRT, which is POSIX-specific. On Windows there are no real signals; a process that aborts exits with a positive code. Assert abnormal termination (returncode != 0) instead; the meaningful checks (the pre-call breadcrumb survives and no completion line was written) are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reading certain CED Spike2 files through sonpipe reliably crashed the Python
reader process with
SIGABRT(Abort trap: 6) inside CED's compiledsonpylibrary — with no error surfaced to MATLAB, just silently missing data and a
macOS crash report. This PR fixes the crash, and adds detection + diagnostics so
any future
sonpymisbehavior is loud and locatable instead of silent.Root cause
sonpyreads the requested samples successfully, but on some files the verynext call into
sonpyaborts the process. A breadcrumb log made it obvious:That next call was our own:
read_waveformfetchedGetChannelScale/GetChannelOffsetafter the read to convert ADC counts to real units.The fix
sonpycall after the sample read.read_waveformnow resolvesscale/offsetbeforeReadInts/ReadFloatsand accepts them from thecaller;
cmd_readpasses the values already fetched inchannel_info. Afterthe data comes back, nothing calls into
sonpy, so a good read can't beturned into a crash. Verified in the log:
<- ReadIntsis now followed onlyby
del SonFile→done→hard_exit._wave_tick_rangecapsnmaxto the samples thatactually exist from the start point to EOF, so fixed-size chunking near the
end of a file never hands
sonpyan over-read (another assert trigger).Also in this PR (hardening + diagnostics)
sonpy's static/atexitteardown after a command completed. The entrypoint (
sonpipe.cli:run, used by thesonpipecommand andpython -m sonpipe) flushes all output then callsos._exit(), skipping the teardown.main()stays importable (noos._exit), so tests/in-process use areunaffected.
than relying on garbage collection at shutdown.
SONPIPE_LOGbreadcrumb logging (debuglog): opt-in, off by default,zero overhead when unset. Writes a line before/after every call into
sonpy,flushed with
os.write+fsyncso it survives anabort(); the last linenames the crashing call. Documented in
sonpipe --help, the READMETroubleshooting section, and the module docstring.
MATLAB-side detection (paired PR in NDR-matlab)
The companion NDR-matlab branch adds a matching change so a mid-stream crash is
raised as
sonpipe:crash/sonpipe:truncated(with the exact command) insteadof returning truncated data — it relies on the completion sentinel this CLI
already prints. The fake CLI test double now emits that sentinel too.
Tests
tests/test_debuglog.py: log gating, breadcrumb format, a real subprocessabort()proving the pre-call breadcrumb survives, andrun()'s hard-exit.tests/test_sonfile.pycases:nmaxclamping, scaling withcaller-supplied
scale/offset(assertingsonpyis not consulted after theread), and
close()/ context-manager behavior.tests, which need CED binaries).
Verification note
Because the crash is inside closed-source
sonpy, please spot-check that a knownchannel's values/plot look correct on a previously-crashing file, to confirm
sonpywas returning correct data before the post-read call.🤖 Generated with Claude Code
https://claude.ai/code/session_01C1UYvbvczg9QvqKg8YNr4k
Generated by Claude Code