Files
shorebird-updater/library
Bryan Oltman f4db98b759 feat: treat iOS patches as diffs instead of full artifacts (#88)
* feat: apply iOS patches from diffs

* Clean up imports

* fix lint

* Add iOS, tests

* refactor

* revert changes to shorebird_code_push

* remove unused debug trait

* coverage

* imports
2023-09-22 16:58:31 -04:00
..
2023-03-06 15:06:31 -08:00

Shorebird CodePush Updater

The rust library that does the actual update work.

Design

The updater library is built in Rust for safety (and modernity). It's built as a C-compatible library, so it can be used from any language.

The library is thread-safe, as it needs to be called both from the flutter_main thread (during initialization) and then later from the Dart/UI thread (from application Dart code) in Flutter.

The overarching principle with the Updater is "first, do no harm". The updater should "fail open", terms of continuing to work with the currently installed or active version of the application even when the network is unavailable.

The updater also needs to handle error cases conservatively, such as partial downloads from a server, or malformed responses (e.g. a proxy interfering) and not crash the application or leave the application in a broken state.

Every time the updater runs it needs to verify that the currently installed patch is compatible with the currently installed base version. If it is not, it should refuse to return paths to incompatible patches.

The updater also needs to regularly verify that the current state directory is in a consistent state. If it is not, it should invalidate any installed patches and return to a clean state.

Not all of the above is implemented yet, but such is the intent.

Architecture

The updater is split into separate layers. The top layer is the C-compatible API, which is used by all consumers of the updater. The C-compatible API is a thin wrapper around the Rust API, which is the main implementation but only used directly for testing (see the cli directory).

Thread safety is handled by a global configuration object that is locked when accessed. It's possible I've missed cases where this is not sufficient, and there could be thread safety issues in the library.

  • src/c_api.rs - C-compatible API
  • src/lib.rs - Rust API (and crate root)
  • src/update.rs - Core updater logic
  • src/config.rs - In memory configuration and thread locking
  • src/cache.rs - On-disk state management
  • src/logging.rs - Logging configuration (for platforms that need it)
  • src/network.rs - Logic dealing with network requests and updater server

Rust

We use normal rust idioms (e.g. Result) inside the library and then bridge those to C via an explicit stable C API (explicit enums, null pointers for optional arguments, etc). The reason for this is that it lets the Rust code feel natural and also gives us maximum flexibility in the future for exposing more in the C API without having to refactor the internals of the library.

https://docs.rust-embedded.org/book/interoperability/rust-with-c.html are docs on how to use Rust from C (what we're doing).

https://github.com/RubberDuckEng/safe_wren has an example of building in Rust and exposing it with a C api.

Integration

The updater library is built as a static library, and is linked into the libflutter.so as part of a custom build of Flutter. We also link libflutter.so with the correct flags such that updater symbols are exposed to Dart.

The dart_bindings directory contains the Dart bindings for the updater library.

Building for Android

The best way I found was to install: https://github.com/bbqsrc/cargo-ndk

cargo install cargo-ndk
rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    x86_64-linux-android \
    i686-linux-android
cargo ndk -t armeabi-v7a -t arm64-v8a build --release

When building to include with libflutter.so, you need to build with the same version of the ndk as Flutter is using:

You'll need to have a Flutter engine checkout already setup and synced. As part of gclient sync the Flutter engine repo will pull down a copy of the ndk into src/third_party/android_tools/ndk.

Then you can set the NDK_HOME environment variable to point to that directory. e.g.:

NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk

Then you can build the updater library as above. If you don't want to change your NDK_HOME, you can also set the environment variable for just the one call:

NDK_HOME=$HOME/Documents/GitHub/engine/src/third_party/android_tools/ndk cargo ndk -t armeabi-v7a -t arm64-v8a build --release

Imagined Architecture (not all implemented)

Assumptions (not all enforced yet)

  • Updater library is never allowed to crash, except on bad parameters from C.
  • Network and Disk are untrusted.
  • Running code is trusted.
  • Store-installed bundle is trusted (e.g. APK).
  • Updates are signed by a trusted key.
  • Updates must be applied in order.
  • Updates are applied in a single transaction.

Update State Machine

  • Server is authoritative, regarding current update/patch state. Client can cache state in memory. Not written to disk.
  • Patches are downloaded to a temporary location on disk.
  • Update State Machine:
    • ready: Just woke up, ready to check for updates.
    • checking: Checking for updates.
    • update_available: Update or rollback is available.
    • no_update_available: No update is available.
    • downloading: Downloading an update.
    • downloaded: Downloaded an update.
  • Client keeps on disk:
    • cache of patches in "slots"
    • cache of in-progress download state.
    • Last booted patch (may not have been successful).
    • Last successful patch (never rolled back from unless becomes invalid).
  • Boot State Machine:
    • ready: Just woke up, ready to boot.
    • booting: Booting a patch.
    • booted: Patch is booted, we will not go back from here.

Slot State Machine

  • Patches are cached on disk in "slots".
  • There is a currently active slot (the one that is booted).
  • Patches are identified by base revision + patch number.
  • A given slot is:
    • empty: No update is installed.
    • pending: An update is installed but has not been validated.
    • valid: An update is installed and has been validated.
  • Validation is a temporary state. Patches/slots are revalidated on boot.

Trust model

  • Network and Disk are untrusted.
  • Running software (including apk service) is trusted.
  • Patch contents are signed, public key is included in the APK.

TODO:

  • Add an async API.
  • Write tests for state management.
  • Make state management/filesystem management atomic (and tested).
  • Support validating patches/slots (hashes, signatures, etc).

Later-stage update system design docs