Backport mavftp fixes from AMC - #1267
Conversation
Previous review (2026-08-28)Automated review note — AI-generated (Claude), validated against the live diff (Claude + Codex cross-checked). Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/2026_09_02/devcall_pr_reviews.html#prpymavlink_1267 (also in the AIReview report: https://uav.tridgell.net/DevCallReviews/2026_08_28/devcall_pr_reviews.html#prpymavlink_1267) Reviewed at head All five backported fixes are protocol/logic-correct (the num_params fix was verified against ArduPilot's AP_Filesystem_Param.cpp packer — note MAVProxy's own param_ftp.py still has the same latent total_params bug, worth a follow-up there). Two things block merge: Should fix:
Notes (non-blocking):
|
4d754e2 to
a06da25
Compare
|
@peterbarker CI is green and the AI review issues are addressed |
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/2026_09_02/devcall_pr_reviews.html#prpymavlink-1267 (also in the AIReview report) Re-reviewed at head
For the record, a cold Codex pass claimed the new partial-burst-ack |
peterbarker
left a comment
There was a problem hiding this comment.
⎿ mavftp.py
● 2289 [correctness] The new CLI exit status is taken from `ret`, which for `put`/`list` keeps being overwritten for a full idle_detection_time after the transfer already
succeeded, so successful uploads can exit 1.
● 923 [correctness] An accepted-but-not-completing burst reply now unconditionally returns Success, removing the only "transfer not finished" signal, so a download that stalls
and exits via idle detection reports success with no file written.
|
I addressed both of your findings |
36b14b9 to
fa8c36a
Compare
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Re-reviewed at head The headline fix is correct
What's blocking
CI is red on two Python versions — One unguarded decode remains. Structurally valid parameter data carrying a non-UTF-8 name passes the hardened Verified goodAll direct indexing and |
fa8c36a to
2cd1d20
Compare
Previous review (2026-09-02)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/2026_09_02/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head Still open — narrowed, not closedThe correlation is a real improvement: But
Same user-visible failure as before, one step further in. The new regression places the ACK immediately after the duplicate, so it does not reach this path. New — both new parameter-name tests pass for the wrong reasonThe decoder reads
Both assert Resolved
|
2cd1d20 to
334c1c0
Compare
Capture a MAVFTPReturn failure from a download callback and return it from\nthe reply loop.\n\nThis lets callers of cmd_getparams detect malformed packed parameter data\ninstead of reporting a successful transfer after the callback has rejected\nthe payload. The callback result is cleared for every new download so a\nprior failure cannot affect a later operation. finish termination before returning callback errors return packed-parameter decode failures consume reported callback failures fix(mavftp): skip rejected download output
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_02_2258/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head I measured all three against the merge-base, so these are things this PR changes rather than pre-existing faults. 1. A failed upload now reports successDuring a multi-block Two-block upload whose first
Silent data loss on upload is about the worst failure mode this file has, and firmware and parameter uploads are exactly the multi-block case. The gate needs to let a NACK through regardless of which write it belongs to. 2. Every 256th operation fails at the sequence wrapThe FTP sequence field is 16-bit (
It worked before because 3. A stale reply still re-sends the current request
Lesser point
Both previous findings are genuinely resolvedThe stale-reply scenario no longer reproduces. At step 3 the duplicate has The regression test now exercises the idle path it claims to — Both parameter-name tests now test what they claim. Test coverageWith the PR's |
A successful BurstReadFile reply can contain only part of a download.\n\nReturn success after processing every ACK, including replies that request\nthe next burst. This prevents a valid partial reply from being reported as\na transfer failure on slow telemetry links, while preserving EOF and gap\nrecovery handling. fix(mavftp): safely decode malformed burst nacks
Validate each packed-parameter record before slicing or unpacking it.\n\nMalformed @PARAM responses can end in a partial header or value, or claim a\nshared name prefix longer than the prior parameter name. Report these cases\nas decode failures rather than raising struct errors or producing corrupted\nparameter names.
334c1c0 to
9b13cbb
Compare
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_03_0136/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head Resolved — the sequence wrapYou took the better of the two options:
Resolved — the swallowed upload error
Resolved — the stale-reply re-send, and the session checkThe delayed The suite goes 9 → 17 tests plus 2 subtests. Reverting Open — the new maps are not cleared on timeout or retransmission
So entries accumulate one per retry and survive an operation that times out, in a keyspace that now wraps at 65536. The second pass reports reproducing a concrete consequence — a surviving Three more from the cross-check, not independently reproduced here
None of the three is covered by the new suite. |
Validate decoded packed-parameter records against the transmitted num_params\nheader field.\n\ntotal_params describes the controller-wide parameter count and can be larger\nthan a valid subset response. Using num_params accepts those subset downloads\nwhile still rejecting incomplete or overlong payloads.
Mark file handles opened internally by cmd_put as MAVFTP-owned.\n\nThe existing staging-resource cleanup then closes those handles when the FTP\nsession ends, preventing descriptor leaks and file-lock problems on repeated\nuploads. Handles supplied through cmd_put's fh argument remain caller-owned\nand are left open.
Replace the instance-wide operation_complete flag with a completion record containing the request opcode and reply sequence. This prevents delayed or duplicated replies from a previous operation from completing the command currently being awaited. Add a regression test covering a delayed ListDirectory EOF arriving before a RemoveFile acknowledgement.
Adopt the session IDs returned by OpenFileRO and CreateFile before issuing follow-up requests. Accept those allocation ACKs even though their session differs from the request session. Retain in-flight ReadFile, BurstReadFile, and WriteFile requests so timeout retries resend the original request with its original sequence number. Return decoded NACK errors for gap reads and writes instead of reporting success or FileProtected. Terminate active remote file sessions when either download loop times out. Add regression coverage for session allocation, NACK propagation, retransmission sequence reuse, and both timeout cleanup paths.
9b13cbb to
8ab6106
Compare
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_03_0512/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head Resolved — both halves of the pending-state leakThe retransmission half is fixed at the source: The timeout half is fixed by a new post-loop block in Resolved — the burst re-send and the TerminateSession validationTwo of the three I passed on without reproducing. The burst handler now uses Still open — a complete, correct read can still report failureThe third one. reply_matches_last_op
or completed_upload
or (reply_matches_active_request and packet_ret.error_code != FtpError.Success)So a reply matching an active request rather than The second pass reproduced the user-visible outcome — two gaps with reordered replies, all 240 bytes correct, gaps empty, Test coverage, measured both waysThe suite goes 17 → 28 tests plus 4 subtests, all passing. Reverting The gap is the open finding above: nothing asserts the final |
The parameter decoder imported typing.Tuple and typing.Dict for annotations but called them as constructors at runtime. This broke getparams on supported Python versions after a successful FTP transfer. Use the built-in tuple and dict constructors for sorting and rebuilding decoded parameter mappings.
The callback path already clears publish_result before invoking the callback, making the failure-branch assignment redundant. Remove the dead assignment while preserving callback failure propagation and the guarantee that callback-owned downloads are not published as files. Document the callback success and failure regression coverage, and make the successful callback fixture advertise the exact four-byte payload it consumes.
Keep read_sector() downloads in memory, retain the caller's requested size, and start BurstReadFile at the requested offset. This prevents FUSE reads from downloading the whole remote file, returning an oversized range, or publishing a local file named after the remote path. Add regression coverage for offset reads, returned range length, and absence of local output.
Use integer parameter type IDs in save_params(), matching the values returned by ftp_param_decode() and extract_params(). This makes the getparams datatype-comment option usable for valid parameter files. Add regression coverage for the emitted float datatype comment.
Return a ReadFile failure after terminating a session for a short acknowledgement that does not satisfy an outstanding gap. This prevents the reply loop from reporting a completed download after a file-size race or malformed reply. Add regression coverage for the unexpected short gap-ACK path.
Catch download callback exceptions, retain an FTP failure result, and continue through the normal session cleanup path. This also makes getparams output failures fail the command rather than leaking the active FTP session. Add regression coverage for callback exceptions during download completion.
Reject unsafe command settings before they can violate retry invariants, stall transfer queues, divide by zero, or exceed the MAVFTP payload limit. Defensively validate upload write sizing for callers that set settings directly. Add regressions for invalid command settings and direct invalid upload block sizes.
Previous review (head 72261c7)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_03_2258/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head Resolved — and pinned by a testThe gate at Verified by reverting it. At this head: 37 passed + 11 subtests. Reverting only that gate to its previous form: 1 failed, 36 passed — and the failure is The other two minor items are resolved too — the test now advertises Two bugs in the new synchronous range readBoth are in the 1. A satisfied small range read never stops, and keeps requesting to EOF. The completion test Reproduced: a 2-byte request answered by one 80-byte ACK leaves 2. Memory scales with the read offset rather than the read size — 256 MiB for a 2-byte read.
Measured with The buffer is offset-indexed because line 928 slices Checked and clear
An independent pass reached the same two bugs and the same verdict, having run the full suite (95 passed, 8 skipped, 11 subtests). CI here is 16 pending. |
Validate file listing entries before splitting their name and size fields. Malformed server data now returns InvalidDataSize instead of raising ValueError from the reply-processing loop. Add regression coverage for a file entry without a size separator.
Add an executable MAVFTP integration script for replaying operations against a connected flight controller. The test verifies heartbeat communication and exercises status, configuration, cancellation, listing, upload, CRC, download, rename, removal, directory creation/removal, and parameter retrieval. Temporary remote paths are unique per run and cleaned up on completion or failure.
Complete synchronous range reads as soon as the requested bytes and any gaps are satisfied, even when the reply is a full-sized burst.\n\nAdd a regression covering a small request fulfilled by a full burst so the client terminates instead of requesting data through EOF.
Store synchronous range-read payloads relative to the requested offset so a small read does not allocate a buffer proportional to the remote file offset.\n\nReturn the in-memory range directly from the compact buffer and add a regression covering a one-megabyte offset with a two-byte read.
Store synchronous range-read payloads relative to the requested offset so small reads do not allocate memory proportional to the remote offset. Keep remote and buffer positions distinct while handling burst gaps, retries, and completion, and add a regression for a two-byte read at a one-megabyte offset.
Exercise directory creation/removal and synchronous range reads using the crafted uploaded file, including a small full-burst request and a high-offset request. Reinitialize the MAVFTP connection before rename/delete to avoid late range-read termination replies interfering with subsequent mutations. Document the controller firmware limitation when that handshake remains incomplete.
72261c7 to
48efecd
Compare
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_04_0458/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head The completion check now runs on every payload writeTwo lines in exactly the right place: Verified by reverting it. 40 passed + 11 subtests at this head; reverting only those two lines gives 1 failed, 39 passed, and the failure is Memory is bounded by the request, not the offsetThree coordinated changes, all needed: the Measured with The scoping is right — every translation is guarded by CheckedAccounting stays consistent across the two addressing modes, which was worth confirming given one buffer is now offset-relative and others absolute: Full suite 98 passed / 8 skipped / 11 subtests. Both reviewers reached APPROVE independently. CI here is 16 pending. |
|
@peterbarker This is good to go as soon as the tests pass. Regarding API changes, there are mostly behavioral/protocol changes rather than signature changes:
No public cmd_*, read, or process_ftp_reply method signatures were added or removed. The targeted local tests passed: 38 tests. |
|
Each of the 23 commits fixes a different issue. |
Keep the rename destination stored as arg2 so main() forwards both paths correctly, while displaying new_remote_path in help output.
Previous reviewAutomated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Full report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_04_2335/devcall_pr_reviews.html#prpymavlink-1267 Re-reviewed at head The pylint commit is exactly what it claimsParsing Pushed again mid-run, and one of them fixes a real bugThree more commits landed at
What I missed last round
This is exactly the blind spot a cold review exists for: my previous round returned APPROVE, so a findings-validation pass had nothing to check and would never have looked here. Burst replies are correlated without their sequence number. In One thing I'm not carrying across: a cold pass reported that passing a callback to Declared Python floor is 3.9, all three changed files compile there, and the suite passes 40 tests plus 11 subtests. |
|
Note Superseded. This review was for head ca69138. The current review is at Original review (superseded, kept for reference)Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Reviewed at head ca69138. Full report, with what was checked and the carried-over context: https://uav.tridgell.net/DevCallReviews/2026_09_05/devcall_pr_reviews.html#prpymavlink-1267 REQUEST CHANGESNarrowly, and only about the last commit. Both findings from my previous round are properly fixed and I verified both by measurement. But The ratchet
Measured with your own Why a straggler can be so far ahead: ArduPilot answers one burst request with up to Suggested fix, verified: delete Neither case has a test at this head. If you take the fix, one test each would be worth having — a burst ack pair delivered out of order asserting no gap remains, and a post-retry straggler asserting the restarted stream is still accepted. Minor, and it disappears with that fix: the Previous round — triage
Carried over so nobody re-finds it
The PR body still doesn't record the behaviour changes. Your 2026-09-03 comment lists them well; folding it into the description would make it survive into the merge commit. Two worth adding that aren't in that list: I also checked this against ArduPilot #34296 (the server-side stale-session fix, in flight now): it touches only session validation, not reply sequence numbering or the burst loop, so the two do not conflict — and pymavlink still needs the client side for older firmware and for PX4. I verified the sequence semantics against ArduPilot's An independent cold review, given only the PR number, flagged the same burst-correlation change. |
ca69138 to
4d9e897
Compare
Catch OverflowError when converting API-provided setting values to float, returning InvalidArguments instead of leaking a traceback. Add regression coverage for arbitrarily large integers.
|
Automated review note — AI-generated (Claude), validated against the live diff. Please sanity-check before acting. Re-reviewed at head Follow-up report: https://uav.tridgell.net/DevCallReviews/followups/2026_09_05_2045/devcall_pr_reviews.html#prpymavlink-1267 COMMENT — up from REQUEST CHANGESThe blocker is deleted exactly as proposed and both regression cases now behave like the parent. But the fix I recommended introduced a new boundary, and that one is on me. Previous roundThe ratchet is gone — RESOLVED, verified by execution. Both tests I asked for exist and are RED without the fix. The unreachable My "PX4 semantics unverified" caveat — now verified, and it holds. PX4's New — and it comes from the fix I asked forWith the floor now fixed for the whole burst, a burst longer than 32,767 replies rejects its own tail ( The previous head advanced the floor per reply, so this could not arise. It is a regression against the parent, introduced by removing the six lines I asked to have removed — an independent cold review found it, and I reproduced the boundary. Reachability, which is why this is not a blocker. ArduPilot answers one burst request with at most A fix that keeps both properties. The point of removing the ratchet was to tolerate reordering, so the floor should trail rather than track. Advance it to Anything up to The Minor: both new burst tests seed Carried over
The PR body still records none of the behaviour changes. It is unchanged at three lines. The Thread and CI@peterbarker's CI: 21 of 21 passing, nothing pending. Locally at this head: 45 passed, 13 subtests passed. An independent cold review returned REQUEST CHANGES on the burst boundary above; I adopted the finding and not the severity, for the reachability reason given. |
I would like at some point to get rid of my mavftp.py fork in AMC.
So here are some fixes from that downstream port