Commit Graph

27 Commits

Author SHA1 Message Date
Eric Seidel 34509fca3c refactor: split C API into Dart and engine surfaces (#350)
The C surface in `library/src/c_api` was a single bucket of `pub extern "C"`
functions covering both consumers — `package:shorebird_code_push` (via
ffigen) and Shorebird's Flutter engine fork (via direct C++ link). That
made it hard to reason about which symbols are stable ABI versus
internal, and ffigen was generating bindings for engine-only symbols
that no Dart code calls.

Split into two self-contained submodules and two cbindgen-generated
headers:

- `c_api::dart` → `include/updater_dart.h` (stable ABI; ffigen entry
  point). Defines `UpdateResult`, the `SHOREBIRD_*` status constants, and
  the five Dart-stable functions: `shorebird_current_boot_patch_number`,
  `shorebird_next_boot_patch_number`,
  `shorebird_check_for_downloadable_update`,
  `shorebird_update_with_result`, `shorebird_free_update_result`.
- `c_api::engine` → `include/updater_engine.h` (no stability guarantee).
  Defines `AppParameters`, `FileCallbacks`, and the engine-only functions:
  `shorebird_init`, `shorebird_should_auto_update`,
  `shorebird_validate_next_boot_patch`, `shorebird_next_boot_patch_path`,
  `shorebird_free_string`, `shorebird_start_update_thread`, and the
  `shorebird_report_launch_*` trio.

Each bucket file is self-contained: cbindgen scans only the file
(`with_src` in build.rs) and emits the items it defines plus the C
types they reference. There are no exclude/include lists in the
cbindgen configs — adding a function to one bucket automatically lands
it in the right header, and items in the other bucket cannot leak.

`mod.rs` shrinks to a thin layer of private helpers shared by both
buckets (`to_rust`, `allocate_c_string`, `free_c_string`, `log_on_error`)
plus the test module.

`include/updater.h` is removed; consumers include the specific header
for their use case. The Flutter engine's
`shell/common/shorebird/updater.cc` will be updated in a follow-up
engine-repo PR to include `updater_engine.h` directly.

Also drops two retired Dart-side symbols:

- `shorebird_update` (replaced by `shorebird_update_with_result` in the
  Dart 2.0 rewrite, Nov 2024).
- `shorebird_check_for_update` (replaced by
  `shorebird_check_for_downloadable_update` in the same rewrite).

The shorebird_code_push package's `_legacyFallback` was the only path
that still called `shorebird_update`. The package's `flutter: >=3.24.5`
constraint guarantees the engine has `shorebird_update_with_result`, so
the fallback was unreachable in practice. Removing it lets us drop the
ABI symbol.

Bumps shorebird_code_push to 2.0.7. Bindings regenerated via ffigen now
contain only the five Dart-stable symbols.

Follow-up engine PR will: include `updater_engine.h` instead of the
removed `updater.h`; clean up `android_exports.lst` (drop the ghost
`shorebird_active_path` and `shorebird_active_patch_number` exports,
drop `shorebird_check_for_update`).
2026-05-04 15:56:09 -07:00
Eric Seidel 1f2abac401 fix: current_boot_patch survives server-driven rollback (#348)
* fix: current_boot_patch survives server-driven rollback

Customer report (shorebirdtech/shorebird#3728): when the device's running
patch is rolled back to the base release (no replacement patch), the
running session sees `checkForUpdate` return `upToDate` even though a
restart is needed. Patch-to-patch rollback works because the server's
replacement patch makes `check_for_downloadable_update` return true, so
Dart short-circuits to `outdated` before the comparison runs.

Root cause: `UpdaterState::current_boot_patch()` derived its return value
from `currently_booting_patch.or(last_successfully_booted_patch)`. After
boot success, only `last_booted_patch` reflected the running patch. When
the server rolled back that patch, `try_fall_back_from_patch` cleared
`last_booted_patch` (correctly — it's no longer a valid fallback), and
the FFI `shorebird_current_boot_patch_number` then reported 0 even
though the process was still running the rolled-back patch.

The conflation: `last_booted_patch` was doing two unrelated jobs —
"fallback target" (its real role) and "what's running" (the proxy via
`.or()` that broke under rollback). The earlier Dart-only fix in
shorebirdtech/updater#312 assumed the FFI would still report the
running patch number; that assumption only held in the mock.

Fix: introduce a dedicated `current_boot_patch: Option<usize>` field
on `PatchesState`. Set by `report_launch_start` from `next_boot_patch`
(or `None` for a release boot). Read directly by
`UpdaterState::current_boot_patch()` — no derivation, no fallback.
Each field now has exactly one job:

- `last_booted_patch`: fallback target for `try_fall_back_from_patch`.
  Doc updated to remove the "(usually the currently running patch)"
  parenthetical that perpetuated the conflation.
- `current_boot_patch` (new): what this process is using. Survives
  rollbacks of that patch (the process is still using it). Reset on
  the next `report_launch_start` — including `None` on a release
  boot, so it doesn't go stale.
- `currently_booting_patch`: unchanged. Still the boot-in-progress
  flag for crash detection on the next init.

C API surface unchanged. `shorebird_current_boot_patch_number` still
returns the same `usize` it always has — it just gets the right answer
under rollback now.

Verification (testing at the C API level, since that's the contract):

- New regression test `rollback_to_release_keeps_current_boot_patch`
  reproduces the customer's bug. Fails on the parent commit
  (`current_boot_patch_number` returns 0); passes after this fix
  (returns 1).
- New `rollback_to_release_then_restart_clears_current_boot_patch`
  proves the post-restart cleanup: the on-disk `current_boot_patch`
  is `Some(1)` from the previous run, but the next launch's
  `report_launch_start` resets it to `None` since `next_boot_patch`
  is `None`. No false-positive `restartRequired` on the release boot.
- New `rollback_patch_to_patch_reports_current_and_next_distinctly`
  proves we didn't break the patch-to-patch case. Running on patch 2,
  server rolls back to patch 1: after `update()`, `current=2, next=1`.
- All 225 existing tests pass without modification, including every
  C API test.

Refs: shorebirdtech/shorebird#3728, shorebirdtech/updater#312, #270

* docs: TODOs for follow-up cleanup of patch state model

Two cleanups deferred from #348 to keep the rollback fix focused:

1. Rename `last_booted_patch` → `fallback_patch`. Single mechanical
   rename, but touches ~30 test names that read in terms of the
   current field name.
2. Remove `currently_booting_patch` entirely. With `current_boot_patch`
   now tracking what's running, the boot-in-progress signal collapses
   to `boot_started_at.is_some()`, and the crashed-patch-on-init
   identification falls out of the previous run's `current_boot_patch`.
   This is the larger of the two — touches crash-detection logic and
   the boot-record helpers.

Both should land as their own commits so the diff for each is easy to
read and the rollback fix stays minimal.

* test: assert rollback-only phases never report events

In `rollback_to_release_keeps_current_boot_patch` and
`rollback_to_release_then_restart_clears_current_boot_patch`, the
phase that performs only the server-driven rollback never calls
`shorebird_update` or `shorebird_report_launch_*`, so no event
should ever be reported during it. Replace the no-op report hook
with `UNEXPECTED_REPORT` to make that an asserted property of the
test rather than a silent assumption — if a future change starts
queueing or sending events from `check_for_downloadable_update`,
these tests will surface it immediately.

Phase-1 spawned threads (PatchDownload, PatchInstallSuccess) are
unaffected: they hold a clone of the config from when they were
spawned, so they hit the phase-1 hooks and never reach phase-2's
panicking handler.

The patch-to-patch test keeps the no-op hook because phase 2 there
calls `shorebird_update`, which legitimately spawns a PatchDownload
event using the new hooks.

* docs: flag the last_booted_patch conflation as the underlying bug

Replace the rename TODO with one that names the actual unfixed bug:
`last_booted_patch` gets cleared in `try_fall_back_from_patch` while
the running process is still using the patch. That's the deeper
incoherence — the field's name and `record_boot_success` say it's a
historical record, but the rollback path treats it as an operational
fallback target. Those two roles only diverge under server rollback,
which is the customer's case.

This PR sidesteps the conflation by adding `current_boot_patch` for
the "what's running" semantic. The TODOs now flag both:

- The conflation itself, on the field declaration.
- The specific line in try_fall_back_from_patch that does the
  historically-incorrect clearing.

A sibling PR will prototype the alternative — keep last_booted_patch
historical, express "don't fall back to this patch" via a separate
signal — so we can compare the two approaches.

* fix: stop clearing last_booted_patch when its patch is rolled back

Roll #349 into this PR. Both fixes together — they address different
real bugs and combining eliminates each PR's loose ends.

Underlying data-model bug: `last_booted_patch` was conflated. Its
field name and `record_boot_success` say it's a *historical* record
(\"the patch that last successfully booted, ever\"). But
`try_fall_back_from_patch`'s \"both bad\" branch clears it whenever
the patch becomes invalid as a fallback — including when the server
rolls it back, while the running process is still using it.

Fix: in the \"both bad\" branch, only clear `next_boot_patch`. Leave
`last_booted_patch` alone — that history shouldn't change because
the server told us not to use the patch next time. The \"don't fall
back to this patch\" intent is already covered by:

- `delete_patch_artifacts(bad_patch_number)` at the top of the
  function, which removes the on-disk artifacts.
- `validate_patch_is_bootable` in the else-if branch, which refuses
  to fall back to a patch with missing artifacts.
- `is_known_bad_patch`, which records boot failures explicitly.

`record_boot_failure_for_patch` flows through the same branch and
benefits from the same correction — boot history is preserved across
boot failures. Updated the corresponding test
(`clears_last_booted_patch_if_it_is_the_failed_patch` →
`preserves_last_booted_patch_on_failure_but_marks_bad`) to assert
the new behavior: history preserved, known-bad recorded, artifacts
deleted.

Removes the two TODOs added in the previous commit:
- The conflation TODO on `last_booted_patch` (now fixed).
- The TODO on the offending line (line is being changed).

This pairs with the `current_boot_patch` field added earlier in the
same PR. The two fixes are orthogonal:

- `current_boot_patch` gives us a session-scoped \"what's running\"
  signal, reset on `report_launch_start`. It's what the FFI reads.
- The data-model fix here keeps `last_booted_patch` historically
  accurate, so the field's name finally matches what it stores.

With both, `current_boot_patch()` no longer needs the `.or()`
fallback that was the original source of the customer's bug.
2026-05-01 16:38:47 -07:00
Eric Seidel 563f1b773a fix: return UpdateInProgress status instead of erroring when another update is running (#335)
When `update()` is called while another update (typically the automatic
updater thread) is already running, the Rust updater previously bailed
with `UpdateError::UpdateAlreadyInProgress`, which surfaced in Dart as
`UpdateException: Update already in progress (unknown)`. This is the
single highest-volume `UpdateException` in customer telemetry, yet the
underlying situation is benign — the in-flight update continues on its
own, the caller simply did not start a new one.

Add a new `UpdateStatus::UpdateInProgress` variant and matching C
status code `SHOREBIRD_UPDATE_IN_PROGRESS = 4`. `updater::update()`
catches the `UpdateAlreadyInProgress` error from the lock helper and
maps it to `Ok(UpdateStatus::UpdateInProgress)`. The Dart wrapper
treats the new status as a successful return alongside
`SHOREBIRD_UPDATE_INSTALLED`, so `update()` no longer throws for this
case.

Update the existing `usage_during_hung_update` c_api test to assert the
new contract, and add a Dart test covering the in-progress return path.

Version skew: new Dart on an old engine still sees the legacy
`SHOREBIRD_UPDATE_ERROR` + "Update already in progress" message and
will still throw. The fix lands once both sides ship.

Partially addresses shorebirdtech/shorebird#3682 — does not resolve the
broader asymmetry of `update()` semantics (it still does not wait for
someone else's in-flight update to finish), which remains as v2 design
work in shorebirdtech/shorebird#3684.
2026-04-08 01:22:04 +00:00
Eric Seidel 50954da68a style: fix clippy warnings and add clippy check to CI (#321)
* style: fix clippy warnings and add clippy check to CI

Runs `cargo clippy --all-targets -- -D warnings` in the rust_crate CI
action to catch lint issues before they land.

Fixes: redundant field names, needless returns, unnecessary closures,
io_other_error, needless borrows, single_match, unnecessary_unwrap,
and adds missing safety docs.

* chore: add 'clippy' to cspell dictionary
2026-04-02 02:02:05 +00:00
Eric Seidel 6d0e4a1193 chore(deps): bump Rust and Dart dependencies (#315)
* chore(deps): bump Rust and Dart dependencies

Bump Rust dependencies in library/ and patch/:
- comde: 0.2.3 → 0.3.1 (library), 0.2.3 → 0.3.0 (patch)
- zip: 0.6.4 → 3.0.0 (breaking: FileOptions → SimpleFileOptions)
- android_logger: 0.13.0 → 0.15.0
- mockall: 0.12.1 → 0.13.1
- serial_test: 2.0.0 → 3.2.0
- cbindgen: 0.24.0 → 0.28.0

Bump Dart dev dependency in shorebird_code_push/:
- ffigen: upper bound <17.0.0 → <19.0.0

Updated zip API usage (FileOptions → SimpleFileOptions) and
adjusted test assertion for changed error message.

Binary size impact (macOS release, arm64):
- libupdater.a: +83 KB (+0.28%)
- libupdater.dylib: +34 KB (+0.75%)

Closes #206, #271, #273.

* chore: add EOCD to cspell dictionary

The zip 3.0 crate changed its error message to reference "EOCD"
(End of Central Directory), which cspell doesn't recognize.
2026-03-30 15:50:44 -07:00
Bryan Oltman 8bfe1bac47 fix: separate validation checks from next_boot_patch getter (#297)
* fix: separate validation checks from next_boot_patch getter

* coverage

* coverage

* coverage
2025-10-08 17:55:29 -04:00
Felix Angelov 54e1e2ce2f fix(updater): re-export shorebird_check_for_update for backward compat (#258) 2024-12-19 15:51:07 -06:00
Felix Angelov ee3f5ec669 feat(shorebird_code_push): track support (#232)
* feat(shorebird_code_push): track support

* cleanup and add todos

* docs

* run ffigen

* use c_char instead of char

* Add channel support

* tests

* tests

* update podfile.lock

* Update example to include tracks selector

---------

Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
2024-11-12 14:58:50 -05:00
Felix Angelov 6f1be35bd3 feat(shorebird_code_push): rewrite Dart API (#225) 2024-11-04 12:22:37 -06:00
Bryan Oltman ed013eb257 feat: Update C API to consume Read+Seek callbacks (#111)
* Update C API to consume Read+Seek callbacks

* remove open and close functions

* update to reflect new interface

* Refactor posix file i/o to c_api (#113)

* Refactor posix file i/o to c_api

* fix comment

* rename ExternalFile to ReadSeek

* cleanup and comments

* Add SHOREBIRD_PATCH_BASE_FILENAME const

* fix lint

* add fake callbacks for c_api tests

* fix tests

* remove os_last_error

* remove todos, add comments

* cleanup

* remove params to open

* add c_api module, tests

* reorganize

* Return Err if CFileProvider open returns null

* add comments and docs
2024-01-16 14:28:04 -05:00
Bryan Oltman b4024cf499 fix: add separate storage dir for state (#84) 2023-09-12 16:13:19 -04:00
Eric Seidel e4b182d75d chore: fix 30 of the 54 warnings from clippy pedantic. (#81)
I just ran `cargo clippy -- -W clippy::pedantic` and fixed things.

These are more invasive that the default set and the remaining
warnings are mostly about our (abysmal) public docs missing
Error and Panic sections to explain errors and panicks.
2023-09-08 17:51:07 +00:00
Eric Seidel 20b65f14c2 chore: run cargo clippy over our rust codebase (#80)
* chore: apply fixes from clippy

* Add a Safety section to appease clippy
2023-09-08 17:30:19 +00:00
Eric Seidel 15ab0f2295 feat: add support for parsing auto_update from shorebird.yaml (#65)
* feat: add support for parsing auto_update from shorebird.yaml

The actual support will be in the C++ engine, but this keeps
all yaml parsing in the Rust code for simplicity.

---------

Co-authored-by: Felix Angelov <felix@shorebird.dev>
2023-08-03 18:37:09 +00:00
Bryan Oltman 318d86e13f feat: add shorebird_current_boot_patch_number function (#36)
* feat: add shorebird_current_boot_patch_number function

* Continue support for next_patch_version

* rename, return null in case of 0
2023-06-20 16:45:43 -04:00
Bryan Oltman 0222f526a6 chore: use a number instead of a string for next boot patch number (#34) 2023-06-19 22:49:59 -04:00
Eric Seidel 226df7d08d test: Make our rust tests use per-thread config rather than global static (#14)
* Make our rust tests use per-thread config rather than global static

This lets our unit tests run in parallel.  I also moved our "integration"
tests back to be unit tests for now.

This also exposes a network mocking abstraction.

I've added an incomplete test of applying a full patch.  Still
needs some work to complete.

* Finish making the patch success test work

* Changed our test config to use println instead of info/error, etc. so that tests show output on failure (there might be a better way?)
Unfortunately macro_use (which is the way we were causing error!, info! etc to appear everywhere with only one global import before, only works on crates not on std, so I had to be explicit with my imports per-file.
* Add a function to build a fake zip file, because that's what the updater currently expects.  Better would be for us to move to a AssetManager system I suspect, then the updater would ask the asset manager for the libapp.so and we'd return it instead of having to go through writing out a zip file.
* Added a string_patch.rs tool and shared code between that and the patch tool.  This made it possible/easy for me to generate the necessary binary patches as well as needed hashes to pretend to be the patch server.
* Added one simple test of the patch tool core logic.

In total we're now above 80% coverage of our rust code after this.

* remove stray comment
2023-04-26 13:07:19 -05:00
Eric Seidel 4ab78869b8 feat: Add start_updater_thread to update off the main thread (#11)
* feat: Add start_updater_thread to update off the main thread

This makes it so that clients can easily not block when wanting to
queue an update.

I have a separate patch which updates the Engine to use this new
API.

I also needed to split the concept of the "next_boot" patch
from the "current_boot" patch, previously refered to as
"current" or "active" patch.  This required adding a
report_launch_start api to let the updater library know
when to set current_boot patch from next_boot.

I also removed the rust updater/cli in this as well as the
vmpath argument to init.

I also exposed the report_launch_success api, but its not yet
used by the Engine.

I renamed report_failed_launch to report_launch_failure to match
report_launch_start which I introduced.

* Update naming per comments from Felix.

Also added a helper for char* allocation (not sure if it's better).

* Update library/src/updater.rs

---------

Co-authored-by: Felix Angelov <felix@shorebird.dev>
2023-04-25 09:06:40 -07:00
Eric Seidel 1281624ccf Rename release_version to version_name, add version_code, remove vm_path (#8) 2023-04-19 19:46:41 +00:00
Felix Angelov 60a931f0d9 fix(updater): use all base library paths (#206) 2023-03-31 12:44:12 -05:00
Eric Seidel 0d113ab094 feat: Add support for diff patches (#163)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
2023-03-24 19:59:35 +00:00
Eric Seidel 1220e63f9e Expose "report_launch_failure" so engine can call it. (#97) 2023-03-20 17:32:03 -05:00
Felix Angelov f5103bc1e8 refactor(updater): conform to new backend interfaces (#90) 2023-03-17 13:00:35 -05:00
Eric Seidel b18ac7b947 feature: Teach the rust updater library about shorebird.yaml (#54) 2023-03-10 23:49:19 -06:00
Eric Seidel eacc7e8b87 Make updater library thread safe
This was needed so that it could be called from Dart as well as flutter_main/C++.

It turns out flutter_main does not run on the "ui thread", so when Dart was calling
into the updater it would panic due to thinking the updater (which was using a thread local) was not yet initialized.

Also added the log-panics crate on Android so that panics appear in adb logcat.
2023-03-08 13:03:13 -08:00
Eric Seidel 52ec7f3c4e refactor: split updater library into layers (#47) 2023-03-08 11:11:10 -06:00
Eric Seidel f757251162 chore: Fix the rust build (#27) 2023-03-06 15:06:31 -08:00