docs: Add ability to mark patches good/bad (not used yet) (#70)

This commit is contained in:
Eric Seidel
2023-03-14 14:41:28 -07:00
committed by GitHub
parent df1bf4a50d
commit a50c1d3126
6 changed files with 141 additions and 66 deletions
+2 -57
View File
@@ -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
-1
View File
@@ -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 => {
+70
View File
@@ -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
+49 -7
View File
@@ -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<String>,
/// List of patches that successfully booted. We will never rollback past
/// one of these for this device.
successful_patches: Vec<String>,
/// Currently selected slot.
current_slot_index: usize,
/// List of slots.
slots: Vec<Slot>,
// 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<UpdaterState> {
// Load UpdaterState from disk
let path = Path::new(cache_dir).join("state.json");
@@ -81,8 +125,7 @@ pub fn current_patch(state: &UpdaterState) -> Option<PatchInfo> {
// 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)?;
-1
View File
@@ -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());
+20
View File
@@ -98,6 +98,26 @@ pub fn active_patch() -> Option<PatchInfo> {
});
}
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| {