Flush the shell transport's staged write before it is read back (#61) - #63
Conversation
The `sh -c` script both shell transports run stages a temporary and renames it into place with nothing flushed anywhere, so a crash or a power loss shortly after `put_file` reports success can leave the destination absent, empty, or holding the previous contents. That is the one staged-write path left when the in-process ones were made durable, and it is the path `secrets.json` is written by — the file a re-run reads its AppRole back from. Both halves are flushed now: the temporary once its owner and mode are on it, and the directory holding the destination once the rename has created the entry there. The targeted `sync "$1"` is attempted first, because coreutils flushes that one object rather than the machine, and a bare `sync` is the floor an implementation refusing the operand falls to. Which one runs is settled by what the call does at run time rather than by probing for the name: `sync` is present on every implementation a target can carry, so its presence says nothing about whether the operand is honoured, and the one that accepts and ignores it has already performed the fallback's flush by the time it exits 0. A host with no working `sync` at all says so on stderr rather than failing the install or passing the write off as durable. Closes #61
The asymmetry this issue names is caller-visible: `put_file_natively` promised a durable landing and the shell transports promised nothing, and `Executor::put_file` — the doc a caller holding an `Executor` actually reads — separated the transports only by step 3's mechanism. Now that both halves are flushed on every transport the promise is uniform, so it belongs on the trait method, along with the one caveat that the shell transports rest theirs on the target's `sync`. The stub test asserted the destination's directory was flushed but said nothing about the temporary, so removing the first `flush` would have left it green. It now asserts the first call names the temporary and the directory is flushed only after it. Part of #61
`Executor::put_file`'s doc promised that a host with no working `sync` "says on stderr that it was not flushed", but both shell transports capture the child's stderr and read it only on a non-zero exit, so a write that otherwise succeeded hands the caller nothing. The crate has no logging facade to raise the line through, and gaining one is not this primitive's to decide, so the doc states the boundary rather than promising past it. Part of #61
|
[Reviewer Round 1] Approved — I found no blocking issues. The script now flushes the temporary only after its ownership and mode have been applied, and flushes the destination directory after the rename ( The new tests exercise—not merely inspect—all four relevant target behaviors, including the rejecting implementation’s bare fallback and the no-working-sync warning ( The PR links |
|
[Review Verdict Round 1: APPROVED] |
Suggested squash commitTitle Body |
PUT_FILE_SCRIPT— thesh -cscript bothLocalExecutor::put_fileandSshExecutor::put_filerun — staged and renamed with nothing flushed anywhere, so a crash shortly afterput_filereported success could leavedestabsent, empty, or holding the previous contents. That is the pathbootlerwritessecrets.jsonover, the fileread_secretsreads back on a re-run. This closes the gap #58 deliberately left, making every transport's landing durable as well as atomic.Changes
A
flush()shell function,sync "$1" 2>/dev/null || sync || echo "warning: $1 was not flushed: no working sync" >&2, called twice: once afterchown/chmodand before themv, so what it protects is the temporary's bytes together with the owner and mode just applied to them; once after themv, for the directory entry the rename created, which does not exist at the first point.Selection is by what the construct does at run time, never by a name probe —
syncis present in all three implementations a target can carry, socommand -vcannot tell the one that honours the operand from the one that ignores it. Coreutils flushes the named object (a directory operand included); macOS and a busybox built withoutFEATURE_SYNC_FANCYaccept the operand, ignore it and flush the host, having already done what the fallback would; an implementation that refuses the operand exits non-zero and the baresyncruns. A host with no workingsyncat all still lands the file and says on stderr that it was not flushed.ddappears nowhere, in either spelling.PUT_FILE_SCRIPT's doc states what is flushed, which half each construct covers, which shells and utilities each was chosen against, and the cost the floor carries — a baresyncflushes every filesystem on the host, which is what a target without coreutils gets, and Linux'ssyncwaits for the writeback where POSIX would let it return early.Executor::put_file's doc now states the durability promise for every transport, names the one thing it rests on that this crate does not supply, and says plainly that the skipped-flush warning reaches nobody: this script's stderr surfaces only throughExecutorError::Transfer, so aput_filereturningOkon such a host reports nothing.set -ebehaviour, theEXITtrap, and the post-mvinode check are unchanged in effect; the||list's left operands are exempt fromset -ein dash, bash and busybox ash alike. No new dependency, no public signature change, no new error variant, andCHANGELOG.mdis untouched — nothing here is observable to a user of a release this crate has not cut.Closes #61
Test plan
chmodandmv, the directory's aftermv||on the targeted form, with nocommand -vprobe and noddanywhere in the scriptsyncare exercised with a stub earlier on the child'sPATH— honours the operand, refuses it, accepts and ignores it — and the write succeeds in every case, with the refusing stub asserted to reach the baresyncfor both halves rather than to failsync: the write still succeeds and the skipped flush is asserted on stderrLocalExecutorlanding tests pass unchanged, ownership, mode and inode-check assertions includedLocalExecutorandSshExecutorstill emit an identical script, through the singlelanding_argvconstruction sitecargo fmt -- --check --config group_imports=StdExternalCratecargo clippy --all-targets -- -D warningsandcargo clippy --all-targets --features test-support -- -D warningscargo testandcargo test --features test-support, on macOS as well as Linux, where the hostsyncis itself the operand-ignoring implementation