From ee3f5ec669a190759b046bfdddf73cf136d22bf5 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 12 Nov 2024 13:58:50 -0600 Subject: [PATCH] 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 Co-authored-by: Bryan Oltman --- library/include/updater.h | 19 +++- library/src/c_api/mod.rs | 84 ++++++++++---- library/src/updater.rs | 59 ++++++---- shorebird_code_push/example/ios/Podfile.lock | 2 +- shorebird_code_push/example/lib/main.dart | 104 ++++++++++++++++-- .../lib/shorebird_code_push.dart | 3 +- .../lib/src/generated/updater_bindings.g.dart | 97 ++++++++++------ .../lib/src/shorebird_updater.dart | 17 ++- .../lib/src/shorebird_updater_io.dart | 12 +- .../lib/src/shorebird_updater_web.dart | 5 +- shorebird_code_push/lib/src/updater.dart | 18 ++- .../test/src/shorebird_updater_io_test.dart | 51 ++++++++- .../test/src/updater_test.dart | 55 +++++++-- 13 files changed, 411 insertions(+), 115 deletions(-) diff --git a/library/include/updater.h b/library/include/updater.h index fda7253..b8e578f 100644 --- a/library/include/updater.h +++ b/library/include/updater.h @@ -151,9 +151,15 @@ SHOREBIRD_EXPORT void shorebird_free_string(const char *c_string); 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. @@ -161,10 +167,15 @@ SHOREBIRD_EXPORT bool shorebird_check_for_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. */ -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. diff --git a/library/src/c_api/mod.rs b/library/src/c_api/mod.rs index ab7927c..31d0752 100644 --- a/library/src/c_api/mod.rs +++ b/library/src/c_api/mod.rs @@ -68,6 +68,7 @@ pub struct UpdateResult { pub status: i32, pub message: *const libc::c_char, } + #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct FileCallbacks { @@ -94,6 +95,13 @@ fn to_rust(c_string: *const libc::c_char) -> anyhow::Result { Ok(c_str.to_str()?.to_string()) } +fn to_rust_option(c_string: *const c_char) -> anyhow::Result> { + 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. fn allocate_c_string(rust_string: &str) -> anyhow::Result<*mut c_char> { 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] -pub extern "C" fn shorebird_check_for_update() -> bool { - log_on_error(updater::check_for_update, "checking for update", false) +pub extern "C" fn shorebird_check_for_downloadable_update(c_channel: *const c_char) -> bool { + 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. #[no_mangle] pub extern "C" fn shorebird_update() { log_on_error( - || updater::update().map(|result| shorebird_info!("Update result: {}", result)), + || updater::update(None).map(|result| shorebird_info!("Update result: {}", result)), "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. #[no_mangle] -pub extern "C" fn shorebird_update_with_result() -> *const UpdateResult { - let result = to_update_result(updater::update()); +pub extern "C" fn shorebird_update_with_result(c_channel: *const c_char) -> *const UpdateResult { + 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)); } @@ -522,7 +550,10 @@ mod test { // set up the network hooks to return a patch. 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"` let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45"; Ok(PatchCheckResponse { @@ -547,7 +578,7 @@ mod test { |_url, _event| Ok(()), ); // 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. shorebird_update(); @@ -565,7 +596,7 @@ mod test { #[serial] #[test] - fn patch_success_with_result() { + fn patch_success_with_result() -> anyhow::Result<()> { testing_reset_config(); let tmp_dir = TempDir::new("example").unwrap(); @@ -584,7 +615,8 @@ mod test { // set up the network hooks to return a patch. testing_set_network_hooks( - |_url, _request| { + |_url, request| { + assert_eq!(request.channel, "beta"); // Generated by `string_patch "hello world" "hello tests"` let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45"; Ok(PatchCheckResponse { @@ -609,10 +641,12 @@ mod test { |_url, _event| Ok(()), ); // 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. - let result = shorebird_update_with_result(); + let result = shorebird_update_with_result(channel_c_str); + unsafe { shorebird_free_string(channel_c_str) }; unsafe { assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED); @@ -627,6 +661,8 @@ mod test { unsafe { shorebird_free_string(c_path) }; let new = std::fs::read_to_string(path).unwrap(); assert_eq!(new, expected_new); + + Ok(()) } #[serial] @@ -661,7 +697,7 @@ mod test { ); // Go ahead and do the update. - let result = shorebird_update_with_result(); + let result = shorebird_update_with_result(std::ptr::null()); unsafe { assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE); @@ -702,7 +738,7 @@ mod test { ); // Go ahead and do the update. - let result = shorebird_update_with_result(); + let result = shorebird_update_with_result(std::ptr::null()); unsafe { assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR); @@ -712,7 +748,7 @@ mod test { #[serial] #[test] - fn patch_download_failure_with_result() { + fn patch_download_failure_with_result() -> anyhow::Result<()> { testing_reset_config(); let tmp_dir = TempDir::new("example").unwrap(); @@ -730,7 +766,11 @@ mod test { // set up the network hooks to return a patch. 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"` let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45"; Ok(PatchCheckResponse { @@ -749,12 +789,16 @@ mod test { ); // 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 { assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR); shorebird_free_update_result(result as *mut UpdateResult); } + + Ok(()) } #[serial] @@ -806,7 +850,7 @@ mod test { assert_eq!(shorebird_current_boot_patch_number(), 0); // 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. shorebird_update(); @@ -913,7 +957,7 @@ mod test { shorebird_start_update_thread(); // Wait for the thread to start. 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. std::thread::sleep(std::time::Duration::from_millis(100)); diff --git a/library/src/updater.rs b/library/src/updater.rs index 6f3604e..b46280d 100644 --- a/library/src/updater.rs +++ b/library/src/updater.rs @@ -242,12 +242,24 @@ pub fn should_auto_update() -> anyhow::Result { with_config(|config| Ok(config.auto_update)) } -/// Synchronously checks for an update and returns true if an update is available. -pub fn check_for_update() -> anyhow::Result { +/// Synchronously checks 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 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 { 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(( - PatchCheckRequest::new(config), + PatchCheckRequest::new(&config), patches_check_url(&config.base_url), config.network_hooks.patch_check_request_fn, )) @@ -319,7 +331,7 @@ fn copy_update_config() -> anyhow::Result { // Callers must possess the Updater lock, but we don't care about the contents // since they're empty. -fn update_internal(_: &UpdaterLockState) -> anyhow::Result { +fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Result { // Only one copy of Update can be running at a time. // Update will take the global Updater lock. // Update will need to take the Config lock at times, but will only @@ -337,7 +349,10 @@ fn update_internal(_: &UpdaterLockState) -> anyhow::Result { // Takes Config lock and installs patch. // 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 don't stall the client. @@ -460,8 +475,8 @@ fn should_install_patch(patch_number: usize) -> Result anyhow::Result { - with_updater_thread_lock(update_internal) +pub fn update(channel: Option<&str>) -> anyhow::Result { + 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 @@ -642,7 +657,7 @@ pub fn report_launch_success() -> anyhow::Result<()> { /// and install it if available. pub fn start_update_thread() { std::thread::spawn(move || { - let result = update(); + let result = update(None); let status = match result { Ok(status) => status, Err(err) => { @@ -949,7 +964,7 @@ mod tests { let apk_path = tmp_dir.path().join("base.apk"); 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); // This is gross. @@ -1011,7 +1026,7 @@ mod tests { let apk_path = tmp_dir.path().join("base.apk"); 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); // This is gross. @@ -1147,7 +1162,7 @@ mod tests { // Make sure we're starting with no next boot patch. 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. assert_eq!(result, crate::UpdateStatus::UpdateIsBadPatch); @@ -1182,7 +1197,7 @@ mod tests { install_fake_patch(patch_number)?; - let update_status = super::update()?; + let update_status = super::update(None)?; assert_eq!(update_status, crate::UpdateStatus::NoUpdate); @@ -1242,7 +1257,7 @@ mod tests { }) .unwrap(); - super::update().unwrap(); + super::update(None).unwrap(); // Only 3 events should have been sent. event_mock.expect(3).assert(); @@ -1296,7 +1311,7 @@ mod tests { ); // 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. let config_thread = std::thread::spawn(|| with_config(|_| Ok(()))); @@ -1358,7 +1373,7 @@ mod rollback_tests { Ok(()) })?; - let update_result = crate::update(); + let update_result = crate::update(None); assert_eq!(update_result.unwrap(), crate::UpdateStatus::NoUpdate); Ok(()) @@ -1393,7 +1408,7 @@ mod rollback_tests { Ok(()) })?; - crate::update()?; + crate::update(None)?; with_mut_state(|state| { assert!(state.next_boot_patch().is_none()); @@ -1462,7 +1477,7 @@ mod rollback_tests { Ok(()) })?; - let update_result = crate::update(); + let update_result = crate::update(None); assert_eq!(update_result.unwrap(), crate::UpdateStatus::UpdateInstalled); with_mut_state(|state| { @@ -1475,7 +1490,7 @@ mod rollback_tests { } #[cfg(test)] -mod check_for_update_tests { +mod check_for_downloadable_update_tests { use anyhow::Result; use serial_test::serial; use tempdir::TempDir; @@ -1518,7 +1533,7 @@ mod check_for_update_tests { 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); Ok(()) @@ -1536,7 +1551,7 @@ mod check_for_update_tests { report_launch_start()?; 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); Ok(()) @@ -1550,7 +1565,7 @@ mod check_for_update_tests { let tmp_dir = TempDir::new("example").unwrap(); 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); Ok(()) diff --git a/shorebird_code_push/example/ios/Podfile.lock b/shorebird_code_push/example/ios/Podfile.lock index 9cbc8fa..50a5530 100644 --- a/shorebird_code_push/example/ios/Podfile.lock +++ b/shorebird_code_push/example/ios/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.1 diff --git a/shorebird_code_push/example/lib/main.dart b/shorebird_code_push/example/lib/main.dart index d351b6c..14a13c9 100644 --- a/shorebird_code_push/example/lib/main.dart +++ b/shorebird_code_push/example/lib/main.dart @@ -29,6 +29,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final _updater = ShorebirdUpdater(); late final bool _isUpdaterAvailable; + var _currentTrack = UpdateTrack.stable; var _isCheckingForUpdates = false; Patch? _currentPatch; @@ -54,10 +55,20 @@ class _MyHomePageState extends State { try { setState(() => _isCheckingForUpdates = true); // Check if there's an update available. - final status = await _updater.checkForUpdate(); + final status = await _updater.checkForUpdate(track: _currentTrack); if (!mounted) return; // 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) { // If an error occurs, we log it for now. debugPrint('Error checking for update: $error'); @@ -88,7 +99,9 @@ class _MyHomePageState extends State { ..hideCurrentMaterialBanner() ..showMaterialBanner( MaterialBanner( - content: const Text('Update available'), + content: Text( + 'Update available for the ${_currentTrack.name} track.', + ), actions: [ TextButton( onPressed: () async { @@ -104,6 +117,26 @@ class _MyHomePageState extends State { ); } + 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() { ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() @@ -145,8 +178,10 @@ class _MyHomePageState extends State { Future _downloadUpdate() async { _showDownloadingBanner(); try { - // Perform the update (e.g download the latest patch). - await _updater.update(); + // Perform the update (e.g download the latest patch on [_currentTrack]). + // Note that [track] is optional. Not passing it will default to the + // stable track. + await _updater.update(track: _currentTrack); if (!mounted) return; // Show a banner to inform the user that the update is ready and that they // need to restart the app. @@ -166,9 +201,21 @@ class _MyHomePageState extends State { backgroundColor: theme.colorScheme.inversePrimary, title: const Text('Shorebird Code Push'), ), - body: _isUpdaterAvailable - ? _CurrentPatchVersion(patch: _currentPatch) - : const _ShorebirdUnavailable(), + body: Column( + children: [ + 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( onPressed: _isCheckingForUpdates ? null : _checkForUpdate, 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 onChanged; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Text('Update track:'), + SegmentedButton( + 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. class _LoadingIndicator extends StatelessWidget { const _LoadingIndicator(); diff --git a/shorebird_code_push/lib/shorebird_code_push.dart b/shorebird_code_push/lib/shorebird_code_push.dart index 6c7049c..b892eed 100644 --- a/shorebird_code_push/lib/shorebird_code_push.dart +++ b/shorebird_code_push/lib/shorebird_code_push.dart @@ -8,4 +8,5 @@ export 'src/shorebird_updater.dart' ShorebirdUpdater, UpdateException, UpdateFailureReason, - UpdateStatus; + UpdateStatus, + UpdateTrack; diff --git a/shorebird_code_push/lib/src/generated/updater_bindings.g.dart b/shorebird_code_push/lib/src/generated/updater_bindings.g.dart index 3415ffe..1b78aa6 100644 --- a/shorebird_code_push/lib/src/generated/updater_bindings.g.dart +++ b/shorebird_code_push/lib/src/generated/updater_bindings.g.dart @@ -189,13 +189,13 @@ class UpdaterBindings { _waitpidPtr.asFunction, int)>(); int waitid( - idtype_t arg0, - Dart__uint32_t arg1, + int arg0, + int arg1, ffi.Pointer arg2, int arg3, ) { return _waitid( - arg0.value, + arg0, arg1, arg2, arg3, @@ -204,8 +204,8 @@ class UpdaterBindings { late final _waitidPtr = _lookup< ffi.NativeFunction< - ffi.Int Function(ffi.UnsignedInt, id_t, ffi.Pointer, - ffi.Int)>>('waitid'); + ffi.Int Function( + ffi.Int32, id_t, ffi.Pointer, ffi.Int)>>('waitid'); late final _waitid = _waitidPtr .asFunction, int)>(); @@ -2456,16 +2456,26 @@ class UpdaterBindings { late final _shorebird_free_update_result = _shorebird_free_update_resultPtr .asFunction)>(); - /// Check for an update. Returns true if an update is available. - bool shorebird_check_for_update() { - return _shorebird_check_for_update(); + /// 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. + bool shorebird_check_for_downloadable_update( + ffi.Pointer c_channel, + ) { + return _shorebird_check_for_downloadable_update( + c_channel, + ); } - late final _shorebird_check_for_updatePtr = - _lookup>( - 'shorebird_check_for_update'); - late final _shorebird_check_for_update = - _shorebird_check_for_updatePtr.asFunction(); + late final _shorebird_check_for_downloadable_updatePtr = + _lookup)>>( + 'shorebird_check_for_downloadable_update'); + late final _shorebird_check_for_downloadable_update = + _shorebird_check_for_downloadable_updatePtr + .asFunction)>(); /// Synchronously download an update if one is available. void shorebird_update() { @@ -2477,17 +2487,26 @@ class UpdaterBindings { late final _shorebird_update = _shorebird_updatePtr.asFunction(); - /// 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. - ffi.Pointer shorebird_update_with_result() { - return _shorebird_update_with_result(); + ffi.Pointer shorebird_update_with_result( + ffi.Pointer c_channel, + ) { + return _shorebird_update_with_result( + c_channel, + ); } - late final _shorebird_update_with_resultPtr = - _lookup Function()>>( - 'shorebird_update_with_result'); + late final _shorebird_update_with_resultPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('shorebird_update_with_result'); late final _shorebird_update_with_result = _shorebird_update_with_resultPtr - .asFunction Function()>(); + .asFunction Function(ffi.Pointer)>(); /// Start a thread to download an update if one is available. void shorebird_start_update_thread() { @@ -2640,20 +2659,10 @@ final class _opaque_pthread_t extends ffi.Struct { external ffi.Array __opaque; } -enum idtype_t { - P_ALL(0), - P_PID(1), - 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"), - }; +abstract class idtype_t { + static const int P_ALL = 0; + static const int P_PID = 1; + static const int P_PGID = 2; } 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_1 = 150100; + const int __IPHONE_2_0 = 20000; 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_1 = 180100; + const int __WATCHOS_1_0 = 10000; 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_1 = 110100; + const int __TVOS_9_0 = 90000; 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_1 = 180100; + const int __BRIDGEOS_2_0 = 20000; 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_1 = 90100; + const int __DRIVERKIT_19_0 = 190000; 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_1 = 240100; + const int __VISIONOS_1_0 = 10000; 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_1 = 20100; + const int MAC_OS_X_VERSION_10_0 = 1000; 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_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; diff --git a/shorebird_code_push/lib/src/shorebird_updater.dart b/shorebird_code_push/lib/src/shorebird_updater.dart index d46eab3..9329282 100644 --- a/shorebird_code_push/lib/src/shorebird_updater.dart +++ b/shorebird_code_push/lib/src/shorebird_updater.dart @@ -121,8 +121,7 @@ abstract class ShorebirdUpdater { /// Checks for available updates and returns the [UpdateStatus]. /// This method should be used to determine the update status before calling /// [update]. - /// Returns `null` if the updater is not available. - Future checkForUpdate(); + Future checkForUpdate({UpdateTrack? track}); /// Updates the app to the latest patch (if available). /// 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. /// * [checkForUpdate], which should be called to check if an update is /// available before calling this method. - Future update(); + Future 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, } diff --git a/shorebird_code_push/lib/src/shorebird_updater_io.dart b/shorebird_code_push/lib/src/shorebird_updater_io.dart index fd72122..b41df0a 100644 --- a/shorebird_code_push/lib/src/shorebird_updater_io.dart +++ b/shorebird_code_push/lib/src/shorebird_updater_io.dart @@ -66,12 +66,16 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater { } @override - Future checkForUpdate() async { + Future checkForUpdate({UpdateTrack? track}) async { 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 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; return next != null && current?.number != next.number ? UpdateStatus.restartRequired @@ -79,13 +83,13 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater { } @override - Future update() async { + Future update({UpdateTrack? track}) async { if (!_isAvailable) return; Pointer result = nullptr; try { - result = await _run(_updater.update); + result = await _run(() => _updater.update(track: track)); } catch (_) { return _legacyFallback(); } diff --git a/shorebird_code_push/lib/src/shorebird_updater_web.dart b/shorebird_code_push/lib/src/shorebird_updater_web.dart index 4fb8b23..8deb040 100644 --- a/shorebird_code_push/lib/src/shorebird_updater_web.dart +++ b/shorebird_code_push/lib/src/shorebird_updater_web.dart @@ -23,8 +23,9 @@ class ShorebirdUpdaterImpl implements ShorebirdUpdater { Future readNextPatch() async => null; @override - Future checkForUpdate() async => UpdateStatus.unavailable; + Future checkForUpdate({UpdateTrack? track}) async => + UpdateStatus.unavailable; @override - Future update() async {} + Future update({UpdateTrack? track}) async {} } diff --git a/shorebird_code_push/lib/src/updater.dart b/shorebird_code_push/lib/src/updater.dart index 4b1330c..aa5535a 100644 --- a/shorebird_code_push/lib/src/updater.dart +++ b/shorebird_code_push/lib/src/updater.dart @@ -1,8 +1,10 @@ import 'dart:ffi' as ffi; import 'dart:ffi'; +import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart'; +import 'package:shorebird_code_push/src/shorebird_updater.dart'; /// {@template updater} /// A wrapper around the generated [UpdaterBindings] that, when necessary, @@ -20,9 +22,6 @@ class Updater { /// The currently active 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 /// currentPatchNumber if no new patch is available. int nextPatchNumber() => bindings.shorebird_next_boot_patch_number(); @@ -30,9 +29,20 @@ class Updater { /// Downloads the latest patch, if available. 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(), + ); + /// Downloads the latest patch, if available and returns an [UpdateResult] /// to indicate whether the update was successful. - Pointer update() => bindings.shorebird_update_with_result(); + Pointer update({UpdateTrack? track}) => + bindings.shorebird_update_with_result( + track == null ? ffi.nullptr : track.name.toNativeUtf8().cast(), + ); /// Frees an update result allocated by the updater. void freeUpdateResult(Pointer ptr) => diff --git a/shorebird_code_push/test/src/shorebird_updater_io_test.dart b/shorebird_code_push/test/src/shorebird_updater_io_test.dart index 3fc9dc8..84f683a 100644 --- a/shorebird_code_push/test/src/shorebird_updater_io_test.dart +++ b/shorebird_code_push/test/src/shorebird_updater_io_test.dart @@ -242,7 +242,7 @@ void main() { group('when updater has an update available', () { setUp(() { when(updater.currentPatchNumber).thenReturn(0); - when(updater.checkForUpdate).thenReturn(true); + when(updater.checkForDownloadableUpdate).thenReturn(true); shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); }); @@ -258,7 +258,7 @@ void main() { setUp(() { when(updater.currentPatchNumber).thenReturn(0); when(updater.nextPatchNumber).thenReturn(1); - when(updater.checkForUpdate).thenReturn(false); + when(updater.checkForDownloadableUpdate).thenReturn(false); shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); }); @@ -274,7 +274,7 @@ void main() { setUp(() { when(updater.currentPatchNumber).thenReturn(1); when(updater.nextPatchNumber).thenReturn(1); - when(updater.checkForUpdate).thenReturn(false); + when(updater.checkForDownloadableUpdate).thenReturn(false); 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', () { @@ -565,6 +587,29 @@ Please upgrade the Shorebird Engine for improved error messages.''', 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(sizeOf()); + 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); + }); + }); }); }); } diff --git a/shorebird_code_push/test/src/updater_test.dart b/shorebird_code_push/test/src/updater_test.dart index bc54283..cbc39a8 100644 --- a/shorebird_code_push/test/src/updater_test.dart +++ b/shorebird_code_push/test/src/updater_test.dart @@ -2,6 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.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/updater.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', () { when( - () => updaterBindings.shorebird_check_for_update(), + () => updaterBindings.shorebird_check_for_downloadable_update( + nullptr, + ), ).thenReturn(true); - expect(updater.checkForUpdate(), isTrue); + expect(updater.checkForDownloadableUpdate(), isTrue); when( - () => updaterBindings.shorebird_check_for_update(), + () => updaterBindings.shorebird_check_for_downloadable_update( + nullptr, + ), ).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).cast().toDartString(), + ), + equals(['beta', 'stable']), + ); + }); }); }); @@ -73,10 +112,12 @@ void main() { group('update', () { test('calls bindings.shorebird_update_with_result', () { when( - () => updaterBindings.shorebird_update_with_result(), + () => updaterBindings.shorebird_update_with_result(nullptr), ).thenReturn(nullptr); updater.update(); - verify(() => updaterBindings.shorebird_update_with_result()).called(1); + verify( + () => updaterBindings.shorebird_update_with_result(nullptr), + ).called(1); }); });