docs: Add ability to mark patches good/bad (not used yet) (#70)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user