feature: Teach the rust updater library about shorebird.yaml (#54)
This commit is contained in:
+6
-5
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user