feature: Teach the rust updater library about shorebird.yaml (#54)

This commit is contained in:
Eric Seidel
2023-03-10 21:49:19 -08:00
committed by GitHub
parent da702e4ce9
commit b18ac7b947
10 changed files with 99 additions and 82 deletions
+6 -5
View File
@@ -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
+2
View File
@@ -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"
+8 -19
View File
@@ -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.
+18 -32
View File
@@ -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<String> {
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.
+15 -1
View File
@@ -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<Slot>,
// 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<UpdaterState> {
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<PatchInfo> {
pub fn client_id(state: &UpdaterState) -> String {
state.client_id.to_string()
}
pub fn current_patch(state: &UpdaterState) -> Option<PatchInfo> {
// If there is no state, return None.
if state.slots.is_empty() {
return None;
+7 -7
View File
@@ -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);
}
+1
View File
@@ -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::*;
+5 -3
View File
@@ -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<PatchInfo>,
state: &UpdaterState,
) -> anyhow::Result<PatchCheckResponse> {
#[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());
+17 -15
View File
@@ -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<String>, // 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<String>, // 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<UpdateStatus> {
// 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<UpdateStatus> {
return Ok(UpdateStatus::UpdateInstalled);
}
/// Reads the current patch from the cache and returns it.
pub fn active_patch() -> Option<PatchInfo> {
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);
+20
View File
@@ -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<String>,
/// Update URL. Defaults to the default update URL if not set.
pub base_url: Option<String>,
}
impl YamlConfig {
/// Read in shorebird.yaml from a string.
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml)
}
}