feat(shorebird_code_push): track support (#232)
* feat(shorebird_code_push): track support * cleanup and add todos * docs * run ffigen * use c_char instead of char * Add channel support * tests * tests * update podfile.lock * Update example to include tracks selector --------- Co-authored-by: Bryan Oltman <bryan@shorebird.dev> Co-authored-by: Bryan Oltman <bryanoltman@gmail.com>
This commit is contained in:
@@ -151,9 +151,15 @@ SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string);
|
|||||||
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
|
SHOREBIRD_EXPORT void shorebird_free_update_result(struct UpdateResult *result);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for an update. Returns true if an update is available.
|
* Check for an update on the first non-null channel of:
|
||||||
|
* 1. `c_channel`
|
||||||
|
* 2. The channel specified in shorebird.yaml
|
||||||
|
* 3. The default "stable" channel
|
||||||
|
*
|
||||||
|
* Returns true if an update exists that has not yet been downloaded.
|
||||||
*/
|
*/
|
||||||
SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
SHOREBIRD_EXPORT
|
||||||
|
bool shorebird_check_for_downloadable_update(const char *c_channel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously download an update if one is available.
|
* Synchronously download an update if one is available.
|
||||||
@@ -161,10 +167,15 @@ SHOREBIRD_EXPORT bool shorebird_check_for_update(void);
|
|||||||
SHOREBIRD_EXPORT void shorebird_update(void);
|
SHOREBIRD_EXPORT void shorebird_update(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously download an update if one is available.
|
* Synchronously download an update on the first non-null channel of:
|
||||||
|
* 1. `c_channel`
|
||||||
|
* 2. The channel specified in shorebird.yaml
|
||||||
|
* 3. The default "stable" channel
|
||||||
|
*
|
||||||
* Returns an [UpdateResult] indicating whether the update was successful.
|
* Returns an [UpdateResult] indicating whether the update was successful.
|
||||||
*/
|
*/
|
||||||
SHOREBIRD_EXPORT const struct UpdateResult *shorebird_update_with_result(void);
|
SHOREBIRD_EXPORT
|
||||||
|
const struct UpdateResult *shorebird_update_with_result(const char *c_channel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a thread to download an update if one is available.
|
* Start a thread to download an update if one is available.
|
||||||
|
|||||||
+64
-20
@@ -68,6 +68,7 @@ pub struct UpdateResult {
|
|||||||
pub status: i32,
|
pub status: i32,
|
||||||
pub message: *const libc::c_char,
|
pub message: *const libc::c_char,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct FileCallbacks {
|
pub struct FileCallbacks {
|
||||||
@@ -94,6 +95,13 @@ fn to_rust(c_string: *const libc::c_char) -> anyhow::Result<String> {
|
|||||||
Ok(c_str.to_str()?.to_string())
|
Ok(c_str.to_str()?.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_rust_option(c_string: *const c_char) -> anyhow::Result<Option<String>> {
|
||||||
|
if c_string.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(to_rust(c_string)?))
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts a Rust string to a C string, caller must free the C string.
|
/// Converts a Rust string to a C string, caller must free the C string.
|
||||||
fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> {
|
fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> {
|
||||||
let c_str = CString::new(rust_string)?;
|
let c_str = CString::new(rust_string)?;
|
||||||
@@ -266,27 +274,47 @@ pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for an update. Returns true if an update is available.
|
/// Check for an update on the first non-null channel of:
|
||||||
|
/// 1. `c_channel`
|
||||||
|
/// 2. The channel specified in shorebird.yaml
|
||||||
|
/// 3. The default "stable" channel
|
||||||
|
///
|
||||||
|
/// Returns true if an update exists that has not yet been downloaded.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn shorebird_check_for_update() -> bool {
|
pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_char) -> bool {
|
||||||
log_on_error(updater::check_for_update, "checking for update", false)
|
log_on_error(
|
||||||
|
|| {
|
||||||
|
let channel = to_rust_option(c_channel)?;
|
||||||
|
updater::check_for_downloadable_update(channel.as_deref())
|
||||||
|
},
|
||||||
|
"checking for update",
|
||||||
|
false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronously download an update if one is available.
|
/// Synchronously download an update if one is available.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn shorebird_update() {
|
pub extern "C" fn shorebird_update() {
|
||||||
log_on_error(
|
log_on_error(
|
||||||
|| updater::update().map(|result| shorebird_info!("Update result: {}", result)),
|
|| updater::update(None).map(|result| shorebird_info!("Update result: {}", result)),
|
||||||
"downloading update",
|
"downloading update",
|
||||||
(),
|
(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronously download an update if one is available.
|
/// Synchronously download an update on the first non-null channel of:
|
||||||
|
/// 1. `c_channel`
|
||||||
|
/// 2. The channel specified in shorebird.yaml
|
||||||
|
/// 3. The default "stable" channel
|
||||||
|
///
|
||||||
/// Returns an [UpdateResult] indicating whether the update was successful.
|
/// Returns an [UpdateResult] indicating whether the update was successful.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn shorebird_update_with_result() -> *const UpdateResult {
|
pub extern "C" fn shorebird_update_with_result(c_channel: *const c_char) -> *const UpdateResult {
|
||||||
let result = to_update_result(updater::update());
|
let channel = to_rust_option(c_channel);
|
||||||
|
let result = match channel {
|
||||||
|
Ok(channel) => to_update_result(updater::update(channel.as_deref())),
|
||||||
|
Err(err) => to_update_result(Err(err)),
|
||||||
|
};
|
||||||
return Box::into_raw(Box::new(result));
|
return Box::into_raw(Box::new(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +550,10 @@ mod test {
|
|||||||
|
|
||||||
// set up the network hooks to return a patch.
|
// set up the network hooks to return a patch.
|
||||||
testing_set_network_hooks(
|
testing_set_network_hooks(
|
||||||
|_url, _request| {
|
|_url, request| {
|
||||||
|
// We didn't specify a channel in either the shorebird_check_for_downloadable_update
|
||||||
|
// call or the shorebird.yaml, so we should default to "stable".
|
||||||
|
assert_eq!(request.channel, "stable");
|
||||||
// Generated by `string_patch "hello world" "hello tests"`
|
// Generated by `string_patch "hello world" "hello tests"`
|
||||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||||
Ok(PatchCheckResponse {
|
Ok(PatchCheckResponse {
|
||||||
@@ -547,7 +578,7 @@ mod test {
|
|||||||
|_url, _event| Ok(()),
|
|_url, _event| Ok(()),
|
||||||
);
|
);
|
||||||
// There is an update available.
|
// There is an update available.
|
||||||
assert!(shorebird_check_for_update());
|
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
|
||||||
|
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
shorebird_update();
|
shorebird_update();
|
||||||
@@ -565,7 +596,7 @@ mod test {
|
|||||||
|
|
||||||
#[serial]
|
#[serial]
|
||||||
#[test]
|
#[test]
|
||||||
fn patch_success_with_result() {
|
fn patch_success_with_result() -> anyhow::Result<()> {
|
||||||
testing_reset_config();
|
testing_reset_config();
|
||||||
let tmp_dir = TempDir::new("example").unwrap();
|
let tmp_dir = TempDir::new("example").unwrap();
|
||||||
|
|
||||||
@@ -584,7 +615,8 @@ mod test {
|
|||||||
|
|
||||||
// set up the network hooks to return a patch.
|
// set up the network hooks to return a patch.
|
||||||
testing_set_network_hooks(
|
testing_set_network_hooks(
|
||||||
|_url, _request| {
|
|_url, request| {
|
||||||
|
assert_eq!(request.channel, "beta");
|
||||||
// Generated by `string_patch "hello world" "hello tests"`
|
// Generated by `string_patch "hello world" "hello tests"`
|
||||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||||
Ok(PatchCheckResponse {
|
Ok(PatchCheckResponse {
|
||||||
@@ -609,10 +641,12 @@ mod test {
|
|||||||
|_url, _event| Ok(()),
|
|_url, _event| Ok(()),
|
||||||
);
|
);
|
||||||
// There is an update available.
|
// There is an update available.
|
||||||
assert!(shorebird_check_for_update());
|
let channel_c_str = allocate_c_string("beta")?;
|
||||||
|
assert!(shorebird_check_for_downloadable_update(channel_c_str));
|
||||||
|
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
let result = shorebird_update_with_result();
|
let result = shorebird_update_with_result(channel_c_str);
|
||||||
|
unsafe { shorebird_free_string(channel_c_str) };
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED);
|
assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED);
|
||||||
@@ -627,6 +661,8 @@ mod test {
|
|||||||
unsafe { shorebird_free_string(c_path) };
|
unsafe { shorebird_free_string(c_path) };
|
||||||
let new = std::fs::read_to_string(path).unwrap();
|
let new = std::fs::read_to_string(path).unwrap();
|
||||||
assert_eq!(new, expected_new);
|
assert_eq!(new, expected_new);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serial]
|
#[serial]
|
||||||
@@ -661,7 +697,7 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
let result = shorebird_update_with_result();
|
let result = shorebird_update_with_result(std::ptr::null());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE);
|
assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE);
|
||||||
@@ -702,7 +738,7 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
let result = shorebird_update_with_result();
|
let result = shorebird_update_with_result(std::ptr::null());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||||
@@ -712,7 +748,7 @@ mod test {
|
|||||||
|
|
||||||
#[serial]
|
#[serial]
|
||||||
#[test]
|
#[test]
|
||||||
fn patch_download_failure_with_result() {
|
fn patch_download_failure_with_result() -> anyhow::Result<()> {
|
||||||
testing_reset_config();
|
testing_reset_config();
|
||||||
let tmp_dir = TempDir::new("example").unwrap();
|
let tmp_dir = TempDir::new("example").unwrap();
|
||||||
|
|
||||||
@@ -730,7 +766,11 @@ mod test {
|
|||||||
|
|
||||||
// set up the network hooks to return a patch.
|
// set up the network hooks to return a patch.
|
||||||
testing_set_network_hooks(
|
testing_set_network_hooks(
|
||||||
|_url, _request| {
|
|_url, request| {
|
||||||
|
// shorebird_update_with_result was called with the beta channel, ensure that is
|
||||||
|
// piped through to the network request.
|
||||||
|
assert_eq!(request.channel, "beta");
|
||||||
|
|
||||||
// Generated by `string_patch "hello world" "hello tests"`
|
// Generated by `string_patch "hello world" "hello tests"`
|
||||||
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45";
|
||||||
Ok(PatchCheckResponse {
|
Ok(PatchCheckResponse {
|
||||||
@@ -749,12 +789,16 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
let result = shorebird_update_with_result();
|
let channel_c_str = allocate_c_string("beta")?;
|
||||||
|
let result = shorebird_update_with_result(channel_c_str);
|
||||||
|
unsafe { shorebird_free_string(channel_c_str) };
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR);
|
||||||
shorebird_free_update_result(result as *mut UpdateResult);
|
shorebird_free_update_result(result as *mut UpdateResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serial]
|
#[serial]
|
||||||
@@ -806,7 +850,7 @@ mod test {
|
|||||||
assert_eq!(shorebird_current_boot_patch_number(), 0);
|
assert_eq!(shorebird_current_boot_patch_number(), 0);
|
||||||
|
|
||||||
// There is an update available.
|
// There is an update available.
|
||||||
assert!(shorebird_check_for_update());
|
assert!(shorebird_check_for_downloadable_update(std::ptr::null()));
|
||||||
// Go ahead and do the update.
|
// Go ahead and do the update.
|
||||||
shorebird_update();
|
shorebird_update();
|
||||||
|
|
||||||
@@ -913,7 +957,7 @@ mod test {
|
|||||||
shorebird_start_update_thread();
|
shorebird_start_update_thread();
|
||||||
// Wait for the thread to start.
|
// Wait for the thread to start.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
assert!(updater::update().is_err());
|
assert!(updater::update(None).is_err());
|
||||||
}
|
}
|
||||||
// Unlock the lock, and wait for the thread to finish.
|
// Unlock the lock, and wait for the thread to finish.
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
|||||||
+37
-22
@@ -242,12 +242,24 @@ pub fn should_auto_update() -> anyhow::Result<bool> {
|
|||||||
with_config(|config| Ok(config.auto_update))
|
with_config(|config| Ok(config.auto_update))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronously checks for an update and returns true if an update is available.
|
/// Synchronously checks for an update on the first non-null channel of:
|
||||||
pub fn check_for_update() -> anyhow::Result<bool> {
|
/// 1. `c_channel`
|
||||||
|
/// 2. The channel specified in shorebird.yaml
|
||||||
|
/// 3. The default "stable" channel
|
||||||
|
///
|
||||||
|
/// Returns true if an update is available for download. Will return false if the update is already
|
||||||
|
/// downloaded and ready to install.
|
||||||
|
pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result<bool> {
|
||||||
let (request, url, request_fn) = with_config(|config| {
|
let (request, url, request_fn) = with_config(|config| {
|
||||||
// Get the required info to make the request.
|
let mut config = config.clone();
|
||||||
|
|
||||||
|
match channel {
|
||||||
|
Some(channel) => config.channel = channel.to_string(),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
PatchCheckRequest::new(config),
|
PatchCheckRequest::new(&config),
|
||||||
patches_check_url(&config.base_url),
|
patches_check_url(&config.base_url),
|
||||||
config.network_hooks.patch_check_request_fn,
|
config.network_hooks.patch_check_request_fn,
|
||||||
))
|
))
|
||||||
@@ -319,7 +331,7 @@ fn copy_update_config() -> anyhow::Result<UpdateConfig> {
|
|||||||
|
|
||||||
// Callers must possess the Updater lock, but we don't care about the contents
|
// Callers must possess the Updater lock, but we don't care about the contents
|
||||||
// since they're empty.
|
// since they're empty.
|
||||||
fn update_internal(_: &UpdaterLockState) -> anyhow::Result<UpdateStatus> {
|
fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Result<UpdateStatus> {
|
||||||
// Only one copy of Update can be running at a time.
|
// Only one copy of Update can be running at a time.
|
||||||
// Update will take the global Updater lock.
|
// Update will take the global Updater lock.
|
||||||
// Update will need to take the Config lock at times, but will only
|
// Update will need to take the Config lock at times, but will only
|
||||||
@@ -337,7 +349,10 @@ fn update_internal(_: &UpdaterLockState) -> anyhow::Result<UpdateStatus> {
|
|||||||
// Takes Config lock and installs patch.
|
// Takes Config lock and installs patch.
|
||||||
// Saves state to disk (holds Config lock while writing).
|
// Saves state to disk (holds Config lock while writing).
|
||||||
|
|
||||||
let config = copy_update_config()?;
|
let mut config = copy_update_config()?;
|
||||||
|
if channel.is_some() {
|
||||||
|
config.channel = channel.unwrap().to_string();
|
||||||
|
}
|
||||||
|
|
||||||
// We discard any events if we have more than 3 queued to make sure
|
// We discard any events if we have more than 3 queued to make sure
|
||||||
// we don't stall the client.
|
// we don't stall the client.
|
||||||
@@ -460,8 +475,8 @@ fn should_install_patch(patch_number: usize) -> Result<ShouldInstallPatchCheckRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronously checks for an update and downloads and installs it if available.
|
/// Synchronously checks for an update and downloads and installs it if available.
|
||||||
pub fn update() -> anyhow::Result<UpdateStatus> {
|
pub fn update(channel: Option<&str>) -> anyhow::Result<UpdateStatus> {
|
||||||
with_updater_thread_lock(update_internal)
|
with_updater_thread_lock(|lock_state| update_internal(lock_state, channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a path to a patch file, and a base file, apply the patch to the base
|
/// Given a path to a patch file, and a base file, apply the patch to the base
|
||||||
@@ -642,7 +657,7 @@ pub fn report_launch_success() -> anyhow::Result<()> {
|
|||||||
/// and install it if available.
|
/// and install it if available.
|
||||||
pub fn start_update_thread() {
|
pub fn start_update_thread() {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = update();
|
let result = update(None);
|
||||||
let status = match result {
|
let status = match result {
|
||||||
Ok(status) => status,
|
Ok(status) => status,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -949,7 +964,7 @@ mod tests {
|
|||||||
let apk_path = tmp_dir.path().join("base.apk");
|
let apk_path = tmp_dir.path().join("base.apk");
|
||||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||||
|
|
||||||
let result = super::update()?;
|
let result = super::update(None)?;
|
||||||
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
||||||
|
|
||||||
// This is gross.
|
// This is gross.
|
||||||
@@ -1011,7 +1026,7 @@ mod tests {
|
|||||||
let apk_path = tmp_dir.path().join("base.apk");
|
let apk_path = tmp_dir.path().join("base.apk");
|
||||||
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes());
|
||||||
|
|
||||||
let result = super::update()?;
|
let result = super::update(None)?;
|
||||||
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
assert_eq!(result, crate::UpdateStatus::UpdateInstalled);
|
||||||
|
|
||||||
// This is gross.
|
// This is gross.
|
||||||
@@ -1147,7 +1162,7 @@ mod tests {
|
|||||||
// Make sure we're starting with no next boot patch.
|
// Make sure we're starting with no next boot patch.
|
||||||
assert!(updater_state.next_boot_patch().is_none());
|
assert!(updater_state.next_boot_patch().is_none());
|
||||||
|
|
||||||
let result = super::update()?;
|
let result = super::update(None)?;
|
||||||
|
|
||||||
// Ensure that we've skipped the known bad patch.
|
// Ensure that we've skipped the known bad patch.
|
||||||
assert_eq!(result, crate::UpdateStatus::UpdateIsBadPatch);
|
assert_eq!(result, crate::UpdateStatus::UpdateIsBadPatch);
|
||||||
@@ -1182,7 +1197,7 @@ mod tests {
|
|||||||
|
|
||||||
install_fake_patch(patch_number)?;
|
install_fake_patch(patch_number)?;
|
||||||
|
|
||||||
let update_status = super::update()?;
|
let update_status = super::update(None)?;
|
||||||
|
|
||||||
assert_eq!(update_status, crate::UpdateStatus::NoUpdate);
|
assert_eq!(update_status, crate::UpdateStatus::NoUpdate);
|
||||||
|
|
||||||
@@ -1242,7 +1257,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
super::update().unwrap();
|
super::update(None).unwrap();
|
||||||
// Only 3 events should have been sent.
|
// Only 3 events should have been sent.
|
||||||
event_mock.expect(3).assert();
|
event_mock.expect(3).assert();
|
||||||
|
|
||||||
@@ -1296,7 +1311,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Invoke check_for_update to kick off a patch check request
|
// Invoke check_for_update to kick off a patch check request
|
||||||
let _ = std::thread::spawn(crate::check_for_update);
|
let _ = std::thread::spawn(|| crate::check_for_downloadable_update(None));
|
||||||
|
|
||||||
// Call with_config to get the config lock. This should complete before the patch check request is resolved.
|
// Call with_config to get the config lock. This should complete before the patch check request is resolved.
|
||||||
let config_thread = std::thread::spawn(|| with_config(|_| Ok(())));
|
let config_thread = std::thread::spawn(|| with_config(|_| Ok(())));
|
||||||
@@ -1358,7 +1373,7 @@ mod rollback_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let update_result = crate::update();
|
let update_result = crate::update(None);
|
||||||
assert_eq!(update_result.unwrap(), crate::UpdateStatus::NoUpdate);
|
assert_eq!(update_result.unwrap(), crate::UpdateStatus::NoUpdate);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1393,7 +1408,7 @@ mod rollback_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
crate::update()?;
|
crate::update(None)?;
|
||||||
|
|
||||||
with_mut_state(|state| {
|
with_mut_state(|state| {
|
||||||
assert!(state.next_boot_patch().is_none());
|
assert!(state.next_boot_patch().is_none());
|
||||||
@@ -1462,7 +1477,7 @@ mod rollback_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let update_result = crate::update();
|
let update_result = crate::update(None);
|
||||||
assert_eq!(update_result.unwrap(), crate::UpdateStatus::UpdateInstalled);
|
assert_eq!(update_result.unwrap(), crate::UpdateStatus::UpdateInstalled);
|
||||||
|
|
||||||
with_mut_state(|state| {
|
with_mut_state(|state| {
|
||||||
@@ -1475,7 +1490,7 @@ mod rollback_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod check_for_update_tests {
|
mod check_for_downloadable_update_tests {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
@@ -1518,7 +1533,7 @@ mod check_for_update_tests {
|
|||||||
|
|
||||||
install_fake_patch(patch_number)?;
|
install_fake_patch(patch_number)?;
|
||||||
|
|
||||||
let is_update_available = crate::check_for_update()?;
|
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||||
assert!(!is_update_available);
|
assert!(!is_update_available);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1536,7 +1551,7 @@ mod check_for_update_tests {
|
|||||||
report_launch_start()?;
|
report_launch_start()?;
|
||||||
report_launch_failure()?;
|
report_launch_failure()?;
|
||||||
|
|
||||||
let is_update_available = crate::check_for_update()?;
|
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||||
assert!(!is_update_available);
|
assert!(!is_update_available);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1550,7 +1565,7 @@ mod check_for_update_tests {
|
|||||||
let tmp_dir = TempDir::new("example").unwrap();
|
let tmp_dir = TempDir::new("example").unwrap();
|
||||||
init_for_testing(&tmp_dir, Some(&server.url()));
|
init_for_testing(&tmp_dir, Some(&server.url()));
|
||||||
|
|
||||||
let is_update_available = crate::check_for_update()?;
|
let is_update_available = crate::check_for_downloadable_update(None)?;
|
||||||
assert!(is_update_available);
|
assert!(is_update_available);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.16.1
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MyHomePage extends StatefulWidget {
|
|||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
final _updater = ShorebirdUpdater();
|
final _updater = ShorebirdUpdater();
|
||||||
late final bool _isUpdaterAvailable;
|
late final bool _isUpdaterAvailable;
|
||||||
|
var _currentTrack = UpdateTrack.stable;
|
||||||
var _isCheckingForUpdates = false;
|
var _isCheckingForUpdates = false;
|
||||||
Patch? _currentPatch;
|
Patch? _currentPatch;
|
||||||
|
|
||||||
@@ -54,10 +55,20 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
try {
|
try {
|
||||||
setState(() => _isCheckingForUpdates = true);
|
setState(() => _isCheckingForUpdates = true);
|
||||||
// Check if there's an update available.
|
// Check if there's an update available.
|
||||||
final status = await _updater.checkForUpdate();
|
final status = await _updater.checkForUpdate(track: _currentTrack);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// If there is an update available, show a banner.
|
// If there is an update available, show a banner.
|
||||||
if (status == UpdateStatus.outdated) _showUpdateAvailableBanner();
|
switch (status) {
|
||||||
|
case UpdateStatus.upToDate:
|
||||||
|
_showNoUpdateAvailableBanner();
|
||||||
|
case UpdateStatus.outdated:
|
||||||
|
_showUpdateAvailableBanner();
|
||||||
|
case UpdateStatus.restartRequired:
|
||||||
|
_showRestartBanner();
|
||||||
|
case UpdateStatus.unavailable:
|
||||||
|
// Do nothing, there is already a warning displayed at the top of the
|
||||||
|
// screen.
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If an error occurs, we log it for now.
|
// If an error occurs, we log it for now.
|
||||||
debugPrint('Error checking for update: $error');
|
debugPrint('Error checking for update: $error');
|
||||||
@@ -88,7 +99,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
..hideCurrentMaterialBanner()
|
..hideCurrentMaterialBanner()
|
||||||
..showMaterialBanner(
|
..showMaterialBanner(
|
||||||
MaterialBanner(
|
MaterialBanner(
|
||||||
content: const Text('Update available'),
|
content: Text(
|
||||||
|
'Update available for the ${_currentTrack.name} track.',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -104,6 +117,26 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showNoUpdateAvailableBanner() {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentMaterialBanner()
|
||||||
|
..showMaterialBanner(
|
||||||
|
MaterialBanner(
|
||||||
|
content: Text(
|
||||||
|
'No update available on the ${_currentTrack.name} track.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
|
||||||
|
},
|
||||||
|
child: const Text('Dismiss'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showRestartBanner() {
|
void _showRestartBanner() {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..hideCurrentMaterialBanner()
|
..hideCurrentMaterialBanner()
|
||||||
@@ -145,8 +178,10 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
Future<void> _downloadUpdate() async {
|
Future<void> _downloadUpdate() async {
|
||||||
_showDownloadingBanner();
|
_showDownloadingBanner();
|
||||||
try {
|
try {
|
||||||
// Perform the update (e.g download the latest patch).
|
// Perform the update (e.g download the latest patch on [_currentTrack]).
|
||||||
await _updater.update();
|
// Note that [track] is optional. Not passing it will default to the
|
||||||
|
// stable track.
|
||||||
|
await _updater.update(track: _currentTrack);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// Show a banner to inform the user that the update is ready and that they
|
// Show a banner to inform the user that the update is ready and that they
|
||||||
// need to restart the app.
|
// need to restart the app.
|
||||||
@@ -166,9 +201,21 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
backgroundColor: theme.colorScheme.inversePrimary,
|
backgroundColor: theme.colorScheme.inversePrimary,
|
||||||
title: const Text('Shorebird Code Push'),
|
title: const Text('Shorebird Code Push'),
|
||||||
),
|
),
|
||||||
body: _isUpdaterAvailable
|
body: Column(
|
||||||
? _CurrentPatchVersion(patch: _currentPatch)
|
children: [
|
||||||
: const _ShorebirdUnavailable(),
|
if (!_isUpdaterAvailable) const _ShorebirdUnavailable(),
|
||||||
|
const Spacer(),
|
||||||
|
_CurrentPatchVersion(patch: _currentPatch),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_TrackPicker(
|
||||||
|
currentTrack: _currentTrack,
|
||||||
|
onChanged: (track) {
|
||||||
|
setState(() => _currentTrack = track);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: _isCheckingForUpdates ? null : _checkForUpdate,
|
onPressed: _isCheckingForUpdates ? null : _checkForUpdate,
|
||||||
tooltip: 'Check for update',
|
tooltip: 'Check for update',
|
||||||
@@ -224,6 +271,47 @@ class _CurrentPatchVersion extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widget that allows selection of update track.
|
||||||
|
class _TrackPicker extends StatelessWidget {
|
||||||
|
const _TrackPicker({
|
||||||
|
required this.currentTrack,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final UpdateTrack currentTrack;
|
||||||
|
|
||||||
|
final ValueChanged<UpdateTrack> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text('Update track:'),
|
||||||
|
SegmentedButton<UpdateTrack>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
label: Text('Stable'),
|
||||||
|
value: UpdateTrack.stable,
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
label: Text('Beta'),
|
||||||
|
icon: Icon(Icons.science),
|
||||||
|
value: UpdateTrack.beta,
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
label: Text('Staging'),
|
||||||
|
icon: Icon(Icons.construction),
|
||||||
|
value: UpdateTrack.staging,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {currentTrack},
|
||||||
|
onSelectionChanged: (tracks) => onChanged(tracks.single),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable loading indicator.
|
/// A reusable loading indicator.
|
||||||
class _LoadingIndicator extends StatelessWidget {
|
class _LoadingIndicator extends StatelessWidget {
|
||||||
const _LoadingIndicator();
|
const _LoadingIndicator();
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export 'src/shorebird_updater.dart'
|
|||||||
ShorebirdUpdater,
|
ShorebirdUpdater,
|
||||||
UpdateException,
|
UpdateException,
|
||||||
UpdateFailureReason,
|
UpdateFailureReason,
|
||||||
UpdateStatus;
|
UpdateStatus,
|
||||||
|
UpdateTrack;
|
||||||
|
|||||||
@@ -189,13 +189,13 @@ class UpdaterBindings {
|
|||||||
_waitpidPtr.asFunction<int Function(int, ffi.Pointer<ffi.Int>, int)>();
|
_waitpidPtr.asFunction<int Function(int, ffi.Pointer<ffi.Int>, int)>();
|
||||||
|
|
||||||
int waitid(
|
int waitid(
|
||||||
idtype_t arg0,
|
int arg0,
|
||||||
Dart__uint32_t arg1,
|
int arg1,
|
||||||
ffi.Pointer<siginfo_t> arg2,
|
ffi.Pointer<siginfo_t> arg2,
|
||||||
int arg3,
|
int arg3,
|
||||||
) {
|
) {
|
||||||
return _waitid(
|
return _waitid(
|
||||||
arg0.value,
|
arg0,
|
||||||
arg1,
|
arg1,
|
||||||
arg2,
|
arg2,
|
||||||
arg3,
|
arg3,
|
||||||
@@ -204,8 +204,8 @@ class UpdaterBindings {
|
|||||||
|
|
||||||
late final _waitidPtr = _lookup<
|
late final _waitidPtr = _lookup<
|
||||||
ffi.NativeFunction<
|
ffi.NativeFunction<
|
||||||
ffi.Int Function(ffi.UnsignedInt, id_t, ffi.Pointer<siginfo_t>,
|
ffi.Int Function(
|
||||||
ffi.Int)>>('waitid');
|
ffi.Int32, id_t, ffi.Pointer<siginfo_t>, ffi.Int)>>('waitid');
|
||||||
late final _waitid = _waitidPtr
|
late final _waitid = _waitidPtr
|
||||||
.asFunction<int Function(int, int, ffi.Pointer<siginfo_t>, int)>();
|
.asFunction<int Function(int, int, ffi.Pointer<siginfo_t>, int)>();
|
||||||
|
|
||||||
@@ -2456,16 +2456,26 @@ class UpdaterBindings {
|
|||||||
late final _shorebird_free_update_result = _shorebird_free_update_resultPtr
|
late final _shorebird_free_update_result = _shorebird_free_update_resultPtr
|
||||||
.asFunction<void Function(ffi.Pointer<UpdateResult>)>();
|
.asFunction<void Function(ffi.Pointer<UpdateResult>)>();
|
||||||
|
|
||||||
/// Check for an update. Returns true if an update is available.
|
/// Check for an update on the first non-null channel of:
|
||||||
bool shorebird_check_for_update() {
|
/// 1. `c_channel`
|
||||||
return _shorebird_check_for_update();
|
/// 2. The channel specified in shorebird.yaml
|
||||||
|
/// 3. The default "stable" channel
|
||||||
|
///
|
||||||
|
/// Returns true if an update exists that has not yet been downloaded.
|
||||||
|
bool shorebird_check_for_downloadable_update(
|
||||||
|
ffi.Pointer<ffi.Char> c_channel,
|
||||||
|
) {
|
||||||
|
return _shorebird_check_for_downloadable_update(
|
||||||
|
c_channel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _shorebird_check_for_updatePtr =
|
late final _shorebird_check_for_downloadable_updatePtr =
|
||||||
_lookup<ffi.NativeFunction<ffi.Bool Function()>>(
|
_lookup<ffi.NativeFunction<ffi.Bool Function(ffi.Pointer<ffi.Char>)>>(
|
||||||
'shorebird_check_for_update');
|
'shorebird_check_for_downloadable_update');
|
||||||
late final _shorebird_check_for_update =
|
late final _shorebird_check_for_downloadable_update =
|
||||||
_shorebird_check_for_updatePtr.asFunction<bool Function()>();
|
_shorebird_check_for_downloadable_updatePtr
|
||||||
|
.asFunction<bool Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
/// Synchronously download an update if one is available.
|
/// Synchronously download an update if one is available.
|
||||||
void shorebird_update() {
|
void shorebird_update() {
|
||||||
@@ -2477,17 +2487,26 @@ class UpdaterBindings {
|
|||||||
late final _shorebird_update =
|
late final _shorebird_update =
|
||||||
_shorebird_updatePtr.asFunction<void Function()>();
|
_shorebird_updatePtr.asFunction<void Function()>();
|
||||||
|
|
||||||
/// Synchronously download an update if one is available.
|
/// Synchronously download an update on the first non-null channel of:
|
||||||
|
/// 1. `c_channel`
|
||||||
|
/// 2. The channel specified in shorebird.yaml
|
||||||
|
/// 3. The default "stable" channel
|
||||||
|
///
|
||||||
/// Returns an [UpdateResult] indicating whether the update was successful.
|
/// Returns an [UpdateResult] indicating whether the update was successful.
|
||||||
ffi.Pointer<UpdateResult> shorebird_update_with_result() {
|
ffi.Pointer<UpdateResult> shorebird_update_with_result(
|
||||||
return _shorebird_update_with_result();
|
ffi.Pointer<ffi.Char> c_channel,
|
||||||
|
) {
|
||||||
|
return _shorebird_update_with_result(
|
||||||
|
c_channel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _shorebird_update_with_resultPtr =
|
late final _shorebird_update_with_resultPtr = _lookup<
|
||||||
_lookup<ffi.NativeFunction<ffi.Pointer<UpdateResult> Function()>>(
|
ffi.NativeFunction<
|
||||||
'shorebird_update_with_result');
|
ffi.Pointer<UpdateResult> Function(
|
||||||
|
ffi.Pointer<ffi.Char>)>>('shorebird_update_with_result');
|
||||||
late final _shorebird_update_with_result = _shorebird_update_with_resultPtr
|
late final _shorebird_update_with_result = _shorebird_update_with_resultPtr
|
||||||
.asFunction<ffi.Pointer<UpdateResult> Function()>();
|
.asFunction<ffi.Pointer<UpdateResult> Function(ffi.Pointer<ffi.Char>)>();
|
||||||
|
|
||||||
/// Start a thread to download an update if one is available.
|
/// Start a thread to download an update if one is available.
|
||||||
void shorebird_start_update_thread() {
|
void shorebird_start_update_thread() {
|
||||||
@@ -2640,20 +2659,10 @@ final class _opaque_pthread_t extends ffi.Struct {
|
|||||||
external ffi.Array<ffi.Char> __opaque;
|
external ffi.Array<ffi.Char> __opaque;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum idtype_t {
|
abstract class idtype_t {
|
||||||
P_ALL(0),
|
static const int P_ALL = 0;
|
||||||
P_PID(1),
|
static const int P_PID = 1;
|
||||||
P_PGID(2);
|
static const int P_PGID = 2;
|
||||||
|
|
||||||
final int value;
|
|
||||||
const idtype_t(this.value);
|
|
||||||
|
|
||||||
static idtype_t fromValue(int value) => switch (value) {
|
|
||||||
0 => P_ALL,
|
|
||||||
1 => P_PID,
|
|
||||||
2 => P_PGID,
|
|
||||||
_ => throw ArgumentError("Unknown value for idtype_t: $value"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class __darwin_arm_exception_state extends ffi.Struct {
|
final class __darwin_arm_exception_state extends ffi.Struct {
|
||||||
@@ -4060,6 +4069,8 @@ const int __MAC_14_5 = 140500;
|
|||||||
|
|
||||||
const int __MAC_15_0 = 150000;
|
const int __MAC_15_0 = 150000;
|
||||||
|
|
||||||
|
const int __MAC_15_1 = 150100;
|
||||||
|
|
||||||
const int __IPHONE_2_0 = 20000;
|
const int __IPHONE_2_0 = 20000;
|
||||||
|
|
||||||
const int __IPHONE_2_1 = 20100;
|
const int __IPHONE_2_1 = 20100;
|
||||||
@@ -4220,6 +4231,8 @@ const int __IPHONE_17_5 = 170500;
|
|||||||
|
|
||||||
const int __IPHONE_18_0 = 180000;
|
const int __IPHONE_18_0 = 180000;
|
||||||
|
|
||||||
|
const int __IPHONE_18_1 = 180100;
|
||||||
|
|
||||||
const int __WATCHOS_1_0 = 10000;
|
const int __WATCHOS_1_0 = 10000;
|
||||||
|
|
||||||
const int __WATCHOS_2_0 = 20000;
|
const int __WATCHOS_2_0 = 20000;
|
||||||
@@ -4316,6 +4329,8 @@ const int __WATCHOS_10_5 = 100500;
|
|||||||
|
|
||||||
const int __WATCHOS_11_0 = 110000;
|
const int __WATCHOS_11_0 = 110000;
|
||||||
|
|
||||||
|
const int __WATCHOS_11_1 = 110100;
|
||||||
|
|
||||||
const int __TVOS_9_0 = 90000;
|
const int __TVOS_9_0 = 90000;
|
||||||
|
|
||||||
const int __TVOS_9_1 = 90100;
|
const int __TVOS_9_1 = 90100;
|
||||||
@@ -4414,6 +4429,8 @@ const int __TVOS_17_5 = 170500;
|
|||||||
|
|
||||||
const int __TVOS_18_0 = 180000;
|
const int __TVOS_18_0 = 180000;
|
||||||
|
|
||||||
|
const int __TVOS_18_1 = 180100;
|
||||||
|
|
||||||
const int __BRIDGEOS_2_0 = 20000;
|
const int __BRIDGEOS_2_0 = 20000;
|
||||||
|
|
||||||
const int __BRIDGEOS_3_0 = 30000;
|
const int __BRIDGEOS_3_0 = 30000;
|
||||||
@@ -4468,6 +4485,8 @@ const int __BRIDGEOS_8_5 = 80500;
|
|||||||
|
|
||||||
const int __BRIDGEOS_9_0 = 90000;
|
const int __BRIDGEOS_9_0 = 90000;
|
||||||
|
|
||||||
|
const int __BRIDGEOS_9_1 = 90100;
|
||||||
|
|
||||||
const int __DRIVERKIT_19_0 = 190000;
|
const int __DRIVERKIT_19_0 = 190000;
|
||||||
|
|
||||||
const int __DRIVERKIT_20_0 = 200000;
|
const int __DRIVERKIT_20_0 = 200000;
|
||||||
@@ -4496,6 +4515,8 @@ const int __DRIVERKIT_23_5 = 230500;
|
|||||||
|
|
||||||
const int __DRIVERKIT_24_0 = 240000;
|
const int __DRIVERKIT_24_0 = 240000;
|
||||||
|
|
||||||
|
const int __DRIVERKIT_24_1 = 240100;
|
||||||
|
|
||||||
const int __VISIONOS_1_0 = 10000;
|
const int __VISIONOS_1_0 = 10000;
|
||||||
|
|
||||||
const int __VISIONOS_1_1 = 10100;
|
const int __VISIONOS_1_1 = 10100;
|
||||||
@@ -4504,6 +4525,8 @@ const int __VISIONOS_1_2 = 10200;
|
|||||||
|
|
||||||
const int __VISIONOS_2_0 = 20000;
|
const int __VISIONOS_2_0 = 20000;
|
||||||
|
|
||||||
|
const int __VISIONOS_2_1 = 20100;
|
||||||
|
|
||||||
const int MAC_OS_X_VERSION_10_0 = 1000;
|
const int MAC_OS_X_VERSION_10_0 = 1000;
|
||||||
|
|
||||||
const int MAC_OS_X_VERSION_10_1 = 1010;
|
const int MAC_OS_X_VERSION_10_1 = 1010;
|
||||||
@@ -4628,9 +4651,11 @@ const int MAC_OS_VERSION_14_5 = 140500;
|
|||||||
|
|
||||||
const int MAC_OS_VERSION_15_0 = 150000;
|
const int MAC_OS_VERSION_15_0 = 150000;
|
||||||
|
|
||||||
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 140000;
|
const int MAC_OS_VERSION_15_1 = 150100;
|
||||||
|
|
||||||
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150000;
|
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000;
|
||||||
|
|
||||||
|
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100;
|
||||||
|
|
||||||
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;
|
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,7 @@ abstract class ShorebirdUpdater {
|
|||||||
/// Checks for available updates and returns the [UpdateStatus].
|
/// Checks for available updates and returns the [UpdateStatus].
|
||||||
/// This method should be used to determine the update status before calling
|
/// This method should be used to determine the update status before calling
|
||||||
/// [update].
|
/// [update].
|
||||||
/// Returns `null` if the updater is not available.
|
Future<UpdateStatus> checkForUpdate({UpdateTrack? track});
|
||||||
Future<UpdateStatus?> checkForUpdate();
|
|
||||||
|
|
||||||
/// Updates the app to the latest patch (if available).
|
/// Updates the app to the latest patch (if available).
|
||||||
/// Future will complete once the update is fully downloaded and ready
|
/// Future will complete once the update is fully downloaded and ready
|
||||||
@@ -136,5 +135,17 @@ abstract class ShorebirdUpdater {
|
|||||||
/// * [isAvailable], which indicates whether the updater is available.
|
/// * [isAvailable], which indicates whether the updater is available.
|
||||||
/// * [checkForUpdate], which should be called to check if an update is
|
/// * [checkForUpdate], which should be called to check if an update is
|
||||||
/// available before calling this method.
|
/// available before calling this method.
|
||||||
Future<void> update();
|
Future<void> update({UpdateTrack? track});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track to check for updates on.
|
||||||
|
enum UpdateTrack {
|
||||||
|
/// The staging track used for internal testing.
|
||||||
|
staging,
|
||||||
|
|
||||||
|
/// The beta track used for public testing.
|
||||||
|
beta,
|
||||||
|
|
||||||
|
/// The stable track used for general availability.
|
||||||
|
stable,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,12 +66,16 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UpdateStatus> checkForUpdate() async {
|
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async {
|
||||||
if (!_isAvailable) return UpdateStatus.unavailable;
|
if (!_isAvailable) return UpdateStatus.unavailable;
|
||||||
|
|
||||||
final isUpdateAvailable = await _run(_updater.checkForUpdate);
|
// First, check to see whether an update is available for download.
|
||||||
|
final isUpdateAvailable =
|
||||||
|
await _run(() => _updater.checkForDownloadableUpdate(track: track));
|
||||||
if (isUpdateAvailable) return UpdateStatus.outdated;
|
if (isUpdateAvailable) return UpdateStatus.outdated;
|
||||||
|
|
||||||
|
// If no new update is available for download, see if a new patch exists
|
||||||
|
// on disk that requires a restart.
|
||||||
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
|
final (current, next) = await (readCurrentPatch(), readNextPatch()).wait;
|
||||||
return next != null && current?.number != next.number
|
return next != null && current?.number != next.number
|
||||||
? UpdateStatus.restartRequired
|
? UpdateStatus.restartRequired
|
||||||
@@ -79,13 +83,13 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> update() async {
|
Future<void> update({UpdateTrack? track}) async {
|
||||||
if (!_isAvailable) return;
|
if (!_isAvailable) return;
|
||||||
|
|
||||||
Pointer<UpdateResult> result = nullptr;
|
Pointer<UpdateResult> result = nullptr;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await _run(_updater.update);
|
result = await _run(() => _updater.update(track: track));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return _legacyFallback();
|
return _legacyFallback();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater {
|
|||||||
Future<Patch?> readNextPatch() async => null;
|
Future<Patch?> readNextPatch() async => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<UpdateStatus> checkForUpdate() async => UpdateStatus.unavailable;
|
Future<UpdateStatus> checkForUpdate({UpdateTrack? track}) async =>
|
||||||
|
UpdateStatus.unavailable;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> update() async {}
|
Future<void> update({UpdateTrack? track}) async {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:ffi' as ffi;
|
import 'dart:ffi' as ffi;
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||||
|
import 'package:shorebird_code_push/src/shorebird_updater.dart';
|
||||||
|
|
||||||
/// {@template updater}
|
/// {@template updater}
|
||||||
/// A wrapper around the generated [UpdaterBindings] that, when necessary,
|
/// A wrapper around the generated [UpdaterBindings] that, when necessary,
|
||||||
@@ -20,9 +22,6 @@ class Updater {
|
|||||||
/// The currently active patch number.
|
/// The currently active patch number.
|
||||||
int currentPatchNumber() => bindings.shorebird_current_boot_patch_number();
|
int currentPatchNumber() => bindings.shorebird_current_boot_patch_number();
|
||||||
|
|
||||||
/// Whether a new patch is available.
|
|
||||||
bool checkForUpdate() => bindings.shorebird_check_for_update();
|
|
||||||
|
|
||||||
/// The next patch number that will be loaded. Will be the same as
|
/// The next patch number that will be loaded. Will be the same as
|
||||||
/// currentPatchNumber if no new patch is available.
|
/// currentPatchNumber if no new patch is available.
|
||||||
int nextPatchNumber() => bindings.shorebird_next_boot_patch_number();
|
int nextPatchNumber() => bindings.shorebird_next_boot_patch_number();
|
||||||
@@ -30,9 +29,20 @@ class Updater {
|
|||||||
/// Downloads the latest patch, if available.
|
/// Downloads the latest patch, if available.
|
||||||
void downloadUpdate() => bindings.shorebird_update();
|
void downloadUpdate() => bindings.shorebird_update();
|
||||||
|
|
||||||
|
// New Methods added to support v2.0.0 of the Dart APIs //
|
||||||
|
|
||||||
|
/// Whether a new patch is available for download.
|
||||||
|
bool checkForDownloadableUpdate({UpdateTrack? track}) =>
|
||||||
|
bindings.shorebird_check_for_downloadable_update(
|
||||||
|
track == null ? ffi.nullptr : track.name.toNativeUtf8().cast<Char>(),
|
||||||
|
);
|
||||||
|
|
||||||
/// Downloads the latest patch, if available and returns an [UpdateResult]
|
/// Downloads the latest patch, if available and returns an [UpdateResult]
|
||||||
/// to indicate whether the update was successful.
|
/// to indicate whether the update was successful.
|
||||||
Pointer<UpdateResult> update() => bindings.shorebird_update_with_result();
|
Pointer<UpdateResult> update({UpdateTrack? track}) =>
|
||||||
|
bindings.shorebird_update_with_result(
|
||||||
|
track == null ? ffi.nullptr : track.name.toNativeUtf8().cast<Char>(),
|
||||||
|
);
|
||||||
|
|
||||||
/// Frees an update result allocated by the updater.
|
/// Frees an update result allocated by the updater.
|
||||||
void freeUpdateResult(Pointer<UpdateResult> ptr) =>
|
void freeUpdateResult(Pointer<UpdateResult> ptr) =>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ void main() {
|
|||||||
group('when updater has an update available', () {
|
group('when updater has an update available', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(updater.currentPatchNumber).thenReturn(0);
|
when(updater.currentPatchNumber).thenReturn(0);
|
||||||
when(updater.checkForUpdate).thenReturn(true);
|
when(updater.checkForDownloadableUpdate).thenReturn(true);
|
||||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
when(updater.currentPatchNumber).thenReturn(0);
|
when(updater.currentPatchNumber).thenReturn(0);
|
||||||
when(updater.nextPatchNumber).thenReturn(1);
|
when(updater.nextPatchNumber).thenReturn(1);
|
||||||
when(updater.checkForUpdate).thenReturn(false);
|
when(updater.checkForDownloadableUpdate).thenReturn(false);
|
||||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
when(updater.currentPatchNumber).thenReturn(1);
|
when(updater.currentPatchNumber).thenReturn(1);
|
||||||
when(updater.nextPatchNumber).thenReturn(1);
|
when(updater.nextPatchNumber).thenReturn(1);
|
||||||
when(updater.checkForUpdate).thenReturn(false);
|
when(updater.checkForDownloadableUpdate).thenReturn(false);
|
||||||
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,6 +285,28 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a track is provided', () {
|
||||||
|
const track = UpdateTrack.beta;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
when(updater.currentPatchNumber).thenReturn(0);
|
||||||
|
when(
|
||||||
|
() => updater.checkForDownloadableUpdate(track: track),
|
||||||
|
).thenReturn(true);
|
||||||
|
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forwards the provided track to the underlying updater call',
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
shorebirdUpdater.checkForUpdate(track: track),
|
||||||
|
completion(equals(UpdateStatus.outdated)),
|
||||||
|
);
|
||||||
|
verify(() => updater.checkForDownloadableUpdate(track: track))
|
||||||
|
.called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('update', () {
|
group('update', () {
|
||||||
@@ -565,6 +587,29 @@ Please upgrade the Shorebird Engine for improved error messages.''',
|
|||||||
verify(() => updater.freeUpdateResult(any())).called(1);
|
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a track is provided', () {
|
||||||
|
const track = UpdateTrack.beta;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
when(updater.currentPatchNumber).thenReturn(0);
|
||||||
|
final result = calloc.allocate<UpdateResult>(sizeOf<UpdateResult>());
|
||||||
|
result.ref.status = SHOREBIRD_UPDATE_INSTALLED;
|
||||||
|
addTearDown(() => calloc.free(result));
|
||||||
|
when(() => updater.update(track: track)).thenReturn(result);
|
||||||
|
shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forwards the provided track to the underlying updater call',
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
shorebirdUpdater.update(track: track),
|
||||||
|
completes,
|
||||||
|
);
|
||||||
|
verify(() => updater.update(track: track)).called(1);
|
||||||
|
verify(() => updater.freeUpdateResult(any())).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:ffi';
|
|||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||||
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart';
|
||||||
import 'package:shorebird_code_push/src/updater.dart';
|
import 'package:shorebird_code_push/src/updater.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
@@ -38,17 +39,55 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('checkForUpdate', () {
|
group('checkForDownloadableUpdate', () {
|
||||||
test('forwards the result of shorebird_check_for_update', () {
|
test('forwards the result of shorebird_check_for_update', () {
|
||||||
when(
|
when(
|
||||||
() => updaterBindings.shorebird_check_for_update(),
|
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||||
|
nullptr,
|
||||||
|
),
|
||||||
).thenReturn(true);
|
).thenReturn(true);
|
||||||
expect(updater.checkForUpdate(), isTrue);
|
expect(updater.checkForDownloadableUpdate(), isTrue);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => updaterBindings.shorebird_check_for_update(),
|
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||||
|
nullptr,
|
||||||
|
),
|
||||||
).thenReturn(false);
|
).thenReturn(false);
|
||||||
expect(updater.checkForUpdate(), isFalse);
|
expect(updater.checkForDownloadableUpdate(), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('when a track is provided', () {
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||||
|
any(),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forwards the result of shorebird_check_for_update', () {
|
||||||
|
expect(
|
||||||
|
updater.checkForDownloadableUpdate(track: UpdateTrack.beta),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
updater.checkForDownloadableUpdate(track: UpdateTrack.stable),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
final captured = verify(
|
||||||
|
() => updaterBindings.shorebird_check_for_downloadable_update(
|
||||||
|
captureAny(),
|
||||||
|
),
|
||||||
|
).captured;
|
||||||
|
expect(
|
||||||
|
captured.map(
|
||||||
|
(cstr) => (cstr as Pointer<Char>).cast<Utf8>().toDartString(),
|
||||||
|
),
|
||||||
|
equals(['beta', 'stable']),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,10 +112,12 @@ void main() {
|
|||||||
group('update', () {
|
group('update', () {
|
||||||
test('calls bindings.shorebird_update_with_result', () {
|
test('calls bindings.shorebird_update_with_result', () {
|
||||||
when(
|
when(
|
||||||
() => updaterBindings.shorebird_update_with_result(),
|
() => updaterBindings.shorebird_update_with_result(nullptr),
|
||||||
).thenReturn(nullptr);
|
).thenReturn(nullptr);
|
||||||
updater.update();
|
updater.update();
|
||||||
verify(() => updaterBindings.shorebird_update_with_result()).called(1);
|
verify(
|
||||||
|
() => updaterBindings.shorebird_update_with_result(nullptr),
|
||||||
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user