1f2abac401
* 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.
249 lines
7.5 KiB
C
249 lines
7.5 KiB
C
#ifndef updater_h
|
|
#define updater_h
|
|
|
|
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */
|
|
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#ifdef _WIN32
|
|
#define SHOREBIRD_EXPORT __declspec(dllexport)
|
|
#else
|
|
#define SHOREBIRD_EXPORT __attribute__((visibility("default")))
|
|
#endif
|
|
|
|
|
|
/**
|
|
* An unknown error occurred while updating. The update was not installed.
|
|
* This is a catch-all for errors that don't fit into the other categories.
|
|
*/
|
|
#define SHOREBIRD_UPDATE_ERROR -1
|
|
|
|
/**
|
|
* No update is available (e.g. the app is already up-to-date)
|
|
*/
|
|
#define SHOREBIRD_NO_UPDATE 0
|
|
|
|
/**
|
|
* An update was installed successfully. It will boot from the update on the
|
|
* next app launch.
|
|
*/
|
|
#define SHOREBIRD_UPDATE_INSTALLED 1
|
|
|
|
/**
|
|
* An error occurred while updating. The update was not installed.
|
|
*/
|
|
#define SHOREBIRD_UPDATE_HAD_ERROR 2
|
|
|
|
/**
|
|
* The downloaded patch was not installed because it was invalid.
|
|
*/
|
|
#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3
|
|
|
|
/**
|
|
* Another update was already in progress when this call was made. The
|
|
* already-running update will continue; the caller did not start a new one.
|
|
* This is a benign outcome, not an error.
|
|
*/
|
|
#define SHOREBIRD_UPDATE_IN_PROGRESS 4
|
|
|
|
/**
|
|
* Struct containing configuration parameters for the updater.
|
|
* Passed to all updater functions.
|
|
* NOTE: If this struct is changed all language bindings must be updated.
|
|
*/
|
|
typedef struct AppParameters {
|
|
/**
|
|
* release_version, required. Named version of the app, off of which
|
|
* updates are based. Can be either a version number or a hash.
|
|
*/
|
|
const char *release_version;
|
|
/**
|
|
* Array of paths to the original aot library, required. For Flutter apps
|
|
* these are the paths to the bundled libapp.so. May be used for
|
|
* compression downloaded artifacts.
|
|
*/
|
|
const char *const *original_libapp_paths;
|
|
/**
|
|
* Length of the original_libapp_paths array.
|
|
*/
|
|
int original_libapp_paths_size;
|
|
/**
|
|
* Path to app storage directory where the updater will store serialized
|
|
* state and other data that persists between releases.
|
|
*/
|
|
const char *app_storage_dir;
|
|
/**
|
|
* Path to cache directory where the updater will store downloaded
|
|
* artifacts and data that can be deleted when a new release is detected.
|
|
*/
|
|
const char *code_cache_dir;
|
|
} AppParameters;
|
|
|
|
typedef struct FileCallbacks {
|
|
/**
|
|
* Opens the "file" (actually an in-memory buffer) and returns a handle.
|
|
*/
|
|
void *(*open)(void);
|
|
/**
|
|
* Reads count bytes from the file into buffer. Returns the number of
|
|
* bytes read.
|
|
*/
|
|
uintptr_t (*read)(void *file_handle, uint8_t *buffer, uintptr_t count);
|
|
/**
|
|
* Moves the file pointer to the given offset relative from whence (one of
|
|
* libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new
|
|
* offset relative to the start of the file.
|
|
*/
|
|
int64_t (*seek)(void *file_handle, int64_t offset, int32_t whence);
|
|
/**
|
|
* Closes and frees the file handle.
|
|
*/
|
|
void (*close)(void *file_handle);
|
|
} FileCallbacks;
|
|
|
|
typedef struct UpdateResult {
|
|
int32_t status;
|
|
const char *message;
|
|
} UpdateResult;
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif // __cplusplus
|
|
|
|
/**
|
|
* Configures updater. First parameter is a struct containing configuration
|
|
* from the running app. Second parameter is a YAML string containing
|
|
* configuration compiled into the app. Returns true on success and false on
|
|
* failure. If false is returned, the updater library will not be usable.
|
|
*/
|
|
SHOREBIRD_EXPORT
|
|
bool shorebird_init(const struct AppParameters *c_params,
|
|
struct FileCallbacks c_file_callbacks,
|
|
const char *c_yaml);
|
|
|
|
/**
|
|
* Returns if the app should run the updater automatically on launch.
|
|
*/
|
|
SHOREBIRD_EXPORT bool shorebird_should_auto_update(void);
|
|
|
|
/**
|
|
* The currently running patch number, or 0 if the release has not been
|
|
* patched. The internal name for this concept is `running_patch`; the
|
|
* FFI symbol keeps the historical `current_boot_patch_number` spelling
|
|
* because Flutter Engine links against it.
|
|
*/
|
|
SHOREBIRD_EXPORT uintptr_t shorebird_current_boot_patch_number(void);
|
|
|
|
/**
|
|
* The patch number that will boot on the next run of the app, or 0 if there is
|
|
* no next patch.
|
|
*/
|
|
SHOREBIRD_EXPORT uintptr_t shorebird_next_boot_patch_number(void);
|
|
|
|
/**
|
|
* Performs integrity checks on the next boot patch. If the patch fails these checks, the patch
|
|
* will be deleted and the next boot patch will be set to the last successfully booted patch or
|
|
* the base release if there is no last successfully booted patch.
|
|
*/
|
|
SHOREBIRD_EXPORT
|
|
void shorebird_validate_next_boot_patch(void);
|
|
|
|
/**
|
|
* The path to the patch that will boot on the next run of the app, or NULL if
|
|
* there is no next patch.
|
|
*/
|
|
SHOREBIRD_EXPORT char *shorebird_next_boot_patch_path(void);
|
|
|
|
/**
|
|
* Free a string returned by the updater library.
|
|
* # Safety
|
|
*
|
|
* If this function is called with a non-null pointer, it must be a pointer
|
|
* returned by the updater library.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string);
|
|
|
|
/**
|
|
* Frees an `UpdateResult` previously returned by `shorebird_check_for_update`.
|
|
*
|
|
* # Safety
|
|
*
|
|
* `result` must be a valid pointer returned by `shorebird_check_for_update`,
|
|
* or null (in which case this is a no-op).
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
|
|
|
|
/**
|
|
* Check for an update. Returns true if an update is available.
|
|
*/
|
|
SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
|
|
|
/**
|
|
* Check for an update on the first non-null channel of:
|
|
* 1. `c_channel`
|
|
* 2. The channel specified in shorebird.yaml
|
|
* 3. The default "stable" channel
|
|
*
|
|
* Returns true if an update exists that has not yet been downloaded.
|
|
*/
|
|
SHOREBIRD_EXPORT
|
|
bool shorebird_check_for_downloadable_update(const char *c_channel);
|
|
|
|
/**
|
|
* Synchronously download an update if one is available.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_update(void);
|
|
|
|
/**
|
|
* Synchronously download an update on the first non-null channel of:
|
|
* 1. `c_channel`
|
|
* 2. The channel specified in shorebird.yaml
|
|
* 3. The default "stable" channel
|
|
*
|
|
* Returns an [UpdateResult] indicating whether the update was successful.
|
|
*/
|
|
SHOREBIRD_EXPORT
|
|
const struct UpdateResult *shorebird_update_with_result(const char *c_channel);
|
|
|
|
/**
|
|
* Start a thread to download an update if one is available.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_start_update_thread(void);
|
|
|
|
/**
|
|
* Tell the updater that we're launching from what it told us was the
|
|
* next patch to boot from. This will copy the next boot patch to be the
|
|
* `current_boot` patch.
|
|
*
|
|
* It is required to call this function before calling
|
|
* `shorebird_report_launch_success` or `shorebird_report_launch_failure`.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_report_launch_start(void);
|
|
|
|
/**
|
|
* Report that the app failed to launch. This will cause the updater to
|
|
* attempt to roll back to the previous version if this version has not
|
|
* been launched successfully before.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_report_launch_failure(void);
|
|
|
|
/**
|
|
* Report that the app launched successfully. This will mark the current
|
|
* as having been launched successfully. We don't currently do anything
|
|
* with this information, but it could be used to record a point at which
|
|
* we will not roll back from.
|
|
*
|
|
* This is not currently wired up to be called from the Engine. It's unclear
|
|
* where best to connect it. Expo waits 5 seconds after the app launches
|
|
* and then marks the launch as successful. We could do something similar.
|
|
*/
|
|
SHOREBIRD_EXPORT void shorebird_report_launch_success(void);
|
|
|
|
#ifdef __cplusplus
|
|
} // extern "C"
|
|
#endif // __cplusplus
|
|
|
|
#endif /* updater_h */
|