scripts/regression_test.sh exercises both writer behaviors -- normal
append-only growth (the original, must-keep-working case) and preallocated
growth (ftruncate-extend then shrink-and-rename, the case that caused the
hang/corruption fixed in the previous commit) -- in both transfer
directions, against a real ssh test host with a real capture file sliced to
a record-aligned prefix. scripts/pcap_offsets.py computes record-boundary
cut points for staging realistic growth.
Bump FINALIZE_GRACE_PERIOD from 500ms to 1s: a comfortable margin even for
a writer whose header and payload writes aren't atomic -- if the header
landed, the body should follow shortly.
A preallocating capture writer (ftruncate-extend, zero-filled) reports the
full reserved file size immediately and fills real data into it in place,
without file length ever reflecting real progress until a final truncate at
rotation. The tailer trusted raw file length for both "is there new data"
and "are we done", so it read (and, in raw mode, forwarded) preallocated
zero padding as real packet data, then could never satisfy pos == file_len
once the file was truncated down at rotation -- a permanent hang, and worse,
a corrupted destination even when the hang didn't bite.
Replace file-length-based tracking with confirmed_len(), derived from
actually walking pcap records (RecordAlignedChunker), for any partial
(still-growing) source -- non-compress non-partial transfers are untouched,
preserving today's "arbitrary content" guarantee there. Growth detection now
re-reads from the last confirmed boundary every poll rather than only ever
reading forward past what's already been read, since a preallocating writer
can flip a byte from zero to real content without file length ever changing.
Two correctness properties enforced by the new record scanner, both gated to
partial sources only:
- A record's header being fully present and plausible is not proof its
payload is real (header and payload aren't necessarily written
atomically) -- a candidate record is only confirmed once the *next*
record's header has also been observed and looks real, proof the writer
moved past it. This holds complete_len one record behind by construction.
- That rule alone would starve the true last record of any capture forever,
so a narrow escape hatch trusts the trailing record on its own
plausibility once the rename has been observed and the file's length has
been stable across continuous re-checks for a grace period -- backed by
the writer's own "I'm done" signal (the rename), not a timing guess alone.
Also: an all-zero global header on a partial source is now treated as "not
written yet" rather than a hard error, for the same preallocation reason.
Verified against a live preallocation simulation over ssh (locl.sh): a
source truncated to a padded size well beyond its real content, with a
writer catch-up (in-place record write with no length change) before the
final truncate-and-rename, transfers with no hang and a byte-exact
destination -- confirmed_len stalls precisely at the real/padding boundary
and only advances once content, not file length, proves growth.
Adds an optional timestamped trace of transfer-relevant events (tail
position vs. file length, rename detection, finalize, protocol
send/recv) to both the client and server sides, so a stalled transfer
can be diagnosed from correlated local/remote logs instead of guessing.
Notably logs when a source's final size ends up smaller than what was
already read (pos > current_len after rename) -- this can never
satisfy the pos == current_len finalize condition, which is a real
deadlock risk for writers that preallocate a fixed-size file and
truncate down to the actual capture length before renaming.
--log <path> on either the client or `--server` process traces that
process's own events. --log on ClientCli is local-only; --remote-log
<path> is forwarded as the spawned --server process's --log so both
sides of a remote transfer leave a trace, tied together by timestamp.
ServerCli has no --extension field -- run_send_server already gets
fully-resolved --partial and --final-source from the client, so the
extension is never needed remotely. Passing it made every remote-source
pull fail with "unexpected argument '--extension' found".
scp's "host:" (no path after the colon) means the remote home
directory, reusing the source's basename. resolve_dest_paths only
recognized a directory target via a trailing '/', so an empty path
fell through and was used literally as the filename, producing an
empty --dest-final and hanging the remote --server --recv.