diff --git a/README.md b/README.md index b3ee8d9..bec65da 100644 --- a/README.md +++ b/README.md @@ -4,64 +4,9 @@ This is the C/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. +See cli/README.md for more documentation on the library. + ## Parts * cli: Test the updater library via the Rust API (for development). * dart_cli: Test ffi wrapping of updater library. * library: The rust library that does the actual update work. - -## Imagined Architecture (not all implemented) - -### Update State Machine -* Server is authoritative, regarding current update/patch state. Client can - cache state in memory. Not written to disk. -* 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). - -### 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. - -### Download State Machine -* Patches are downloaded to a temporary location on disk. -* A given download is: - * `queued`: No download has been attempted. - * `downloading`: Download is in progress. - * `success`: Download is complete. - * `failure`: Download failed. - -### 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. - -## 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. - -## 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 -* https://theupdateframework.io/ -* https://fuchsia.dev/fuchsia-src/concepts/packages/software_update_system diff --git a/cli/src/main.rs b/cli/src/main.rs index a6ba69a..6c05e2b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -50,7 +50,6 @@ base_url: http://localhost:8000 match version { Some(v) => { println!("path: {:?}", v.path); - println!("hash: {:?}", v.hash); println!("version: {:?}", v.version); } None => { diff --git a/library/README.md b/library/README.md index 7228a3f..75f9757 100644 --- a/library/README.md +++ b/library/README.md @@ -48,6 +48,19 @@ and there could be thread safety issues in the library. * 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 @@ -83,3 +96,60 @@ It isn't currently wired into the build process, so you'll need to run it manual cargo install cbindgen cbindgen --config cbindgen.toml --crate updater --output library/include/updater.h ``` + +## 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 +* https://theupdateframework.io/ +* https://fuchsia.dev/fuchsia-src/concepts/packages/software_update_system diff --git a/library/src/cache.rs b/library/src/cache.rs index 9d5d5f1..24a6de4 100644 --- a/library/src/cache.rs +++ b/library/src/cache.rs @@ -12,14 +12,14 @@ use crate::network::PatchCheckResponse; pub struct PatchInfo { pub path: String, pub version: String, - pub hash: String, } #[derive(Deserialize, Serialize, Default, Clone)] struct Slot { + /// Path to the slot directory. path: String, - version: String, - hash: String, + /// Version of the patch in this slot. + patch_version: String, } // This struct is public, as callers can have a handle to it, but modifying @@ -31,8 +31,16 @@ pub struct UpdaterState { // and say "this client is in the 10% bucket, so it gets the new version". // It might be better for this to just be a number 0-100? #[serde(default = "Uuid::new_v4")] + /// ID generated for this device/client used for staged rollouts. client_id: Uuid, + /// List of patches that failed to boot. We will never attempt these again. + failed_patches: Vec, + /// List of patches that successfully booted. We will never rollback past + /// one of these for this device. + successful_patches: Vec, + /// Currently selected slot. current_slot_index: usize, + /// List of slots. slots: Vec, // Add file path or FD so modifying functions can save it to disk? } @@ -42,11 +50,47 @@ impl Default for UpdaterState { Self { client_id: Uuid::new_v4(), current_slot_index: 0, + failed_patches: Vec::new(), + successful_patches: Vec::new(), slots: Vec::new(), } } } +impl UpdaterState { + pub fn is_known_good_patch(&self, patch: &PatchInfo) -> bool { + self.successful_patches.iter().any(|v| v == &patch.version) + } + + pub fn is_known_bad_patch(&self, patch: &PatchInfo) -> bool { + self.failed_patches.iter().any(|v| v == &patch.version) + } + + pub fn mark_patch_as_bad(&mut self, patch: &PatchInfo) { + if self.is_known_good_patch(patch) { + warn!("Tried to report failed launch for a known good patch. Ignoring."); + return; + } + + if self.is_known_bad_patch(patch) { + return; + } + self.failed_patches.push(patch.version.clone()); + } + + pub fn mark_patch_as_good(&mut self, patch: &PatchInfo) { + if self.is_known_bad_patch(patch) { + warn!("Tried to report successful launch for a known bad patch. Ignoring."); + return; + } + + if self.is_known_good_patch(patch) { + return; + } + self.successful_patches.push(patch.version.clone()); + } +} + pub fn load_state(cache_dir: &str) -> anyhow::Result { // Load UpdaterState from disk let path = Path::new(cache_dir).join("state.json"); @@ -81,8 +125,7 @@ pub fn current_patch(state: &UpdaterState) -> Option { // Otherwise return the version info from the current slot. return Some(PatchInfo { path: slot.path.clone(), - version: slot.version.clone(), - hash: slot.hash.clone(), + version: slot.patch_version.clone(), }); } @@ -166,8 +209,7 @@ fn download_into_slot( slot_index, Slot { path: path.to_str().unwrap().to_string(), - version: patch.version.clone(), - hash: patch.hash.clone(), + patch_version: patch.version.clone(), }, ); save_state(&state, cache_dir)?; diff --git a/library/src/network.rs b/library/src/network.rs index 36e71a8..bf0e13b 100644 --- a/library/src/network.rs +++ b/library/src/network.rs @@ -42,7 +42,6 @@ pub fn send_patch_check_request( body.insert("base_version", config.base_version.clone()); if let Some(patch) = patch { body.insert("patch_version", patch.version); - body.insert("patch_hash", patch.hash); } body.insert("platform", current_platform().to_string()); body.insert("arch", current_arch().to_string()); diff --git a/library/src/updater.rs b/library/src/updater.rs index 5056631..66f4367 100644 --- a/library/src/updater.rs +++ b/library/src/updater.rs @@ -98,6 +98,26 @@ pub fn active_patch() -> Option { }); } +pub fn report_failed_launch() { + with_config(|config| { + let mut state = load_state(&config.cache_dir).unwrap_or_default(); + + let patch = current_patch(&state).unwrap(); + state.mark_patch_as_bad(&patch); + save_state(&state, &config.cache_dir).unwrap(); + }); +} + +pub fn report_successful_launch() { + with_config(|config| { + let mut state = load_state(&config.cache_dir).unwrap_or_default(); + + let patch = current_patch(&state).unwrap(); + state.mark_patch_as_good(&patch); + save_state(&state, &config.cache_dir).unwrap(); + }); +} + /// Synchronously checks for an update and downloads and installs it if available. pub fn update() -> UpdateStatus { return with_config(|config| {