diff --git a/cli/src/main.rs b/cli/src/main.rs index d692898..a6ba69a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -20,16 +20,17 @@ fn main() { let cli = Cli::parse(); let config = updater::AppConfig { - client_id: "demo".to_string(), cache_dir: "updater_cache".to_owned(), - base_url: Some("http://localhost:8000".to_owned()), - channel: Some("stable".to_owned()), - product_id: "demo".to_owned(), base_version: "0.1.0".to_owned(), original_libapp_path: "libapp.so".to_owned(), vm_path: "libflutter.so".to_owned(), }; - updater::init(config); + let yaml_str = " +product_id: demo +channel: stable +base_url: http://localhost:8000 +"; + updater::init(config, yaml_str); // You can check for the existence of subcommands, and if found use their // matches just as you would the top level cmd diff --git a/library/Cargo.toml b/library/Cargo.toml index df4b398..b07278c 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -24,6 +24,8 @@ anyhow = {version = "1.0.69", features = ["backtrace"]} # For error!(), info!(), etc macros. `print` will not show up on Android. log = "0.4.14" once_cell = "1.17.1" +serde_yaml = "0.9.19" +uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"]} [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13.0" diff --git a/library/include/updater.h b/library/include/updater.h index a308c0a..584cf1f 100644 --- a/library/include/updater.h +++ b/library/include/updater.h @@ -20,29 +20,11 @@ * NOTE: If this struct is changed all language bindings must be updated. */ typedef struct AppParameters { - /** - * Update channel name. Set to NULL or "eng" to disable updates. - */ - const char *channel; - /** - * Client ID, required. Typically a UUID, used for handling - * percentage rollouts. - */ - const char *client_id; - /** - * Product ID, required. Typically generated by Shorebird and included - * in your app to identify which app/channel/version triple to update. - */ - const char *product_id; /** * base_version, required. Named version of the app, off of which updates * are based. Can be either a version number or a hash. */ const char *base_version; - /** - * Update URL. Set to NULL to use the default update URL. - */ - const char *update_url; /** * Path to the original aot library, required. For Flutter apps this * is the path to the bundled libapp.so. May be used for compression @@ -67,7 +49,14 @@ typedef struct AppParameters { extern "C" { #endif // __cplusplus -SHOREBIRD_EXPORT void shorebird_init(const struct AppParameters *c_params); +/** + * 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. + */ +SHOREBIRD_EXPORT +void shorebird_init(const struct AppParameters *c_params, + const char *c_yaml); /** * Return the active version of the app, or NULL if there is no active version. diff --git a/library/src/c_api.rs b/library/src/c_api.rs index 5ce47e2..b17df0e 100644 --- a/library/src/c_api.rs +++ b/library/src/c_api.rs @@ -14,64 +14,50 @@ use crate::updater; /// NOTE: If this struct is changed all language bindings must be updated. #[repr(C)] pub struct AppParameters { - /// Update channel name. Set to NULL or "eng" to disable updates. - pub channel: *const libc::c_char, - /// Client ID, required. Typically a UUID, used for handling - /// percentage rollouts. - pub client_id: *const libc::c_char, - /// Product ID, required. Typically generated by Shorebird and included - /// in your app to identify which app/channel/version triple to update. - pub product_id: *const libc::c_char, /// base_version, required. Named version of the app, off of which updates /// are based. Can be either a version number or a hash. pub base_version: *const libc::c_char, - /// Update URL. Set to NULL to use the default update URL. - pub update_url: *const libc::c_char, + /// Path to the original aot library, required. For Flutter apps this /// is the path to the bundled libapp.so. May be used for compression /// downloaded artifacts. pub original_libapp_path: *const libc::c_char, + /// Path to the app's libflutter.so, required. May be used for ensuring /// downloaded artifacts are compatible with the Flutter/Dart versions /// used by the app. For Flutter apps this should be the path to the /// bundled libflutter.so. For Dart apps this should be the path to the /// dart executable. pub vm_path: *const libc::c_char, + /// Path to cache_dir where the updater will store downloaded artifacts. pub cache_dir: *const libc::c_char, } +fn to_rust(c_string: *const libc::c_char) -> String { + unsafe { CStr::from_ptr(c_string).to_str().unwrap() }.to_string() +} + fn app_config_from_c(c_params: *const AppParameters) -> updater::AppConfig { let c_params_ref = unsafe { &*c_params }; - fn required(c_string: *const libc::c_char) -> String { - unsafe { CStr::from_ptr(c_string).to_str().unwrap() }.to_string() - } - - fn optional(c_string: *const libc::c_char) -> Option { - if c_string == std::ptr::null() { - None - } else { - Some(required(c_string)) - } - } - updater::AppConfig { - client_id: required(c_params_ref.client_id), - cache_dir: required(c_params_ref.cache_dir), - channel: optional(c_params_ref.channel), - product_id: required(c_params_ref.product_id), - base_url: optional(c_params_ref.update_url), - base_version: required(c_params_ref.base_version), - original_libapp_path: required(c_params_ref.original_libapp_path), - vm_path: required(c_params_ref.vm_path), + cache_dir: to_rust(c_params_ref.cache_dir), + base_version: to_rust(c_params_ref.base_version), + original_libapp_path: to_rust(c_params_ref.original_libapp_path), + vm_path: to_rust(c_params_ref.vm_path), } } +/// 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. #[no_mangle] -pub extern "C" fn shorebird_init(c_params: *const AppParameters) { +pub extern "C" fn shorebird_init(c_params: *const AppParameters, c_yaml: *const libc::c_char) { let config = app_config_from_c(c_params); - updater::init(config); + + let yaml_string = to_rust(c_yaml); + updater::init(config, &yaml_string); } /// Return the active version of the app, or NULL if there is no active version. diff --git a/library/src/cache.rs b/library/src/cache.rs index 21977d4..9d5d5f1 100644 --- a/library/src/cache.rs +++ b/library/src/cache.rs @@ -3,6 +3,7 @@ use std::fs::File; use std::io::{BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; +use uuid::Uuid; use serde::{Deserialize, Serialize}; @@ -25,6 +26,12 @@ struct Slot { // anything inside should be done via the functions below. #[derive(Deserialize, Serialize)] pub struct UpdaterState { + // The purpose of the client_id is to allow for staged rollouts + // the server needs some sort of per-client number, so that it can bucket + // 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")] + client_id: Uuid, current_slot_index: usize, slots: Vec, // Add file path or FD so modifying functions can save it to disk? @@ -33,6 +40,7 @@ pub struct UpdaterState { impl Default for UpdaterState { fn default() -> Self { Self { + client_id: Uuid::new_v4(), current_slot_index: 0, slots: Vec::new(), } @@ -44,6 +52,8 @@ pub fn load_state(cache_dir: &str) -> anyhow::Result { let path = Path::new(cache_dir).join("state.json"); let file = File::open(path)?; let reader = BufReader::new(file); + // TODO: Now that we depend on serde_yaml for shorebird.yaml + // we could use yaml here instead of json. let state = serde_json::from_reader(reader)?; Ok(state) } @@ -58,7 +68,11 @@ pub fn save_state(state: &UpdaterState, cache_dir: &str) -> anyhow::Result<()> { Ok(()) } -pub fn current_patch_internal(state: &UpdaterState) -> Option { +pub fn client_id(state: &UpdaterState) -> String { + state.client_id.to_string() +} + +pub fn current_patch(state: &UpdaterState) -> Option { // If there is no state, return None. if state.slots.is_empty() { return None; diff --git a/library/src/config.rs b/library/src/config.rs index 1a06e74..c31bed1 100644 --- a/library/src/config.rs +++ b/library/src/config.rs @@ -3,6 +3,7 @@ use std::sync::Mutex; use crate::updater::AppConfig; +use crate::yaml::YamlConfig; use once_cell::sync::OnceCell; // cbindgen looks for const, ignore these so it doesn't warn about them. @@ -31,11 +32,11 @@ where return f(&lock); } +#[derive(Debug)] pub struct ResolvedConfig { is_initialized: bool, pub cache_dir: String, pub channel: String, - pub client_id: String, pub product_id: String, pub base_version: String, pub original_libapp_path: String, @@ -49,7 +50,6 @@ impl ResolvedConfig { is_initialized: false, cache_dir: String::new(), channel: String::new(), - client_id: String::new(), product_id: String::new(), base_version: String::new(), original_libapp_path: String::new(), @@ -59,27 +59,27 @@ impl ResolvedConfig { } } -pub fn set_config(config: AppConfig) { +pub fn set_config(config: AppConfig, yaml: YamlConfig) { // If there is no base_url, use the default. // If there is no channel, use the default. let mut lock = global_config() .lock() .expect("Failed to acquire updater lock."); - lock.base_url = config + lock.base_url = yaml .base_url .as_deref() .unwrap_or(DEFAULT_BASE_URL) .to_owned(); - lock.channel = config + lock.channel = yaml .channel .as_deref() .unwrap_or(DEFAULT_CHANNEL) .to_owned(); lock.cache_dir = config.cache_dir.to_string(); - lock.client_id = config.client_id.to_string(); - lock.product_id = config.product_id.to_string(); + lock.product_id = yaml.product_id.to_string(); lock.base_version = config.base_version.to_string(); lock.original_libapp_path = config.original_libapp_path.to_string(); lock.vm_path = config.vm_path.to_string(); lock.is_initialized = true; + info!("Updater configured with: {:?}", lock); } diff --git a/library/src/lib.rs b/library/src/lib.rs index 4f44cf3..c54b164 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -11,6 +11,7 @@ mod config; mod logging; mod network; mod updater; +mod yaml; // Take all public items from the updater namespace and make them public. pub use self::updater::*; diff --git a/library/src/network.rs b/library/src/network.rs index d204a17..d2db4f4 100644 --- a/library/src/network.rs +++ b/library/src/network.rs @@ -6,7 +6,7 @@ use std::string::ToString; use serde::Deserialize; -use crate::cache::PatchInfo; +use crate::cache::{client_id, current_patch, UpdaterState}; use crate::config::ResolvedConfig; fn patches_check_url(base_url: &str) -> String { @@ -29,7 +29,7 @@ pub struct PatchCheckResponse { pub fn send_patch_check_request( config: &ResolvedConfig, - patch: Option, + state: &UpdaterState, ) -> anyhow::Result { #[cfg(target_os = "macos")] static PLATFORM: &str = "macos"; @@ -47,10 +47,12 @@ pub fn send_patch_check_request( #[cfg(target_arch = "aarch64")] static ARCH: &str = "aarch64"; + let patch = current_patch(state); + // Send the request to the server. let client = reqwest::blocking::Client::new(); let mut body = HashMap::new(); - body.insert("client_id", config.client_id.clone()); + body.insert("client_id", client_id(state)); body.insert("product_id", config.product_id.clone()); body.insert("channel", config.channel.clone()); body.insert("base_version", config.base_version.clone()); diff --git a/library/src/updater.rs b/library/src/updater.rs index 533a7e5..5056631 100644 --- a/library/src/updater.rs +++ b/library/src/updater.rs @@ -3,12 +3,12 @@ use std::fmt::{Display, Formatter}; use crate::cache::{ - current_patch_internal, download_into_unused_slot, load_state, save_state, set_current_slot, - PatchInfo, + current_patch, download_into_unused_slot, load_state, save_state, set_current_slot, PatchInfo, }; use crate::config::{set_config, with_config, ResolvedConfig}; use crate::logging::init_logging; use crate::network::send_patch_check_request; +use crate::yaml::YamlConfig; pub enum UpdateStatus { NoUpdate, @@ -35,28 +35,28 @@ impl Display for UpdateStatus { // but making &str from CStr* is a bit of a pain. pub struct AppConfig { pub cache_dir: String, - pub channel: Option, // If None, use the 'stable'. - pub client_id: String, - pub product_id: String, pub base_version: String, pub original_libapp_path: String, pub vm_path: String, - pub base_url: Option, // If None, use the default. } -pub fn init(app_config: AppConfig) { +/// Initialize the updater library. +/// Takes a AppConfig struct and a yaml string. +/// The yaml string is the contents of the shorebird.yaml file. +/// The AppConfig struct is information about the running app and where +/// the updater should keep its cache. +pub fn init(app_config: AppConfig, yaml: &str) { init_logging(); - set_config(app_config); + let config = YamlConfig::from_yaml(&yaml).unwrap(); + set_config(app_config, config); } -pub fn check_for_update_internal(config: &ResolvedConfig) -> bool { +fn check_for_update_internal(config: &ResolvedConfig) -> bool { // Load UpdaterState from disk // If there is no state, make an empty state. let state = load_state(&config.cache_dir).unwrap_or_default(); - // Check the current slot. - let patch = current_patch_internal(&state); // Send info from app + current slot to server. - let response_result = send_patch_check_request(&config, patch); + let response_result = send_patch_check_request(&config, &state); match response_result { Err(err) => { error!("Failed update check: {err}"); @@ -68,6 +68,7 @@ pub fn check_for_update_internal(config: &ResolvedConfig) -> bool { } } +/// Synchronously checks for an update and returns true if an update is available. pub fn check_for_update() -> bool { return with_config(check_for_update_internal); } @@ -75,9 +76,8 @@ pub fn check_for_update() -> bool { fn update_internal(config: &ResolvedConfig) -> anyhow::Result { // Load the state from disk. let mut state = load_state(&config.cache_dir).unwrap_or_default(); - let version = current_patch_internal(&state); // Check for update. - let response = send_patch_check_request(&config, version)?; + let response = send_patch_check_request(&config, &state)?; if !response.patch_available { return Ok(UpdateStatus::NoUpdate); } @@ -90,13 +90,15 @@ fn update_internal(config: &ResolvedConfig) -> anyhow::Result { return Ok(UpdateStatus::UpdateInstalled); } +/// Reads the current patch from the cache and returns it. pub fn active_patch() -> Option { return with_config(|config| { let state = load_state(&config.cache_dir).unwrap_or_default(); - return current_patch_internal(&state); + return current_patch(&state); }); } +/// Synchronously checks for an update and downloads and installs it if available. pub fn update() -> UpdateStatus { return with_config(|config| { let result = update_internal(&config); diff --git a/library/src/yaml.rs b/library/src/yaml.rs new file mode 100644 index 0000000..2218f2c --- /dev/null +++ b/library/src/yaml.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; + +/// Struct for parsing shorebird.yaml. +#[derive(Deserialize)] +pub struct YamlConfig { + /// Product ID. Required. Generated by Shorebird and included + /// in your app to identify which app/channel/version triple to update. + pub product_id: String, + /// Update channel name. Defaults to "stable" if not set. + pub channel: Option, + /// Update URL. Defaults to the default update URL if not set. + pub base_url: Option, +} + +impl YamlConfig { + /// Read in shorebird.yaml from a string. + pub fn from_yaml(yaml: &str) -> Result { + serde_yaml::from_str(yaml) + } +}