Eric Seidel 10aaca0f8b fix: skip fetch when prior download is already complete on disk (#351)
* fix: skip fetch when prior download is already complete on disk

A prior attempt that finished downloading but failed a post-download
step (inflate / hash check / install) used to leave the partial file
and sidecar in place. The next update would call compute_resume_offset,
get back the file's full size, and send Range: bytes=N- against an
N-byte resource — past the end of the file, yielding HTTP 416 forever.

Replace compute_resume_offset with a read-only
determine_download_start_state returning Fresh | Resume(u64) |
Complete(u64). update_internal handles Complete by skipping
download_to_path entirely and letting the install path validate the
existing bytes; this also avoids re-downloading when the app is killed
between download and install.

Extract the post-download work into install_downloaded_patch so cleanup
can run unconditionally once after it returns. Three previous cleanup
sites collapse to one. If install rejects the bytes, the next attempt
re-downloads from scratch.

* refactor: pass output_path by value into install_downloaded_patch

Avoids a redundant PathBuf clone on the success path. The caller already
owns the PathBuf from download_dir.join(...), and PatchInfo wants an
owned path, so threading ownership through is strictly cheaper than
borrowing and cloning.

* fix: record actual download size in sidecar to handle chunked transfers

When the server uses chunked transfer encoding (no Content-Length header),
dl_result.content_length is None. The old code recorded that None as
expected_size, which meant a subsequent crash-before-install attempt
would see expected_size: None + a full-size file, fall through to
Resume(file_size), and re-create the HTTP 416 loop this PR fixes.

Recording dl_result.total_bytes (the actual on-disk size) closes the
chunked-encoding case. The two values are equal when Content-Length is
present, so the Content-Length case is unchanged.

Also annotate two pre-existing windows that this PR doesn't fully close,
so they stay visible until the per-patch state machine refactor lands:

- The microsecond gap between download_to_path returning and the second
  write_download_state succeeding can still leave expected_size: None.
- cleanup_download_artifacts logs delete errors instead of propagating;
  a silently-failed delete could re-create the same 416 loop.

Both are tracked in shorebirdtech/shorebird#3737.
2026-05-04 13:53:21 -07:00
2024-04-19 13:51:22 -07:00

Updater library

codecov License: MIT License: Apache

This is the Rust side of the Shorebird code push system. This is built in Rust with a C API for easy calling from other languages, most notably for linking into libflutter.so.

The primary modification Shorebird makes to the stock Flutter engine is adding support for the updater library (this repo). The updater library is written in Rust and is used to update the code running in the Flutter app. The updater library is built as a static library and is linked into the Flutter engine during build time.

Parts

  • library: Runtime library linked into Flutter Engine to unpack and apply updates. Build into libupdater.a and linked into libflutter.so.
  • patch: Developer tooling to package Shorebird updates ("patches"). Built into patch.exe and downloaded and run by shorebird command line tool: https://github.com/shorebirdtech/shorebird/tree/main/packages/shorebird_cli.
  • shorebird_code_push: The Dart bindings for communicating with the updater library from within a Flutter app. Published to pub.dev, usage is optional by developers.

Most interesting code is in the library directory. There is also a README.md in that directory explaining the design.

Developing

It's best to edit this repository from within an engine checkout. See BUILDING_ENGINE.md for instructions on how to set up an engine checkout.

The workflow I use involves 2 to 3 VSC windows:

  1. Opening the engine src.

In that terminal I:

cd third_party/updater
  1. To build the updater as part of the engine:
cargo ndk --target aarch64-linux-android build --release && \
    ninja -C ../../out/android_release_arm64 && say "done"

The cargo part should not be needed, but I haven't yet done the work to integrate the Rust code into the gn files for the Flutter engine yet.

I add say "done" to the end as linking can take several minutes for release Android builds.

  1. In a second window, I open code third_party/updater. I do this because otherwise the rust_analyzer can't seem to find the rust code. We could fix this by adding the directory to the VSC workspace, but I'm not sure where we would put the workspace file in the first place. src is actually shorebirdtech/buildroot and is controlled via gclient by shorebirdtech/engine/DEPS.

  2. In a third window I open my test app. e.g.:

flutter create test_app
cd test_app
shorebird init
shorebird release
code .
  1. To run the test app with my local engine I use:
shorebird run --local-engine-src-path $HOME/Documents/GitHub/engine/src \
    --local-engine android_release_arm64

You may also need to build out/host_release once as flutter build looks for some Dart .dill files in host_release.

Coverage

We'd like to get to 100% coverage but aren't there yet.

https://github.com/taiki-e/cargo-llvm-cov is the best tool I've found for generating coverage reports.

Install: https://github.com/taiki-e/cargo-llvm-cov#installation

cargo llvm-cov will then generate the report.

S
Description
No description provided
Readme 1.6 MiB
Languages
Rust 46.3%
Dart 45.3%
C++ 3.2%
CMake 2.7%
C 1.6%
Other 0.9%