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 a4bec065b9
commit b8aedb5cb3
8 changed files with 161 additions and 67 deletions
+20
View File
@@ -38,6 +38,26 @@ For more information, please refer to the documentation for each package.
If you're interested in contributing, please join us on
[Discord](https://discord.gg/9hKJcWGcaB).
### Environment setup
Working on Shorebird requires Dart and Rust.
We currently assume the Dart from the Flutter SDK on the 'stable' channel. Due
to the way the Dart compiler works, Shorebird requires an exact version of
Flutter/Dart to operate correctly today.
We currently assume Rust 1.67.0 or later, although the code is unlikely to be
sensitive to the exact version of Rust.
Once both are installed, `./scripts/bootstrap.sh` will run `pub get`
and `cargo check` for all packages in the repository.
### Running tests
We don't yet have a script to run tests locally. For now, you can run tests
manually by running `cargo test` in a Rust package directory or `dart test` in
a Dart package directory.
## License
Shorebird projects are licensed for use under either Apache License, Version 2.0
@@ -6,7 +6,6 @@ import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as path;
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
class _MockHttpClient extends Mock implements http.Client {}
+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| {