diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4b01ad2..3e5c5d0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -58,7 +58,7 @@ jobs: matrix: crate: ${{ fromJSON(needs.changes.outputs.needs_rust_build) }} - runs-on: ubuntu-latest + runs-on: macos-latest name: 🦀 Build ${{ matrix.crate }} @@ -98,12 +98,7 @@ jobs: working_directory: ${{ matrix.package }} ci: - needs: - [ - semantic_pull_request, - build_flutter_packages, - build_rust_crates, - ] + needs: [semantic_pull_request, build_flutter_packages, build_rust_crates] if: ${{ always() }} runs-on: ubuntu-latest diff --git a/library/include/updater.h b/library/include/updater.h index 6b0f723..fda7253 100644 --- a/library/include/updater.h +++ b/library/include/updater.h @@ -14,6 +14,33 @@ #endif +/** + * An unknown error occurred while updating. The update was not installed. + * This is a catch-all for errors that don't fit into the other categories. + */ +#define SHOREBIRD_UPDATE_ERROR -1 + +/** + * No update is available (e.g. the app is already up-to-date) + */ +#define SHOREBIRD_NO_UPDATE 0 + +/** + * An update was installed successfully. It will boot from the update on the + * next app launch. + */ +#define SHOREBIRD_UPDATE_INSTALLED 1 + +/** + * An error occurred while updating. The update was not installed. + */ +#define SHOREBIRD_UPDATE_HAD_ERROR 2 + +/** + * The downloaded patch was not installed because it was invalid. + */ +#define SHOREBIRD_UPDATE_IS_BAD_PATCH 3 + /** * Struct containing configuration parameters for the updater. * Passed to all updater functions. @@ -69,6 +96,11 @@ typedef struct FileCallbacks { void (*close)(void *file_handle); } FileCallbacks; +typedef struct UpdateResult { + int32_t status; + const char *message; +} UpdateResult; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -114,7 +146,9 @@ SHOREBIRD_EXPORT char *shorebird_next_boot_patch_path(void); * If this function is called with a non-null pointer, it must be a pointer * returned by the updater library. */ -SHOREBIRD_EXPORT void shorebird_free_string(char *c_string); +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. @@ -126,6 +160,12 @@ SHOREBIRD_EXPORT bool shorebird_check_for_update(void); */ SHOREBIRD_EXPORT void shorebird_update(void); +/** + * Synchronously download an update if one is available. + * Returns an [UpdateResult] indicating whether the update was successful. + */ +SHOREBIRD_EXPORT const struct UpdateResult *shorebird_update_with_result(void); + /** * 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 2a8e9ed..ab7927c 100644 --- a/library/src/c_api/mod.rs +++ b/library/src/c_api/mod.rs @@ -14,7 +14,7 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::path::PathBuf; -use crate::updater; +use crate::{updater, UpdateStatus}; use self::c_file::CFileProvder; @@ -46,6 +46,28 @@ pub struct AppParameters { pub code_cache_dir: *const libc::c_char, } +/// An unknown error occurred while updating. The update was not installed. +/// This is a catch-all for errors that don't fit into the other categories. +pub const SHOREBIRD_UPDATE_ERROR: i32 = -1; + +/// No update is available (e.g. the app is already up-to-date) +pub const SHOREBIRD_NO_UPDATE: i32 = 0; + +/// An update was installed successfully. It will boot from the update on the +/// next app launch. +pub const SHOREBIRD_UPDATE_INSTALLED: i32 = 1; + +/// An error occurred while updating. The update was not installed. +pub const SHOREBIRD_UPDATE_HAD_ERROR: i32 = 2; + +/// The downloaded patch was not installed because it was invalid. +pub const SHOREBIRD_UPDATE_IS_BAD_PATCH: i32 = 3; + +#[repr(C)] +pub struct UpdateResult { + pub status: i32, + pub message: *const libc::c_char, +} #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct FileCallbacks { @@ -183,6 +205,24 @@ fn path_to_c_string(path: Option) -> anyhow::Result<*mut c_char> { }) } +fn to_update_result(status: anyhow::Result) -> UpdateResult { + let result = match status { + Ok(status) => { + let message = status.to_string(); + return UpdateResult { + status: status as i32, + message: allocate_c_string(message.as_str()) + .unwrap_or_else(|_| std::ptr::null_mut()), + }; + } + Err(err) => UpdateResult { + status: SHOREBIRD_UPDATE_ERROR, + message: allocate_c_string(&err.to_string()).unwrap_or_else(|_| std::ptr::null_mut()), + }, + }; + return result; +} + /// The path to the patch that will boot on the next run of the app, or NULL if /// there is no next patch. #[no_mangle] @@ -203,12 +243,26 @@ pub extern "C" fn shorebird_next_boot_patch_path() -> *mut c_char { /// If this function is called with a non-null pointer, it must be a pointer /// returned by the updater library. #[no_mangle] -pub unsafe extern "C" fn shorebird_free_string(c_string: *mut c_char) { +pub unsafe extern "C" fn shorebird_free_string(c_string: *const c_char) { if c_string.is_null() { return; } unsafe { - drop(CString::from_raw(c_string)); + drop(CString::from_raw(c_string as *mut c_char)); + } +} + +#[no_mangle] +pub unsafe extern "C" fn shorebird_free_update_result(result: *mut UpdateResult) { + if result.is_null() { + return; + } + let message = (*result).message; + if !message.is_null() { + shorebird_free_string(message); + } + unsafe { + drop(Box::from_raw(result)); } } @@ -228,6 +282,14 @@ pub extern "C" fn shorebird_update() { ); } +/// Synchronously download an update if one is available. +/// 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()); + return Box::into_raw(Box::new(result)); +} + /// Start a thread to download an update if one is available. #[no_mangle] pub extern "C" fn shorebird_start_update_thread() { @@ -361,6 +423,8 @@ mod test { // free_string also doesn't crash with null. unsafe { shorebird_free_string(std::ptr::null_mut()) } + // free_update_result also doesn't crash with null. + unsafe { shorebird_free_update_result(std::ptr::null_mut()) } } #[serial] @@ -499,6 +563,200 @@ mod test { assert_eq!(new, expected_new); } + #[serial] + #[test] + fn patch_success_with_result() { + testing_reset_config(); + let tmp_dir = TempDir::new("example").unwrap(); + + // Generated by `string_patch "hello world" "hello tests"` + let base = "hello world"; + let expected_new: &str = "hello tests"; + let apk_path = tmp_dir.path().join("base.apk"); + write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes()); + let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so"); + let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap()); + // app_id is required or shorebird_init will fail. + let c_yaml = c_string("app_id: foo"); + assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml)); + free_c_string(c_yaml); + free_parameters(c_params); + + // set up the network hooks to return a patch. + testing_set_network_hooks( + |_url, _request| { + // Generated by `string_patch "hello world" "hello tests"` + let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45"; + Ok(PatchCheckResponse { + patch_available: true, + patch: Some(crate::Patch { + number: 1, + hash: hash.to_owned(), + download_url: "ignored".to_owned(), + hash_signature: None, + }), + rolled_back_patch_numbers: None, + }) + }, + |_url| { + // Generated by `string_patch "hello world" "hello tests"` + let patch_bytes: Vec = vec![ + 40, 181, 47, 253, 0, 128, 177, 0, 0, 223, 177, 0, 0, 0, 16, 0, 0, 6, 0, 0, 0, + 0, 0, 0, 5, 116, 101, 115, 116, 115, 0, + ]; + Ok(patch_bytes) + }, + |_url, _event| Ok(()), + ); + // There is an update available. + assert!(shorebird_check_for_update()); + + // Go ahead and do the update. + let result = shorebird_update_with_result(); + + unsafe { + assert_eq!(result.read().status, SHOREBIRD_UPDATE_INSTALLED); + shorebird_free_update_result(result as *mut UpdateResult); + } + assert_eq!(shorebird_current_boot_patch_number(), 0); + assert_eq!(shorebird_next_boot_patch_number(), 1); + + // Read path contents into memory and check against expected. + let c_path = shorebird_next_boot_patch_path(); + let path = to_rust(c_path).unwrap(); + unsafe { shorebird_free_string(c_path) }; + let new = std::fs::read_to_string(path).unwrap(); + assert_eq!(new, expected_new); + } + + #[serial] + #[test] + fn patch_check_no_patch_with_result() { + testing_reset_config(); + let tmp_dir = TempDir::new("example").unwrap(); + + // Generated by `string_patch "hello world" "hello tests"` + let base = "hello world"; + let apk_path = tmp_dir.path().join("base.apk"); + write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes()); + let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so"); + let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap()); + // app_id is required or shorebird_init will fail. + let c_yaml = c_string("app_id: foo"); + assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml)); + free_c_string(c_yaml); + free_parameters(c_params); + + // set up the network hooks to return a patch. + testing_set_network_hooks( + |_url, _request| { + Ok(PatchCheckResponse { + patch_available: false, + patch: None, + rolled_back_patch_numbers: None, + }) + }, + |_url| Err(anyhow::anyhow!("Error")), + |_url, _event| Ok(()), + ); + + // Go ahead and do the update. + let result = shorebird_update_with_result(); + + unsafe { + assert_eq!(result.read().status, SHOREBIRD_NO_UPDATE); + shorebird_free_update_result(result as *mut UpdateResult); + } + } + + #[serial] + #[test] + fn patch_check_failure_with_result() { + testing_reset_config(); + let tmp_dir = TempDir::new("example").unwrap(); + + // Generated by `string_patch "hello world" "hello tests"` + let base = "hello world"; + let apk_path = tmp_dir.path().join("base.apk"); + write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes()); + let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so"); + let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap()); + // app_id is required or shorebird_init will fail. + let c_yaml = c_string("app_id: foo"); + assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml)); + free_c_string(c_yaml); + free_parameters(c_params); + + // set up the network hooks to return a patch. + testing_set_network_hooks( + |_url, _request| Err(anyhow::anyhow!("Error")), + |_url| { + // Generated by `string_patch "hello world" "hello tests"` + let patch_bytes: Vec = vec![ + 40, 181, 47, 253, 0, 128, 177, 0, 0, 223, 177, 0, 0, 0, 16, 0, 0, 6, 0, 0, 0, + 0, 0, 0, 5, 116, 101, 115, 116, 115, 0, + ]; + Ok(patch_bytes) + }, + |_url, _event| Ok(()), + ); + + // Go ahead and do the update. + let result = shorebird_update_with_result(); + + unsafe { + assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR); + shorebird_free_update_result(result as *mut UpdateResult); + } + } + + #[serial] + #[test] + fn patch_download_failure_with_result() { + testing_reset_config(); + let tmp_dir = TempDir::new("example").unwrap(); + + // Generated by `string_patch "hello world" "hello tests"` + let base = "hello world"; + let apk_path = tmp_dir.path().join("base.apk"); + write_fake_apk(apk_path.to_str().unwrap(), base.as_bytes()); + let fake_libapp_path = tmp_dir.path().join("lib/arch/ignored.so"); + let c_params = parameters(&tmp_dir, fake_libapp_path.to_str().unwrap()); + // app_id is required or shorebird_init will fail. + let c_yaml = c_string("app_id: foo"); + assert!(shorebird_init(&c_params, FileCallbacks::new(), c_yaml)); + free_c_string(c_yaml); + free_parameters(c_params); + + // set up the network hooks to return a patch. + testing_set_network_hooks( + |_url, _request| { + // Generated by `string_patch "hello world" "hello tests"` + let hash = "bb8f1d041a5cdc259055afe9617136799543e0a7a86f86db82f8c1fadbd8cc45"; + Ok(PatchCheckResponse { + patch_available: true, + patch: Some(crate::Patch { + number: 1, + hash: hash.to_owned(), + download_url: "ignored".to_owned(), + hash_signature: None, + }), + rolled_back_patch_numbers: None, + }) + }, + |_url| Err(anyhow::anyhow!("Error")), + |_url, _event| Ok(()), + ); + + // Go ahead and do the update. + let result = shorebird_update_with_result(); + + unsafe { + assert_eq!(result.read().status, SHOREBIRD_UPDATE_ERROR); + shorebird_free_update_result(result as *mut UpdateResult); + } + } + #[serial] #[test] fn current_boot_patch_set_after_reporting_launch_start() { diff --git a/shorebird_code_push/CHANGELOG.md b/shorebird_code_push/CHANGELOG.md index b11bb1a..de834f1 100644 --- a/shorebird_code_push/CHANGELOG.md +++ b/shorebird_code_push/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.0.0-dev.1 + +- **BREAKING**: revamp the updater API + - Remove `ShorebirdCodePush` in favor of `ShorebirdUpdater` + # 1.1.6 - Update log messages to explain what "using no-op implementation" means. diff --git a/shorebird_code_push/README.md b/shorebird_code_push/README.md index 0632876..fdd2f11 100644 --- a/shorebird_code_push/README.md +++ b/shorebird_code_push/README.md @@ -34,13 +34,11 @@ this: // Import the library import 'package:shorebird_code_push/shorebird_code_push.dart'; -// Create an instance of the ShorebirdCodePush class -final shorebirdCodePush = ShorebirdCodePush(); +// Create an instance of the updater class +final updater = ShorebirdUpdater(); // Launch your app -void main() { - runApp(const MyApp()); -} +void main() => runApp(const MyApp()); // [Other code here] @@ -49,20 +47,24 @@ class _MyHomePageState extends State { void initState() { super.initState(); - // Get the current patch number and print it to the console. It will be - // null if no patches are installed. - shorebirdCodePush - .currentPatchNumber() - .then((value) => print('current patch number is $value')); + // Get the current patch number and print it to the console. + // It will be `null` if no patches are installed. + updater.readCurrentPatch().then((currentPatch) { + print('The current patch number is: ${currentPatch.number}'); + }); } Future _checkForUpdates() async { - // Check whether a patch is available to install. - final isUpdateAvailable = await shorebirdCodePush.isNewPatchAvailableForDownload(); + // Check whether a new update is available. + final status = await updater.checkForUpdates(); - if (isUpdateAvailable) { - // Download the new patch if it's available. - await shorebirdCodePush.downloadUpdateIfAvailable(); + if (status == UpdateStatus.outdated) { + try { + // Perform the update + await updater.update(); + } on UpdateException catch (error) { + // Handle any errors that occur while updating. + } } } diff --git a/shorebird_code_push/example/lib/main.dart b/shorebird_code_push/example/lib/main.dart index 2984619..d351b6c 100644 --- a/shorebird_code_push/example/lib/main.dart +++ b/shorebird_code_push/example/lib/main.dart @@ -1,13 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:restart_app/restart_app.dart'; - import 'package:shorebird_code_push/shorebird_code_push.dart'; -// Create an instance of ShorebirdCodePush. Because this example only contains -// a single widget, we create it here, but you will likely only need to create -// a single instance of ShorebirdCodePush in your app. -final _shorebirdCodePush = ShorebirdCodePush(); - void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { @@ -21,198 +14,217 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.red), useMaterial3: true, ), - home: const MyHomePage(title: 'Shorebird Code Push'), + home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { - const MyHomePage({required this.title, super.key}); - - final String title; + const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { - final _isShorebirdAvailable = _shorebirdCodePush.isShorebirdAvailable(); - int? _currentPatchVersion; - bool _isCheckingForUpdate = false; + final _updater = ShorebirdUpdater(); + late final bool _isUpdaterAvailable; + var _isCheckingForUpdates = false; + Patch? _currentPatch; @override void initState() { super.initState(); - // Request the current patch number. - _shorebirdCodePush.currentPatchNumber().then((currentPatchVersion) { - if (!mounted) return; - setState(() { - _currentPatchVersion = currentPatchVersion; - }); + // Check whether Shorebird is available. + setState(() => _isUpdaterAvailable = _updater.isAvailable); + + // Read the current patch (if there is one.) + // `currentPatch` will be `null` if no patch is installed. + _updater.readCurrentPatch().then((currentPatch) { + setState(() => _currentPatch = currentPatch); + }).catchError((Object error) { + // If an error occurs, we log it for now. + debugPrint('Error reading current patch: $error'); }); } Future _checkForUpdate() async { - setState(() { - _isCheckingForUpdate = true; - }); + if (_isCheckingForUpdates) return; - // Ask the Shorebird servers if there is a new patch available. - final isUpdateAvailable = - await _shorebirdCodePush.isNewPatchAvailableForDownload(); - - if (!mounted) return; - - setState(() { - _isCheckingForUpdate = false; - }); - - if (isUpdateAvailable) { - _showUpdateAvailableBanner(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No update available'), - ), - ); + try { + setState(() => _isCheckingForUpdates = true); + // Check if there's an update available. + final status = await _updater.checkForUpdate(); + if (!mounted) return; + // If there is an update available, show a banner. + if (status == UpdateStatus.outdated) _showUpdateAvailableBanner(); + } catch (error) { + // If an error occurs, we log it for now. + debugPrint('Error checking for update: $error'); + } finally { + setState(() => _isCheckingForUpdates = false); } } void _showDownloadingBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - const MaterialBanner( - content: Text('Downloading...'), - actions: [ - SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2, + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + const MaterialBanner( + content: Text('Downloading...'), + actions: [ + SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator(), ), - ), - ], - ), - ); + ], + ), + ); } void _showUpdateAvailableBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - MaterialBanner( - content: const Text('Update available'), - actions: [ - TextButton( - onPressed: () async { - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - await _downloadUpdate(); - - if (!mounted) return; - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - }, - child: const Text('Download'), - ), - ], - ), - ); + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: const Text('Update available'), + actions: [ + TextButton( + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + await _downloadUpdate(); + if (!mounted) return; + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Download'), + ), + ], + ), + ); } void _showRestartBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - const MaterialBanner( - content: Text('A new patch is ready!'), - actions: [ - TextButton( - // Restart the app for the new patch to take effect. - onPressed: Restart.restartApp, - child: Text('Restart app'), - ), - ], - ), - ); + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: const Text('A new patch is ready! Please restart your app.'), + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); } - void _showErrorBanner() { - ScaffoldMessenger.of(context).showMaterialBanner( - MaterialBanner( - content: const Text('An error occurred while downloading the update.'), - actions: [ - TextButton( - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - }, - child: const Text('Dismiss'), + void _showErrorBanner(Object error) { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: Text( + 'An error occurred while downloading the update: $error.', ), - ], - ), - ); + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); } - // Note: this is only run if an update is reported as available. - // [isNewPatchReadyToInstall] returning false does not always indicate an - // error with the download. Future _downloadUpdate() async { _showDownloadingBanner(); - - await Future.wait([ - _shorebirdCodePush.downloadUpdateIfAvailable(), - // Add an artificial delay so the banner has enough time to animate in. - Future.delayed(const Duration(milliseconds: 250)), - ]); - - final isUpdateReadyToInstall = - await _shorebirdCodePush.isNewPatchReadyToInstall(); - - if (!mounted) return; - - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - if (isUpdateReadyToInstall) { + try { + // Perform the update (e.g download the latest patch). + await _updater.update(); + if (!mounted) return; + // Show a banner to inform the user that the update is ready and that they + // need to restart the app. _showRestartBanner(); - } else { - _showErrorBanner(); + } on UpdateException catch (error) { + // If an error occurs, we show a banner with the error message. + _showErrorBanner(error.message); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); - final heading = _currentPatchVersion != null - ? '$_currentPatchVersion' - : 'No patch installed'; + return Scaffold( appBar: AppBar( backgroundColor: theme.colorScheme.inversePrimary, - title: Text(widget.title), + title: const Text('Shorebird Code Push'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Current patch version:'), - Text( - heading, - style: theme.textTheme.headlineMedium, - ), - const SizedBox(height: 20), - if (!_isShorebirdAvailable) - Text( - 'Shorebird Engine not available.', - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.error, - ), - ), - if (_isShorebirdAvailable) - ElevatedButton( - onPressed: _isCheckingForUpdate ? null : _checkForUpdate, - child: _isCheckingForUpdate - ? const _LoadingIndicator() - : const Text('Check for update'), - ), - ], + body: _isUpdaterAvailable + ? _CurrentPatchVersion(patch: _currentPatch) + : const _ShorebirdUnavailable(), + floatingActionButton: FloatingActionButton( + onPressed: _isCheckingForUpdates ? null : _checkForUpdate, + tooltip: 'Check for update', + child: _isCheckingForUpdates + ? const _LoadingIndicator() + : const Icon(Icons.refresh), + ), + ); + } +} + +/// Widget that is mounted when Shorebird is not available. +class _ShorebirdUnavailable extends StatelessWidget { + const _ShorebirdUnavailable(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Text( + ''' +Shorebird is not available. +Please make sure the app was generated via `shorebird release` and that it is running in release mode.''', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.error, ), ), ); } } +/// Widget that displays the current patch version. +class _CurrentPatchVersion extends StatelessWidget { + const _CurrentPatchVersion({required this.patch}); + + final Patch? patch; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Current patch version:'), + Text( + patch != null ? '${patch!.number}' : 'No patch installed', + style: theme.textTheme.headlineMedium, + ), + ], + ), + ); + } +} + +/// A reusable loading indicator. class _LoadingIndicator extends StatelessWidget { const _LoadingIndicator(); diff --git a/shorebird_code_push/example/pubspec.yaml b/shorebird_code_push/example/pubspec.yaml index 0026f55..73c11ed 100644 --- a/shorebird_code_push/example/pubspec.yaml +++ b/shorebird_code_push/example/pubspec.yaml @@ -10,12 +10,10 @@ environment: dependencies: flutter: sdk: flutter - restart_app: ^1.3.2 shorebird_code_push: path: ../ dev_dependencies: - flutter_lints: ^2.0.0 flutter_test: sdk: flutter very_good_analysis: ^5.0.0 diff --git a/shorebird_code_push/example/shorebird.yaml b/shorebird_code_push/example/shorebird.yaml index 890f99e..c4044d3 100644 --- a/shorebird_code_push/example/shorebird.yaml +++ b/shorebird_code_push/example/shorebird.yaml @@ -1,7 +1,14 @@ -# This file is used to configure the Shorebird updater used by your application. -# Learn more at https://shorebird.dev -# This file should be checked into version control. +# This file is used to configure the Shorebird updater used by your app. +# Learn more at https://docs.shorebird.dev +# This file does not contain any sensitive information and should be checked into version control. -# This is the unique identifier assigned to your app. -# It is used by your app to request the correct patches from Shorebird servers. -app_id: 1692ba14-0c8d-490e-9593-13815d2ac1cf \ No newline at end of file +# Your app_id is the unique identifier assigned to your app. +# It is used to identify your app when requesting patches from Shorebird's servers. +# It is not a secret and can be shared publicly. +app_id: 1692ba14-0c8d-490e-9593-13815d2ac1cf + +# auto_update controls if Shorebird should automatically update in the background on launch. +# If auto_update: false, you will need to use package:shorebird_code_push to trigger updates. +# https://pub.dev/packages/shorebird_code_push +# Uncomment the following line to disable automatic updates. +auto_update: false diff --git a/shorebird_code_push/lib/shorebird_code_push.dart b/shorebird_code_push/lib/shorebird_code_push.dart index 60924c4..d0ae8eb 100644 --- a/shorebird_code_push/lib/shorebird_code_push.dart +++ b/shorebird_code_push/lib/shorebird_code_push.dart @@ -1,5 +1,4 @@ /// Get info about your Shorebird code push app library shorebird_code_push; -export 'src/shorebird_code_push_io.dart' - if (dart.library.html) 'src/shorebird_code_push_web.dart'; +export 'src/shorebird_updater.dart'; 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 d7e417d..3415ffe 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( - int arg0, - int arg1, + idtype_t arg0, + Dart__uint32_t arg1, ffi.Pointer arg2, int arg3, ) { return _waitid( - arg0, + arg0.value, arg1, arg2, arg3, @@ -204,8 +204,8 @@ class UpdaterBindings { late final _waitidPtr = _lookup< ffi.NativeFunction< - ffi.Int Function( - ffi.Int32, id_t, ffi.Pointer, ffi.Int)>>('waitid'); + ffi.Int Function(ffi.UnsignedInt, id_t, ffi.Pointer, + ffi.Int)>>('waitid'); late final _waitid = _waitidPtr .asFunction, int)>(); @@ -270,6 +270,264 @@ class UpdaterBindings { set __mb_cur_max(int value) => ___mb_cur_max.value = value; + ffi.Pointer malloc_type_malloc( + int size, + int type_id, + ) { + return _malloc_type_malloc( + size, + type_id, + ); + } + + late final _malloc_type_mallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Size, malloc_type_id_t)>>('malloc_type_malloc'); + late final _malloc_type_malloc = _malloc_type_mallocPtr + .asFunction Function(int, int)>(); + + ffi.Pointer malloc_type_calloc( + int count, + int size, + int type_id, + ) { + return _malloc_type_calloc( + count, + size, + type_id, + ); + } + + late final _malloc_type_callocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Size, ffi.Size, malloc_type_id_t)>>('malloc_type_calloc'); + late final _malloc_type_calloc = _malloc_type_callocPtr + .asFunction Function(int, int, int)>(); + + void malloc_type_free( + ffi.Pointer ptr, + int type_id, + ) { + return _malloc_type_free( + ptr, + type_id, + ); + } + + late final _malloc_type_freePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, malloc_type_id_t)>>('malloc_type_free'); + late final _malloc_type_free = _malloc_type_freePtr + .asFunction, int)>(); + + ffi.Pointer malloc_type_realloc( + ffi.Pointer ptr, + int size, + int type_id, + ) { + return _malloc_type_realloc( + ptr, + size, + type_id, + ); + } + + late final _malloc_type_reallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, ffi.Size, + malloc_type_id_t)>>('malloc_type_realloc'); + late final _malloc_type_realloc = _malloc_type_reallocPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, int, int)>(); + + ffi.Pointer malloc_type_valloc( + int size, + int type_id, + ) { + return _malloc_type_valloc( + size, + type_id, + ); + } + + late final _malloc_type_vallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Size, malloc_type_id_t)>>('malloc_type_valloc'); + late final _malloc_type_valloc = _malloc_type_vallocPtr + .asFunction Function(int, int)>(); + + ffi.Pointer malloc_type_aligned_alloc( + int alignment, + int size, + int type_id, + ) { + return _malloc_type_aligned_alloc( + alignment, + size, + type_id, + ); + } + + late final _malloc_type_aligned_allocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Size, ffi.Size, + malloc_type_id_t)>>('malloc_type_aligned_alloc'); + late final _malloc_type_aligned_alloc = _malloc_type_aligned_allocPtr + .asFunction Function(int, int, int)>(); + + int malloc_type_posix_memalign( + ffi.Pointer> memptr, + int alignment, + int size, + int type_id, + ) { + return _malloc_type_posix_memalign( + memptr, + alignment, + size, + type_id, + ); + } + + late final _malloc_type_posix_memalignPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer>, ffi.Size, + ffi.Size, malloc_type_id_t)>>('malloc_type_posix_memalign'); + late final _malloc_type_posix_memalign = + _malloc_type_posix_memalignPtr.asFunction< + int Function(ffi.Pointer>, int, int, int)>(); + + ffi.Pointer malloc_type_zone_malloc( + ffi.Pointer zone, + int size, + int type_id, + ) { + return _malloc_type_zone_malloc( + zone, + size, + type_id, + ); + } + + late final _malloc_type_zone_mallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, ffi.Size, + malloc_type_id_t)>>('malloc_type_zone_malloc'); + late final _malloc_type_zone_malloc = _malloc_type_zone_mallocPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, int, int)>(); + + ffi.Pointer malloc_type_zone_calloc( + ffi.Pointer zone, + int count, + int size, + int type_id, + ) { + return _malloc_type_zone_calloc( + zone, + count, + size, + type_id, + ); + } + + late final _malloc_type_zone_callocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, ffi.Size, + ffi.Size, malloc_type_id_t)>>('malloc_type_zone_calloc'); + late final _malloc_type_zone_calloc = _malloc_type_zone_callocPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, int, int, int)>(); + + void malloc_type_zone_free( + ffi.Pointer zone, + ffi.Pointer ptr, + int type_id, + ) { + return _malloc_type_zone_free( + zone, + ptr, + type_id, + ); + } + + late final _malloc_type_zone_freePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + malloc_type_id_t)>>('malloc_type_zone_free'); + late final _malloc_type_zone_free = _malloc_type_zone_freePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int)>(); + + ffi.Pointer malloc_type_zone_realloc( + ffi.Pointer zone, + ffi.Pointer ptr, + int size, + int type_id, + ) { + return _malloc_type_zone_realloc( + zone, + ptr, + size, + type_id, + ); + } + + late final _malloc_type_zone_reallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + malloc_type_id_t)>>('malloc_type_zone_realloc'); + late final _malloc_type_zone_realloc = + _malloc_type_zone_reallocPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, int, int)>(); + + ffi.Pointer malloc_type_zone_valloc( + ffi.Pointer zone, + int size, + int type_id, + ) { + return _malloc_type_zone_valloc( + zone, + size, + type_id, + ); + } + + late final _malloc_type_zone_vallocPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, ffi.Size, + malloc_type_id_t)>>('malloc_type_zone_valloc'); + late final _malloc_type_zone_valloc = _malloc_type_zone_vallocPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, int, int)>(); + + ffi.Pointer malloc_type_zone_memalign( + ffi.Pointer zone, + int alignment, + int size, + int type_id, + ) { + return _malloc_type_zone_memalign( + zone, + alignment, + size, + type_id, + ); + } + + late final _malloc_type_zone_memalignPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, ffi.Size, + ffi.Size, malloc_type_id_t)>>('malloc_type_zone_memalign'); + late final _malloc_type_zone_memalign = + _malloc_type_zone_memalignPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, int, int, int)>(); + ffi.Pointer malloc( int __size, ) { @@ -331,11 +589,28 @@ class UpdaterBindings { late final _realloc = _reallocPtr .asFunction Function(ffi.Pointer, int)>(); + ffi.Pointer reallocf( + ffi.Pointer __ptr, + int __size, + ) { + return _reallocf( + __ptr, + __size, + ); + } + + late final _reallocfPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Size)>>('reallocf'); + late final _reallocf = _reallocfPtr + .asFunction Function(ffi.Pointer, int)>(); + ffi.Pointer valloc( - int arg0, + int __size, ) { return _valloc( - arg0, + __size, ); } @@ -415,6 +690,22 @@ class UpdaterBindings { late final _atexit = _atexitPtr.asFunction< int Function(ffi.Pointer>)>(); + int at_quick_exit( + ffi.Pointer> arg0, + ) { + return _at_quick_exit( + arg0, + ); + } + + late final _at_quick_exitPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer>)>>( + 'at_quick_exit'); + late final _at_quick_exit = _at_quick_exitPtr.asFunction< + int Function(ffi.Pointer>)>(); + double atof( ffi.Pointer arg0, ) { @@ -694,6 +985,18 @@ class UpdaterBindings { ffi.Int Function( ffi.Pointer, ffi.Pointer)>>)>(); + void quick_exit( + int arg0, + ) { + return _quick_exit( + arg0, + ); + } + + late final _quick_exitPtr = + _lookup>('quick_exit'); + late final _quick_exit = _quick_exitPtr.asFunction(); + int rand() { return _rand(); } @@ -1979,23 +2282,6 @@ class UpdaterBindings { _lookup>('srandomdev'); late final _srandomdev = _srandomdevPtr.asFunction(); - ffi.Pointer reallocf( - ffi.Pointer __ptr, - int __size, - ) { - return _reallocf( - __ptr, - __size, - ); - } - - late final _reallocfPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, ffi.Size)>>('reallocf'); - late final _reallocf = _reallocfPtr - .asFunction Function(ffi.Pointer, int)>(); - int strtonum( ffi.Pointer __numstr, int __minval, @@ -2071,20 +2357,34 @@ class UpdaterBindings { /// failure. If false is returned, the updater library will not be usable. bool shorebird_init( ffi.Pointer c_params, + FileCallbacks c_file_callbacks, ffi.Pointer c_yaml, ) { return _shorebird_init( c_params, + c_file_callbacks, c_yaml, ); } late final _shorebird_initPtr = _lookup< ffi.NativeFunction< - ffi.Bool Function(ffi.Pointer, + ffi.Bool Function(ffi.Pointer, FileCallbacks, ffi.Pointer)>>('shorebird_init'); late final _shorebird_init = _shorebird_initPtr.asFunction< - bool Function(ffi.Pointer, ffi.Pointer)>(); + bool Function( + ffi.Pointer, FileCallbacks, ffi.Pointer)>(); + + /// Returns if the app should run the updater automatically on launch. + bool shorebird_should_auto_update() { + return _shorebird_should_auto_update(); + } + + late final _shorebird_should_auto_updatePtr = + _lookup>( + 'shorebird_should_auto_update'); + late final _shorebird_should_auto_update = + _shorebird_should_auto_updatePtr.asFunction(); /// The currently running patch number, or 0 if the release has not been /// patched. @@ -2124,6 +2424,10 @@ class UpdaterBindings { .asFunction Function()>(); /// Free a string returned by the updater library. + /// # Safety + /// + /// If this function is called with a non-null pointer, it must be a pointer + /// returned by the updater library. void shorebird_free_string( ffi.Pointer c_string, ) { @@ -2138,6 +2442,20 @@ class UpdaterBindings { late final _shorebird_free_string = _shorebird_free_stringPtr .asFunction)>(); + void shorebird_free_update_result( + ffi.Pointer result, + ) { + return _shorebird_free_update_result( + result, + ); + } + + late final _shorebird_free_update_resultPtr = + _lookup)>>( + 'shorebird_free_update_result'); + 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(); @@ -2159,6 +2477,18 @@ class UpdaterBindings { late final _shorebird_update = _shorebird_updatePtr.asFunction(); + /// Synchronously download an update if one is available. + /// Returns an [UpdateResult] indicating whether the update was successful. + ffi.Pointer shorebird_update_with_result() { + return _shorebird_update_with_result(); + } + + late final _shorebird_update_with_resultPtr = + _lookup Function()>>( + 'shorebird_update_with_result'); + late final _shorebird_update_with_result = _shorebird_update_with_resultPtr + .asFunction Function()>(); + /// Start a thread to download an update if one is available. void shorebird_start_update_thread() { return _shorebird_start_update_thread(); @@ -2171,11 +2501,11 @@ class UpdaterBindings { _shorebird_start_update_threadPtr.asFunction(); /// Tell the updater that we're launching from what it told us was the - /// next patch to boot from. This will copy the next_boot patch to be the - /// current_boot patch. + /// next patch to boot from. This will copy the next boot patch to be the + /// `current_boot` patch. /// /// It is required to call this function before calling - /// shorebird_report_launch_success or shorebird_report_launch_failure. + /// `shorebird_report_launch_success` or `shorebird_report_launch_failure`. void shorebird_report_launch_start() { return _shorebird_report_launch_start(); } @@ -2310,10 +2640,20 @@ final class _opaque_pthread_t extends ffi.Struct { external ffi.Array __opaque; } -abstract class idtype_t { - static const int P_ALL = 0; - static const int P_PID = 1; - static const int P_PGID = 2; +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"), + }; } final class __darwin_arm_exception_state extends ffi.Struct { @@ -2328,6 +2668,7 @@ final class __darwin_arm_exception_state extends ffi.Struct { } typedef __uint32_t = ffi.UnsignedInt; +typedef Dart__uint32_t = int; final class __darwin_arm_exception_state64 extends ffi.Struct { @__uint64_t() @@ -2341,6 +2682,15 @@ final class __darwin_arm_exception_state64 extends ffi.Struct { } typedef __uint64_t = ffi.UnsignedLongLong; +typedef Dart__uint64_t = int; + +final class __darwin_arm_exception_state64_v2 extends ffi.Struct { + @__uint64_t() + external int __far; + + @__uint64_t() + external int __esr; +} final class __darwin_arm_thread_state extends ffi.Struct { @ffi.Array.multi([13]) @@ -2473,6 +2823,7 @@ final class __darwin_sigaltstack extends ffi.Struct { } typedef __darwin_size_t = ffi.UnsignedLong; +typedef Dart__darwin_size_t = int; final class __darwin_ucontext extends ffi.Struct { @ffi.Int() @@ -2551,6 +2902,7 @@ final class __siginfo extends ffi.Struct { typedef pid_t = __darwin_pid_t; typedef __darwin_pid_t = __int32_t; typedef __int32_t = ffi.Int; +typedef Dart__int32_t = int; typedef uid_t = __darwin_uid_t; typedef __darwin_uid_t = __uint32_t; @@ -2620,6 +2972,7 @@ final class timeval extends ffi.Struct { } typedef __darwin_time_t = ffi.Long; +typedef Dart__darwin_time_t = int; typedef __darwin_suseconds_t = __int32_t; final class rusage extends ffi.Struct { @@ -3256,7 +3609,22 @@ final class rusage_info_v6 extends ffi.Struct { @ffi.Uint64() external int ri_penergy_nj; - @ffi.Array.multi([14]) + @ffi.Uint64() + external int ri_secure_time_in_system; + + @ffi.Uint64() + external int ri_secure_ptime_in_system; + + @ffi.Uint64() + external int ri_neural_footprint; + + @ffi.Uint64() + external int ri_lifetime_max_neural_footprint; + + @ffi.Uint64() + external int ri_interval_max_neural_footprint; + + @ffi.Array.multi([9]) external ffi.Array ri_reserved; } @@ -3281,24 +3649,6 @@ final class proc_rlimit_control_wakeupmon extends ffi.Struct { typedef id_t = __darwin_id_t; typedef __darwin_id_t = __uint32_t; -@ffi.Packed(1) -final class _OSUnalignedU16 extends ffi.Struct { - @ffi.Uint16() - external int __val; -} - -@ffi.Packed(1) -final class _OSUnalignedU32 extends ffi.Struct { - @ffi.Uint32() - external int __val; -} - -@ffi.Packed(1) -final class _OSUnalignedU64 extends ffi.Struct { - @ffi.Uint64() - external int __val; -} - final class wait extends ffi.Opaque {} final class div_t extends ffi.Struct { @@ -3325,11 +3675,18 @@ final class lldiv_t extends ffi.Struct { external int rem; } +typedef malloc_type_id_t = ffi.UnsignedLongLong; +typedef Dartmalloc_type_id_t = int; + +final class _malloc_zone_t extends ffi.Opaque {} + +typedef malloc_zone_t = _malloc_zone_t; typedef dev_t = __darwin_dev_t; typedef __darwin_dev_t = __int32_t; typedef mode_t = __darwin_mode_t; typedef __darwin_mode_t = __uint16_t; typedef __uint16_t = ffi.UnsignedShort; +typedef Dart__uint16_t = int; /// Struct containing configuration parameters for the updater. /// Passed to all updater functions. @@ -3348,11 +3705,48 @@ final class AppParameters extends ffi.Struct { @ffi.Int() external int original_libapp_paths_size; - /// Path to cache_dir where the updater will store downloaded artifacts. - external ffi.Pointer cache_dir; + /// Path to app storage directory where the updater will store serialized + /// state and other data that persists between releases. + external ffi.Pointer app_storage_dir; + + /// Path to cache directory where the updater will store downloaded + /// artifacts and data that can be deleted when a new release is detected. + external ffi.Pointer code_cache_dir; } -const int __GNUC_VA_LIST = 1; +final class FileCallbacks extends ffi.Struct { + /// Opens the "file" (actually an in-memory buffer) and returns a handle. + external ffi.Pointer Function()>> + open; + + /// Reads count bytes from the file into buffer. Returns the number of + /// bytes read. + external ffi.Pointer< + ffi.NativeFunction< + ffi.UintPtr Function(ffi.Pointer file_handle, + ffi.Pointer buffer, ffi.UintPtr count)>> read; + + /// Moves the file pointer to the given offset relative from whence (one of + /// libc::SEEK_SET, libc::SEEK_CUR, or libc::SEEK_END). Returns the new + /// offset relative to the start of the file. + external ffi.Pointer< + ffi.NativeFunction< + ffi.Int64 Function(ffi.Pointer file_handle, + ffi.Int64 offset, ffi.Int32 whence)>> seek; + + /// Closes and frees the file handle. + external ffi.Pointer< + ffi + .NativeFunction file_handle)>> + close; +} + +final class UpdateResult extends ffi.Struct { + @ffi.Int32() + external int status; + + external ffi.Pointer message; +} const int __bool_true_false_are_defined = 1; @@ -3362,6 +3756,8 @@ const int false1 = 0; const int __WORDSIZE = 64; +const int __has_safe_buffers = 1; + const int __DARWIN_ONLY_64_BIT_INO_T = 1; const int __DARWIN_ONLY_UNIX_CONFORMANCE = 1; @@ -3420,8 +3816,6 @@ const int __PTHREAD_RWLOCK_SIZE__ = 192; const int __PTHREAD_RWLOCKATTR_SIZE__ = 16; -const int USER_ADDR_NULL = 0; - const int INT8_MAX = 127; const int INT16_MAX = 32767; @@ -3532,14 +3926,16 @@ const int __API_TO_BE_DEPRECATED_MACOS = 100000; const int __API_TO_BE_DEPRECATED_IOS = 100000; -const int __API_TO_BE_DEPRECATED_TVOS = 100000; +const int __API_TO_BE_DEPRECATED_MACCATALYST = 100000; const int __API_TO_BE_DEPRECATED_WATCHOS = 100000; -const int __API_TO_BE_DEPRECATED_MACCATALYST = 100000; +const int __API_TO_BE_DEPRECATED_TVOS = 100000; const int __API_TO_BE_DEPRECATED_DRIVERKIT = 100000; +const int __API_TO_BE_DEPRECATED_VISIONOS = 100000; + const int __MAC_10_0 = 1000; const int __MAC_10_1 = 1010; @@ -3596,6 +3992,8 @@ const int __MAC_10_14_1 = 101401; const int __MAC_10_14_4 = 101404; +const int __MAC_10_14_5 = 101405; + const int __MAC_10_14_6 = 101406; const int __MAC_10_15 = 101500; @@ -3626,6 +4024,14 @@ const int __MAC_12_2 = 120200; const int __MAC_12_3 = 120300; +const int __MAC_12_4 = 120400; + +const int __MAC_12_5 = 120500; + +const int __MAC_12_6 = 120600; + +const int __MAC_12_7 = 120700; + const int __MAC_13_0 = 130000; const int __MAC_13_1 = 130100; @@ -3634,6 +4040,26 @@ const int __MAC_13_2 = 130200; const int __MAC_13_3 = 130300; +const int __MAC_13_4 = 130400; + +const int __MAC_13_5 = 130500; + +const int __MAC_13_6 = 130600; + +const int __MAC_14_0 = 140000; + +const int __MAC_14_1 = 140100; + +const int __MAC_14_2 = 140200; + +const int __MAC_14_3 = 140300; + +const int __MAC_14_4 = 140400; + +const int __MAC_14_5 = 140500; + +const int __MAC_15_0 = 150000; + const int __IPHONE_2_0 = 20000; const int __IPHONE_2_1 = 20100; @@ -3738,6 +4164,8 @@ const int __IPHONE_14_3 = 140300; const int __IPHONE_14_5 = 140500; +const int __IPHONE_14_4 = 140400; + const int __IPHONE_14_6 = 140600; const int __IPHONE_14_7 = 140700; @@ -3754,6 +4182,14 @@ const int __IPHONE_15_3 = 150300; const int __IPHONE_15_4 = 150400; +const int __IPHONE_15_5 = 150500; + +const int __IPHONE_15_6 = 150600; + +const int __IPHONE_15_7 = 150700; + +const int __IPHONE_15_8 = 150800; + const int __IPHONE_16_0 = 160000; const int __IPHONE_16_1 = 160100; @@ -3764,6 +4200,122 @@ const int __IPHONE_16_3 = 160300; const int __IPHONE_16_4 = 160400; +const int __IPHONE_16_5 = 160500; + +const int __IPHONE_16_6 = 160600; + +const int __IPHONE_16_7 = 160700; + +const int __IPHONE_17_0 = 170000; + +const int __IPHONE_17_1 = 170100; + +const int __IPHONE_17_2 = 170200; + +const int __IPHONE_17_3 = 170300; + +const int __IPHONE_17_4 = 170400; + +const int __IPHONE_17_5 = 170500; + +const int __IPHONE_18_0 = 180000; + +const int __WATCHOS_1_0 = 10000; + +const int __WATCHOS_2_0 = 20000; + +const int __WATCHOS_2_1 = 20100; + +const int __WATCHOS_2_2 = 20200; + +const int __WATCHOS_3_0 = 30000; + +const int __WATCHOS_3_1 = 30100; + +const int __WATCHOS_3_1_1 = 30101; + +const int __WATCHOS_3_2 = 30200; + +const int __WATCHOS_4_0 = 40000; + +const int __WATCHOS_4_1 = 40100; + +const int __WATCHOS_4_2 = 40200; + +const int __WATCHOS_4_3 = 40300; + +const int __WATCHOS_5_0 = 50000; + +const int __WATCHOS_5_1 = 50100; + +const int __WATCHOS_5_2 = 50200; + +const int __WATCHOS_5_3 = 50300; + +const int __WATCHOS_6_0 = 60000; + +const int __WATCHOS_6_1 = 60100; + +const int __WATCHOS_6_2 = 60200; + +const int __WATCHOS_7_0 = 70000; + +const int __WATCHOS_7_1 = 70100; + +const int __WATCHOS_7_2 = 70200; + +const int __WATCHOS_7_3 = 70300; + +const int __WATCHOS_7_4 = 70400; + +const int __WATCHOS_7_5 = 70500; + +const int __WATCHOS_7_6 = 70600; + +const int __WATCHOS_8_0 = 80000; + +const int __WATCHOS_8_1 = 80100; + +const int __WATCHOS_8_3 = 80300; + +const int __WATCHOS_8_4 = 80400; + +const int __WATCHOS_8_5 = 80500; + +const int __WATCHOS_8_6 = 80600; + +const int __WATCHOS_8_7 = 80700; + +const int __WATCHOS_8_8 = 80800; + +const int __WATCHOS_9_0 = 90000; + +const int __WATCHOS_9_1 = 90100; + +const int __WATCHOS_9_2 = 90200; + +const int __WATCHOS_9_3 = 90300; + +const int __WATCHOS_9_4 = 90400; + +const int __WATCHOS_9_5 = 90500; + +const int __WATCHOS_9_6 = 90600; + +const int __WATCHOS_10_0 = 100000; + +const int __WATCHOS_10_1 = 100100; + +const int __WATCHOS_10_2 = 100200; + +const int __WATCHOS_10_3 = 100300; + +const int __WATCHOS_10_4 = 100400; + +const int __WATCHOS_10_5 = 100500; + +const int __WATCHOS_11_0 = 110000; + const int __TVOS_9_0 = 90000; const int __TVOS_9_1 = 90100; @@ -3830,6 +4382,10 @@ const int __TVOS_15_3 = 150300; const int __TVOS_15_4 = 150400; +const int __TVOS_15_5 = 150500; + +const int __TVOS_15_6 = 150600; + const int __TVOS_16_0 = 160000; const int __TVOS_16_1 = 160100; @@ -3840,77 +4396,113 @@ const int __TVOS_16_3 = 160300; const int __TVOS_16_4 = 160400; -const int __WATCHOS_1_0 = 10000; +const int __TVOS_16_5 = 160500; -const int __WATCHOS_2_0 = 20000; +const int __TVOS_16_6 = 160600; -const int __WATCHOS_2_1 = 20100; +const int __TVOS_17_0 = 170000; -const int __WATCHOS_2_2 = 20200; +const int __TVOS_17_1 = 170100; -const int __WATCHOS_3_0 = 30000; +const int __TVOS_17_2 = 170200; -const int __WATCHOS_3_1 = 30100; +const int __TVOS_17_3 = 170300; -const int __WATCHOS_3_1_1 = 30101; +const int __TVOS_17_4 = 170400; -const int __WATCHOS_3_2 = 30200; +const int __TVOS_17_5 = 170500; -const int __WATCHOS_4_0 = 40000; +const int __TVOS_18_0 = 180000; -const int __WATCHOS_4_1 = 40100; +const int __BRIDGEOS_2_0 = 20000; -const int __WATCHOS_4_2 = 40200; +const int __BRIDGEOS_3_0 = 30000; -const int __WATCHOS_4_3 = 40300; +const int __BRIDGEOS_3_1 = 30100; -const int __WATCHOS_5_0 = 50000; +const int __BRIDGEOS_3_4 = 30400; -const int __WATCHOS_5_1 = 50100; +const int __BRIDGEOS_4_0 = 40000; -const int __WATCHOS_5_2 = 50200; +const int __BRIDGEOS_4_1 = 40100; -const int __WATCHOS_5_3 = 50300; +const int __BRIDGEOS_5_0 = 50000; -const int __WATCHOS_6_0 = 60000; +const int __BRIDGEOS_5_1 = 50100; -const int __WATCHOS_6_1 = 60100; +const int __BRIDGEOS_5_3 = 50300; -const int __WATCHOS_6_2 = 60200; +const int __BRIDGEOS_6_0 = 60000; -const int __WATCHOS_7_0 = 70000; +const int __BRIDGEOS_6_2 = 60200; -const int __WATCHOS_7_1 = 70100; +const int __BRIDGEOS_6_4 = 60400; -const int __WATCHOS_7_2 = 70200; +const int __BRIDGEOS_6_5 = 60500; -const int __WATCHOS_7_3 = 70300; +const int __BRIDGEOS_6_6 = 60600; -const int __WATCHOS_7_4 = 70400; +const int __BRIDGEOS_7_0 = 70000; -const int __WATCHOS_7_5 = 70500; +const int __BRIDGEOS_7_1 = 70100; -const int __WATCHOS_7_6 = 70600; +const int __BRIDGEOS_7_2 = 70200; -const int __WATCHOS_8_0 = 80000; +const int __BRIDGEOS_7_3 = 70300; -const int __WATCHOS_8_1 = 80100; +const int __BRIDGEOS_7_4 = 70400; -const int __WATCHOS_8_3 = 80300; +const int __BRIDGEOS_7_6 = 70600; -const int __WATCHOS_8_4 = 80400; +const int __BRIDGEOS_8_0 = 80000; -const int __WATCHOS_8_5 = 80500; +const int __BRIDGEOS_8_1 = 80100; -const int __WATCHOS_9_0 = 90000; +const int __BRIDGEOS_8_2 = 80200; -const int __WATCHOS_9_1 = 90100; +const int __BRIDGEOS_8_3 = 80300; -const int __WATCHOS_9_2 = 90200; +const int __BRIDGEOS_8_4 = 80400; -const int __WATCHOS_9_3 = 90300; +const int __BRIDGEOS_8_5 = 80500; -const int __WATCHOS_9_4 = 90400; +const int __BRIDGEOS_9_0 = 90000; + +const int __DRIVERKIT_19_0 = 190000; + +const int __DRIVERKIT_20_0 = 200000; + +const int __DRIVERKIT_21_0 = 210000; + +const int __DRIVERKIT_22_0 = 220000; + +const int __DRIVERKIT_22_4 = 220400; + +const int __DRIVERKIT_22_5 = 220500; + +const int __DRIVERKIT_22_6 = 220600; + +const int __DRIVERKIT_23_0 = 230000; + +const int __DRIVERKIT_23_1 = 230100; + +const int __DRIVERKIT_23_2 = 230200; + +const int __DRIVERKIT_23_3 = 230300; + +const int __DRIVERKIT_23_4 = 230400; + +const int __DRIVERKIT_23_5 = 230500; + +const int __DRIVERKIT_24_0 = 240000; + +const int __VISIONOS_1_0 = 10000; + +const int __VISIONOS_1_1 = 10100; + +const int __VISIONOS_1_2 = 10200; + +const int __VISIONOS_2_0 = 20000; const int MAC_OS_X_VERSION_10_0 = 1000; @@ -3968,29 +4560,77 @@ const int MAC_OS_X_VERSION_10_14_1 = 101401; const int MAC_OS_X_VERSION_10_14_4 = 101404; +const int MAC_OS_X_VERSION_10_14_5 = 101405; + const int MAC_OS_X_VERSION_10_14_6 = 101406; const int MAC_OS_X_VERSION_10_15 = 101500; const int MAC_OS_X_VERSION_10_15_1 = 101501; +const int MAC_OS_X_VERSION_10_15_4 = 101504; + const int MAC_OS_X_VERSION_10_16 = 101600; const int MAC_OS_VERSION_11_0 = 110000; +const int MAC_OS_VERSION_11_1 = 110100; + +const int MAC_OS_VERSION_11_3 = 110300; + +const int MAC_OS_VERSION_11_4 = 110400; + +const int MAC_OS_VERSION_11_5 = 110500; + +const int MAC_OS_VERSION_11_6 = 110600; + const int MAC_OS_VERSION_12_0 = 120000; +const int MAC_OS_VERSION_12_1 = 120100; + +const int MAC_OS_VERSION_12_2 = 120200; + +const int MAC_OS_VERSION_12_3 = 120300; + +const int MAC_OS_VERSION_12_4 = 120400; + +const int MAC_OS_VERSION_12_5 = 120500; + +const int MAC_OS_VERSION_12_6 = 120600; + +const int MAC_OS_VERSION_12_7 = 120700; + const int MAC_OS_VERSION_13_0 = 130000; -const int __DRIVERKIT_19_0 = 190000; +const int MAC_OS_VERSION_13_1 = 130100; -const int __DRIVERKIT_20_0 = 200000; +const int MAC_OS_VERSION_13_2 = 130200; -const int __DRIVERKIT_21_0 = 210000; +const int MAC_OS_VERSION_13_3 = 130300; -const int __MAC_OS_X_VERSION_MIN_REQUIRED = 130000; +const int MAC_OS_VERSION_13_4 = 130400; -const int __MAC_OS_X_VERSION_MAX_ALLOWED = 130300; +const int MAC_OS_VERSION_13_5 = 130500; + +const int MAC_OS_VERSION_13_6 = 130600; + +const int MAC_OS_VERSION_14_0 = 140000; + +const int MAC_OS_VERSION_14_1 = 140100; + +const int MAC_OS_VERSION_14_2 = 140200; + +const int MAC_OS_VERSION_14_3 = 140300; + +const int MAC_OS_VERSION_14_4 = 140400; + +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_X_VERSION_MAX_ALLOWED = 150000; const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1; @@ -4072,6 +4712,8 @@ const int SIGUSR1 = 30; const int SIGUSR2 = 31; +const int USER_ADDR_NULL = 0; + const int __DARWIN_OPAQUE_ARM_THREAD_STATE64 = 0; const int SIGEV_NONE = 0; @@ -4388,6 +5030,10 @@ const int IOPOL_VFS_DISALLOW_RW_FOR_O_EVTONLY_DEFAULT = 0; const int IOPOL_VFS_DISALLOW_RW_FOR_O_EVTONLY_ON = 1; +const int IOPOL_VFS_NOCACHE_WRITE_FS_BLKSIZE_DEFAULT = 0; + +const int IOPOL_VFS_NOCACHE_WRITE_FS_BLKSIZE_ON = 1; + const int WNOHANG = 1; const int WUNTRACED = 2; @@ -4418,14 +5064,14 @@ const int __DARWIN_BIG_ENDIAN = 4321; const int __DARWIN_PDP_ENDIAN = 3412; -const int __DARWIN_BYTE_ORDER = 1234; - const int LITTLE_ENDIAN = 1234; const int BIG_ENDIAN = 4321; const int PDP_ENDIAN = 3412; +const int __DARWIN_BYTE_ORDER = 1234; + const int BYTE_ORDER = 1234; const int NULL = 0; @@ -4435,3 +5081,13 @@ const int EXIT_FAILURE = 1; const int EXIT_SUCCESS = 0; const int RAND_MAX = 2147483647; + +const int SHOREBIRD_UPDATE_ERROR = -1; + +const int SHOREBIRD_NO_UPDATE = 0; + +const int SHOREBIRD_UPDATE_INSTALLED = 1; + +const int SHOREBIRD_UPDATE_HAD_ERROR = 2; + +const int SHOREBIRD_UPDATE_IS_BAD_PATCH = 3; diff --git a/shorebird_code_push/lib/src/shorebird_code_push_base.dart b/shorebird_code_push/lib/src/shorebird_code_push_base.dart deleted file mode 100644 index b9500f6..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push_base.dart +++ /dev/null @@ -1,41 +0,0 @@ -/// {@template shorebird_code_push_base} -/// Get info about your Shorebird code push app. -/// {@endtemplate} -abstract class ShorebirdCodePushBase { - /// Whether the Shorebird Engine is available. - bool isShorebirdAvailable(); - - /// Checks whether a new patch is available for download. - /// - /// Returns true when there is a new patch for this app on Shorebird servers - /// but not yet downloaded to this device. - /// - /// Returns false in all other cases, including when a new patch is installed - /// locally but not yet booted from. - /// Use [isNewPatchReadyToInstall] to check if a new patch has been downloaded - /// and is ready to boot from on next restart. - /// - /// Runs in a separate isolate to avoid blocking the UI thread. - Future isNewPatchAvailableForDownload(); - - /// The version of the currently-installed patch. `null` if no patch is - /// installed (i.e., the app is running the release version). - /// - /// This will also return `null` if Shorebird is not available. - Future currentPatchNumber(); - - /// The version of the patch that will be run on the next app launch. If no - /// new patch has been downloaded, this will be the same as - /// [currentPatchNumber]. - Future nextPatchNumber(); - - /// Downloads the latest patch, if available. - /// Does nothing if there is no new patch available. - Future downloadUpdateIfAvailable(); - - /// Whether a new patch has been downloaded and is ready to install. - /// - /// If true, the patch number returned by [nextPatchNumber] will be run on the - /// next app launch. - Future isNewPatchReadyToInstall(); -} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart b/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart deleted file mode 100644 index 2a3a696..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push_ffi.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:isolate'; - -import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; -import 'package:shorebird_code_push/src/updater.dart'; - -/// {@template shorebird_code_push} -/// Get info about your Shorebird code push app. -/// {@endtemplate} -class ShorebirdCodePushFfi implements ShorebirdCodePushBase { - /// {@macro shorebird_code_push} - ShorebirdCodePushFfi({Updater? updater}) - : _updater = updater ?? const Updater(); - - final Updater _updater; - - @override - Future isNewPatchAvailableForDownload() { - return Isolate.run(_updater.checkForUpdate); - } - - @override - Future currentPatchNumber() { - return Isolate.run(() { - final currentPatchNumber = _updater.currentPatchNumber(); - // 0 means no patch is installed so we return null. - return currentPatchNumber == 0 ? null : currentPatchNumber; - }); - } - - @override - Future nextPatchNumber() { - return Isolate.run( - () { - final patchNumber = _updater.nextPatchNumber(); - // 0 means no patch is next so we return null. - return patchNumber == 0 ? null : patchNumber; - }, - ); - } - - @override - Future downloadUpdateIfAvailable() async { - await Isolate.run(_updater.downloadUpdate); - } - - @override - Future isNewPatchReadyToInstall() async { - final patchNumbers = await Future.wait([ - currentPatchNumber(), - nextPatchNumber(), - ]); - final currentPatch = patchNumbers[0]; - final nextPatch = patchNumbers[1]; - - return nextPatch != null && currentPatch != nextPatch; - } - - @override - bool isShorebirdAvailable() => true; -} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_io.dart b/shorebird_code_push/lib/src/shorebird_code_push_io.dart deleted file mode 100644 index 449ec54..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push_io.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_ffi.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; -import 'package:shorebird_code_push/src/updater.dart'; - -/// Applications should not import this file directly, but -/// import `package:shorebird_code_push/shorebird_code_push.dart` instead. - -/// {@template shorebird_code_push} -/// Get info about your Shorebird code push app. -/// {@endtemplate} -class ShorebirdCodePush implements ShorebirdCodePushBase { - /// {@macro shorebird_code_push} - ShorebirdCodePush() : this._(updater: const Updater()); - - /// Constructor used for testing which allows injecting a mock [Updater]. - @visibleForTesting - ShorebirdCodePush.test({Updater updater = const Updater()}) - : this._(updater: updater); - - ShorebirdCodePush._({required Updater updater}) { - try { - // If the Shorebird Engine is not available, this will throw an exception. - updater.currentPatchNumber(); - _delegate = ShorebirdCodePushFfi(updater: updater); - } catch (error) { - _delegate = ShorebirdCodePushNoop(); - } - } - - late final ShorebirdCodePushBase _delegate; - - @override - bool isShorebirdAvailable() => _delegate.isShorebirdAvailable(); - - @override - Future isNewPatchAvailableForDownload() { - return _delegate.isNewPatchAvailableForDownload(); - } - - @override - Future currentPatchNumber() => _delegate.currentPatchNumber(); - - @override - Future nextPatchNumber() => _delegate.nextPatchNumber(); - - @override - Future downloadUpdateIfAvailable() { - return _delegate.downloadUpdateIfAvailable(); - } - - @override - Future isNewPatchReadyToInstall() { - return _delegate.isNewPatchReadyToInstall(); - } -} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_noop.dart b/shorebird_code_push/lib/src/shorebird_code_push_noop.dart deleted file mode 100644 index a94acb5..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push_noop.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; - -/// {@template shorebird_code_push_noop} -/// A no-op implementation of [ShorebirdCodePushBase]. -/// -/// This is used when the build does not contain the Shorebird Engine. -/// {@endtemplate} -class ShorebirdCodePushNoop implements ShorebirdCodePushBase { - /// {@macro shorebird_code_push_noop} - ShorebirdCodePushNoop() { - // ignore: avoid_print - print(''' -[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. -This occurs when using package:shorebird_code_push in an app that does not -contain the Shorebird Engine. Most commonly this is due to building with -`flutter build` or `flutter run` instead of `shorebird release`. -'''); - } - @override - Future currentPatchNumber() async => null; - - @override - Future downloadUpdateIfAvailable() async {} - - @override - Future isNewPatchAvailableForDownload() async => false; - - @override - Future isNewPatchReadyToInstall() async => false; - - @override - bool isShorebirdAvailable() => false; - - @override - Future nextPatchNumber() async => null; -} diff --git a/shorebird_code_push/lib/src/shorebird_code_push_web.dart b/shorebird_code_push/lib/src/shorebird_code_push_web.dart deleted file mode 100644 index fcf788e..0000000 --- a/shorebird_code_push/lib/src/shorebird_code_push_web.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:shorebird_code_push/src/shorebird_code_push_base.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; - -/// Applications should not import this file directly, but -/// import `package:shorebird_code_push/shorebird_code_push.dart` instead. - -/// {@template shorebird_code_push} -/// Get info about your Shorebird code push app. -/// {@endtemplate} -class ShorebirdCodePush extends ShorebirdCodePushNoop - implements ShorebirdCodePushBase {} diff --git a/shorebird_code_push/lib/src/shorebird_updater.dart b/shorebird_code_push/lib/src/shorebird_updater.dart new file mode 100644 index 0000000..6b749ae --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_updater.dart @@ -0,0 +1,138 @@ +import 'package:shorebird_code_push/src/shorebird_updater_io.dart' + if (dart.library.js_interop) './shorebird_updater_web.dart'; +import 'package:shorebird_code_push/src/updater.dart'; + +/// The reason a call to [ShorebirdUpdater.update] failed. +enum UpdateFailureReason { + /// No update is available. + noUpdate, + + /// The update failed because the patch could not be downloaded. + downloadFailed, + + /// The update failed because the patch failed to install. + installFailed, + + /// The update failed for an unknown reason. + unknown, +} + +/// {@template read_patch_exception} +/// An exception thrown by [ShorebirdUpdater.readCurrentPatch] and +/// [ShorebirdUpdater.readNextPatch] when the read is unsuccessful. +/// {@endtemplate} +class ReadPatchException implements Exception { + /// {@macro update_exception} + const ReadPatchException({required this.message}); + + /// The human-readable error message. + final String message; +} + +/// {@template update_exception} +/// An exception thrown by [ShorebirdUpdater.update] when the update is +/// unsuccessful. +/// {@endtemplate} +class UpdateException implements Exception { + /// {@macro update_exception} + const UpdateException({required this.message, required this.reason}); + + /// The human-readable error message. + final String message; + + /// The reason the update failed. + final UpdateFailureReason reason; +} + +/// Log message when the Shorebird updater is unavailable in the current +/// environment. +void logShorebirdEngineUnavailableMessage() { + // ignore: avoid_print + print(''' +------------------------------------------------------------------------------- +The Shorebird Updater is unavailable in the current environment. +------------------------------------------------------------------------------- +This occurs when using pkg:shorebird_code_push in an app that does not +contain the Shorebird Engine. Most commonly this is due to building with +`flutter build` or `flutter run` instead of `shorebird release` or `shorebird preview`. +It can also occur when running on an unsupported platform (e.g. web or desktop). +'''); +} + +/// {@template patch} +/// An object representing a single patch (over-the-air update). +/// {@endtemplate} +class Patch { + /// {@macro patch} + const Patch({required this.number}); + + /// The patch number. + final int number; +} + +/// The current status of the app in terms of whether its up-to-date. +enum UpdateStatus { + /// The app is up to date (e.g. running the latest patch.) + upToDate, + + /// A new update is available for download. + outdated, + + /// The app is up to date, but a restart is required for the update to take + /// effect. + restartRequired, + + /// The update status is unavailable. This occurs when the updater is not + /// available in the current build. + /// See also: + /// * [ShorebirdUpdater.isAvailable] to determine if the updater is + /// available. + unavailable, +} + +/// {@template shorebird_updater} +/// Manage updates for a Shorebird app. +/// {@endtemplate} +abstract class ShorebirdUpdater { + /// {@macro shorebird_updater} + factory ShorebirdUpdater() => ShorebirdUpdaterImpl(const Updater()); + + /// Whether the updater is available on the current platform. + /// The most common reasons for this returning false are: + /// 1. The app is running in debug mode (Shorebird only supports release + /// mode). + /// 2. The app was *NOT* built using `shorebird release` and does *NOT* + /// contain the Shorebird engine. + bool get isAvailable; + + /// Returns information about the currently installed patch. + /// Returns `null` if no patch has been installed. + /// Returns `null` if the updater is not available. + /// Throws a [ReadPatchException] if the read is unsuccessful. + Future readCurrentPatch(); + + /// Returns information about the most recently downloaded patch. + /// Returns the same patch as [readCurrentPatch] if no new patch has been + /// downloaded. + /// Returns `null` if the updater is not available. + /// Throws a [ReadPatchException] if the read is unsuccessful. + Future readNextPatch(); + + /// 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(); + + /// Updates the app to the latest patch (if available). + /// Note: The app must be restarted for the update to take effect. + /// Note: This method does nothing if the updater is not available. + /// + /// Throws an [UpdateException] if a the update call is unsuccessful. + /// + /// See also: + /// * [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(); +} diff --git a/shorebird_code_push/lib/src/shorebird_updater_io.dart b/shorebird_code_push/lib/src/shorebird_updater_io.dart new file mode 100644 index 0000000..fd72122 --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_updater_io.dart @@ -0,0 +1,149 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +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'; +import 'package:shorebird_code_push/src/updater.dart'; + +@visibleForTesting + +/// Type definition for [Isolate.run]. +typedef IsolateRun = Future Function( + FutureOr Function(), { + String? debugName, +}); + +/// {@template shorebird_updater_io} +/// The Shorebird IO Updater. +/// {@endtemplate} +class ShorebirdUpdaterImpl implements ShorebirdUpdater { + /// {@macro shorebird_updater_io} + ShorebirdUpdaterImpl(this._updater, {IsolateRun? run}) + : _run = run ?? Isolate.run { + try { + // If the Shorebird Engine is not available, this will throw an exception. + // FIXME: Run this in an isolate or refactor the updater to avoid risking + // a hang. If another thread is also calling into Shorebird at the same + // time the underlying Rust code could block getting the config lock. + _updater.currentPatchNumber(); + _isAvailable = true; + } catch (_) { + logShorebirdEngineUnavailableMessage(); + _isAvailable = false; + } + } + + late final bool _isAvailable; + + final Updater _updater; + + final IsolateRun _run; + + @override + bool get isAvailable => _isAvailable; + + @override + Future readCurrentPatch() => _readPatch(_updater.currentPatchNumber); + + @override + Future readNextPatch() => _readPatch(_updater.nextPatchNumber); + + Future _readPatch(int Function() fn) async { + if (!_isAvailable) return null; + return _run( + () { + try { + final patchNumber = fn(); + return patchNumber > 0 ? Patch(number: patchNumber) : null; + } catch (error) { + throw ReadPatchException(message: '$error'); + } + }, + ); + } + + @override + Future checkForUpdate() async { + if (!_isAvailable) return UpdateStatus.unavailable; + + final isUpdateAvailable = await _run(_updater.checkForUpdate); + if (isUpdateAvailable) return UpdateStatus.outdated; + + final (current, next) = await (readCurrentPatch(), readNextPatch()).wait; + return next != null && current?.number != next.number + ? UpdateStatus.restartRequired + : UpdateStatus.upToDate; + } + + @override + Future update() async { + if (!_isAvailable) return; + + Pointer result = nullptr; + + try { + result = await _run(_updater.update); + } catch (_) { + return _legacyFallback(); + } + + const unknownErrorMessage = 'An unknown error occurred.'; + + try { + if (result == nullptr) { + throw const UpdateException( + reason: UpdateFailureReason.unknown, + message: unknownErrorMessage, + ); + } + + final status = result.ref.status; + + if (status == SHOREBIRD_UPDATE_INSTALLED) return; + + final reason = status.toFailureReason(); + final message = result.ref.message != nullptr + ? result.ref.message.cast().toDartString() + : unknownErrorMessage; + throw UpdateException(message: message, reason: reason); + } finally { + _updater.freeUpdateResult(result); + } + } + + // Fallback to downloadUpdate if update is not available. + Future _legacyFallback() async { + await _run(_updater.downloadUpdate); + final (current, next) = await (readCurrentPatch(), readNextPatch()).wait; + final status = next != null && current?.number != next.number + ? UpdateStatus.restartRequired + : UpdateStatus.upToDate; + if (status == UpdateStatus.restartRequired) return; + throw const UpdateException( + message: ''' +Downloading update failed but reason is unknown due to legacy updater. +Please upgrade the Shorebird Engine for improved error messages.''', + reason: UpdateFailureReason.unknown, + ); + } +} + +extension on int { + UpdateFailureReason toFailureReason() { + switch (this) { + case SHOREBIRD_NO_UPDATE: + return UpdateFailureReason.noUpdate; + case SHOREBIRD_UPDATE_HAD_ERROR: + return UpdateFailureReason.downloadFailed; + case SHOREBIRD_UPDATE_IS_BAD_PATCH: + return UpdateFailureReason.installFailed; + case SHOREBIRD_UPDATE_ERROR: + return UpdateFailureReason.unknown; + default: + return UpdateFailureReason.unknown; + } + } +} diff --git a/shorebird_code_push/lib/src/shorebird_updater_web.dart b/shorebird_code_push/lib/src/shorebird_updater_web.dart new file mode 100644 index 0000000..4fb8b23 --- /dev/null +++ b/shorebird_code_push/lib/src/shorebird_updater_web.dart @@ -0,0 +1,30 @@ +import 'package:shorebird_code_push/src/shorebird_updater.dart'; +import 'package:shorebird_code_push/src/updater.dart'; + +/// {@template shorebird_updater_web} +/// The Shorebird web updater. +/// {@endtemplate} +class ShorebirdUpdaterImpl implements ShorebirdUpdater { + /// {@macro shorebird_updater_web} + ShorebirdUpdaterImpl(this._updater) { + logShorebirdEngineUnavailableMessage(); + } + + // ignore: unused_field + final Updater _updater; + + @override + bool get isAvailable => false; + + @override + Future readCurrentPatch() async => null; + + @override + Future readNextPatch() async => null; + + @override + Future checkForUpdate() async => UpdateStatus.unavailable; + + @override + Future update() async {} +} diff --git a/shorebird_code_push/lib/src/updater.dart b/shorebird_code_push/lib/src/updater.dart index 66decec..4b1330c 100644 --- a/shorebird_code_push/lib/src/updater.dart +++ b/shorebird_code_push/lib/src/updater.dart @@ -1,4 +1,5 @@ import 'dart:ffi' as ffi; +import 'dart:ffi'; import 'package:meta/meta.dart'; import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart'; @@ -28,4 +29,12 @@ class Updater { /// Downloads the latest patch, if available. void downloadUpdate() => bindings.shorebird_update(); + + /// Downloads the latest patch, if available and returns an [UpdateResult] + /// to indicate whether the update was successful. + Pointer update() => bindings.shorebird_update_with_result(); + + /// Frees an update result allocated by the updater. + void freeUpdateResult(Pointer ptr) => + bindings.shorebird_free_update_result(ptr); } diff --git a/shorebird_code_push/pubspec.yaml b/shorebird_code_push/pubspec.yaml index 17ee988..597c82f 100644 --- a/shorebird_code_push/pubspec.yaml +++ b/shorebird_code_push/pubspec.yaml @@ -1,11 +1,12 @@ name: shorebird_code_push description: Check for and download Shorebird code push updates from your app. -version: 1.1.6 +version: 2.0.0-dev.1 homepage: https://shorebird.dev repository: https://github.com/shorebirdtech/updater/tree/main/shorebird_code_push environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.5.4 <4.0.0" + flutter: ">=3.24.4 <4.0.0" dependencies: ffi: ^2.0.2 @@ -13,9 +14,9 @@ dependencies: dev_dependencies: ffigen: ">=8.0.2 <16.0.0" - mocktail: ">=0.3.0 <2.0.0" + mocktail: ^1.0.0 test: ^1.19.2 - very_good_analysis: ">=5.0.0 <7.0.0" + very_good_analysis: ^6.0.0 ffigen: output: "lib/src/generated/updater_bindings.g.dart" diff --git a/shorebird_code_push/test/override_print.dart b/shorebird_code_push/test/override_print.dart new file mode 100644 index 0000000..f279d08 --- /dev/null +++ b/shorebird_code_push/test/override_print.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +void Function() overridePrint(void Function(List logs) fn) { + return () { + final printLogs = []; + final spec = ZoneSpecification( + print: (_, __, ___, String msg) { + printLogs.add(msg); + }, + ); + + return Zone.current + .fork(specification: spec) + .run(() => fn(printLogs)); + }; +} diff --git a/shorebird_code_push/test/shorebird_code_push_io_test.dart b/shorebird_code_push/test/shorebird_code_push_io_test.dart deleted file mode 100644 index 559d8a3..0000000 --- a/shorebird_code_push/test/shorebird_code_push_io_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:async'; - -import 'package:mocktail/mocktail.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_io.dart'; -import 'package:shorebird_code_push/src/updater.dart'; -import 'package:test/test.dart'; - -class _MockUpdater extends Mock implements Updater {} - -void main() { - group(ShorebirdCodePush, () { - late List printLogs; - late Updater updater; - late ShorebirdCodePush shorebirdCodePush; - - setUp(() { - printLogs = []; - updater = _MockUpdater(); - when(() => updater.currentPatchNumber()).thenReturn(0); - shorebirdCodePush = runZoned( - () => ShorebirdCodePush.test(updater: updater), - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) => printLogs.add(line), - ), - ); - }); - - test('can be instantiated', () { - shorebirdCodePush = runZoned( - ShorebirdCodePush.new, - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) => printLogs.add(line), - ), - ); - expect(shorebirdCodePush, isNotNull); - expect( - printLogs, - equals( - [ - ''' -[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. -This occurs when using package:shorebird_code_push in an app that does not -contain the Shorebird Engine. Most commonly this is due to building with -`flutter build` or `flutter run` instead of `shorebird release`.\n''', - ], - ), - ); - }); - - test('logs error when updater cannot be initialized', () { - final printLogs = []; - final exception = Exception('Failed to lookup symbol'); - when(() => updater.currentPatchNumber()).thenThrow(exception); - runZoned( - () => ShorebirdCodePush.test(updater: updater), - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) => printLogs.add(line), - ), - ); - expect( - printLogs, - equals( - [ - ''' -[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. -This occurs when using package:shorebird_code_push in an app that does not -contain the Shorebird Engine. Most commonly this is due to building with -`flutter build` or `flutter run` instead of `shorebird release`.\n''', - ], - ), - ); - }); - - group('isShorebirdAvailable', () { - test('proxies to delegate', () { - expect(shorebirdCodePush.isShorebirdAvailable(), isTrue); - }); - }); - - group('isNewPatchAvailableForDownload', () { - test('proxies to delegate', () { - when(() => updater.checkForUpdate()).thenReturn(true); - expectLater( - shorebirdCodePush.isNewPatchAvailableForDownload(), - completion(isTrue), - ); - }); - }); - - group('currentPatchNumber', () { - test('proxies to delegate', () { - when(() => updater.currentPatchNumber()).thenReturn(42); - expectLater( - shorebirdCodePush.currentPatchNumber(), - completion(equals(42)), - ); - }); - }); - - group('nextPatchNumber', () { - test('proxies to delegate', () { - when(() => updater.nextPatchNumber()).thenReturn(42); - expectLater( - shorebirdCodePush.nextPatchNumber(), - completion(equals(42)), - ); - }); - }); - - group('downloadUpdateIfAvailable', () { - test('proxies to delegate', () async { - when(() => updater.downloadUpdate()).thenAnswer((_) async {}); - await expectLater( - shorebirdCodePush.downloadUpdateIfAvailable(), - completes, - ); - }); - }); - - group('isNewPatchReadyToInstall', () { - test('proxies to delegate', () async { - when(() => updater.currentPatchNumber()).thenReturn(0); - when(() => updater.nextPatchNumber()).thenReturn(1); - await expectLater( - shorebirdCodePush.isNewPatchReadyToInstall(), - completion(isTrue), - ); - }); - }); - }); -} diff --git a/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart b/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart deleted file mode 100644 index efaaa97..0000000 --- a/shorebird_code_push/test/src/shorebird_code_push_ffi_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:mocktail/mocktail.dart'; -import 'package:shorebird_code_push/src/shorebird_code_push_ffi.dart'; -import 'package:shorebird_code_push/src/updater.dart'; -import 'package:test/test.dart'; - -class _MockUpdater extends Mock implements Updater {} - -void main() { - group(ShorebirdCodePushFfi, () { - late Updater updater; - late ShorebirdCodePushFfi shorebirdCodePush; - - setUp(() { - updater = _MockUpdater(); - shorebirdCodePush = ShorebirdCodePushFfi(updater: updater); - }); - - group('isShorebirdAvailable', () { - test('returns true', () { - expect(shorebirdCodePush.isShorebirdAvailable(), isTrue); - }); - }); - - group('isNewPatchAvailableForDownload', () { - test('returns false if no update is available', () async { - when(() => updater.checkForUpdate()).thenAnswer((_) => false); - await expectLater( - shorebirdCodePush.isNewPatchAvailableForDownload(), - completion(isFalse), - ); - }); - - test('returns true if an update is available', () async { - when(() => updater.checkForUpdate()).thenAnswer((_) => true); - await expectLater( - shorebirdCodePush.isNewPatchAvailableForDownload(), - completion(isTrue), - ); - }); - - test('surfaces exception if updater throws exception', () async { - when(() => updater.checkForUpdate()).thenThrow(Exception('oh no')); - await expectLater( - () => shorebirdCodePush.isNewPatchAvailableForDownload(), - throwsException, - ); - }); - }); - - group('currentPatchNumber', () { - test('returns null if current patch is reported as 0', () async { - when(() => updater.currentPatchNumber()).thenReturn(0); - await expectLater( - shorebirdCodePush.currentPatchNumber(), - completion(isNull), - ); - }); - - test('forwards the return value of updater.currentPatchNumber', () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - await expectLater( - shorebirdCodePush.currentPatchNumber(), - completion(equals(1)), - ); - }); - - test('surfaces exception if updater throws exception', () async { - when(() => updater.currentPatchNumber()).thenThrow(Exception('oh no')); - await expectLater( - () => shorebirdCodePush.currentPatchNumber(), - throwsException, - ); - }); - }); - - group('nextPatchNumber', () { - test('returns null if current patch is reported as 0', () async { - when(() => updater.nextPatchNumber()).thenReturn(0); - await expectLater( - shorebirdCodePush.nextPatchNumber(), - completion(isNull), - ); - }); - - test('forwards the return value of updater.nextPatchNumber', () async { - when(() => updater.nextPatchNumber()).thenReturn(1); - await expectLater( - shorebirdCodePush.nextPatchNumber(), - completion(equals(1)), - ); - }); - - test('surfaces exception if updater throws exception', () async { - when(() => updater.nextPatchNumber()).thenThrow(Exception('oh no')); - await expectLater( - () => shorebirdCodePush.nextPatchNumber(), - throwsException, - ); - }); - }); - - group('downloadUpdate', () { - test('completes', () async { - when(() => updater.downloadUpdate()).thenReturn(null); - await expectLater( - shorebirdCodePush.downloadUpdateIfAvailable(), - completes, - ); - }); - - test('surfaces exception if updater throws exception', () async { - when(() => updater.downloadUpdate()).thenThrow(Exception('oh no')); - await expectLater( - () => shorebirdCodePush.downloadUpdateIfAvailable(), - throwsException, - ); - }); - }); - - group('isNewPatchReadyToInstall', () { - test('returns false if no new patch is available', () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(0); - await expectLater( - shorebirdCodePush.isNewPatchReadyToInstall(), - completion(isFalse), - ); - }); - - test( - 'returns false if the next patch is the same as the current patch', - () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(1); - await expectLater( - shorebirdCodePush.isNewPatchReadyToInstall(), - completion(isFalse), - ); - }, - ); - - test( - 'returns true if the next patch number is greater ' - 'than the current patch number', () async { - when(() => updater.currentPatchNumber()).thenReturn(1); - when(() => updater.nextPatchNumber()).thenReturn(2); - await expectLater( - shorebirdCodePush.isNewPatchReadyToInstall(), - completion(isTrue), - ); - }); - }); - }); -} diff --git a/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart b/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart deleted file mode 100644 index 04c98c4..0000000 --- a/shorebird_code_push/test/src/shorebird_code_push_noop_test.dart +++ /dev/null @@ -1,74 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'dart:async'; - -import 'package:shorebird_code_push/src/shorebird_code_push_noop.dart'; -import 'package:test/test.dart'; - -void main() { - group(ShorebirdCodePushNoop, () { - late List printLogs; - late ShorebirdCodePushNoop shorebirdCodePush; - - setUp(() { - printLogs = []; - shorebirdCodePush = runZoned( - ShorebirdCodePushNoop.new, - zoneSpecification: ZoneSpecification( - print: (self, parent, zone, line) => printLogs.add(line), - ), - ); - }); - - test('logs warning when instantiated', () { - const expected = ''' -[ShorebirdCodePush]: Shorebird Engine not available, using no-op implementation. -This occurs when using package:shorebird_code_push in an app that does not -contain the Shorebird Engine. Most commonly this is due to building with -`flutter build` or `flutter run` instead of `shorebird release`.\n'''; - expect(printLogs, equals([expected])); - }); - - group('isShorebirdAvailable', () { - test('returns false', () { - expect(shorebirdCodePush.isShorebirdAvailable(), isFalse); - }); - }); - - group('isNewPatchAvailableForDownload', () { - test('returns false', () { - expectLater( - shorebirdCodePush.isNewPatchAvailableForDownload(), - completion(isFalse), - ); - }); - }); - - group('currentPatchNumber', () { - test('returns null', () { - expectLater(shorebirdCodePush.currentPatchNumber(), completion(isNull)); - }); - }); - - group('nextPatchNumber', () { - test('returns null', () { - expectLater(shorebirdCodePush.nextPatchNumber(), completion(isNull)); - }); - }); - - group('downloadUpdate', () { - test('completes', () { - expectLater(shorebirdCodePush.downloadUpdateIfAvailable(), completes); - }); - }); - - group('isNewPatchReadyToInstall', () { - test('returns false', () { - expectLater( - shorebirdCodePush.isNewPatchReadyToInstall(), - completion(isFalse), - ); - }); - }); - }); -} diff --git a/shorebird_code_push/test/src/shorebird_updater_io_test.dart b/shorebird_code_push/test/src/shorebird_updater_io_test.dart new file mode 100644 index 0000000..3fc9dc8 --- /dev/null +++ b/shorebird_code_push/test/src/shorebird_updater_io_test.dart @@ -0,0 +1,570 @@ +import 'dart:async'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart'; +import 'package:shorebird_code_push/src/shorebird_updater.dart'; +import 'package:shorebird_code_push/src/shorebird_updater_io.dart'; +import 'package:shorebird_code_push/src/updater.dart'; +import 'package:test/test.dart'; + +import '../override_print.dart'; + +class _MockUpdater extends Mock implements Updater {} + +Future run( + FutureOr Function() computation, { + String? debugName, +}) async { + return computation(); +} + +void main() { + group(ShorebirdUpdaterImpl, () { + late Updater updater; + late ShorebirdUpdaterImpl shorebirdUpdater; + + setUpAll(() { + registerFallbackValue(Pointer.fromAddress(0)); + }); + + setUp(() { + updater = _MockUpdater(); + }); + + group('isAvailable', () { + group('when updater is available', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(1); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns true', () { + expect(shorebirdUpdater.isAvailable, isTrue); + }); + }); + + group('when updater is unavailable', () { + setUp(() { + when(updater.currentPatchNumber).thenThrow(Exception('oops')); + }); + + test( + 'returns false', + overridePrint((_) { + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + expect(shorebirdUpdater.isAvailable, isFalse); + }), + ); + }); + }); + + group('readPatch', () { + group('when updater is unavailable', () { + setUp(() { + when(updater.currentPatchNumber).thenThrow(Exception('oops')); + }); + + test( + 'returns null', + overridePrint((_) async { + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion(isNull), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion(isNull), + ); + }), + ); + }); + + group('when updater has no installed patches', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.nextPatchNumber).thenReturn(0); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns null', () async { + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion(isNull), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion(isNull), + ); + }); + }); + + group('when updater has a downloaded patch', () { + const currentPatchNumber = 0; + const nextPatchNumber = 1; + setUp(() { + when(updater.currentPatchNumber).thenReturn(currentPatchNumber); + when(updater.nextPatchNumber).thenReturn(nextPatchNumber); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns correct patch numbers', () async { + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion(isNull), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion( + isA().having( + (p) => p.number, + 'number', + nextPatchNumber, + ), + ), + ); + }); + }); + + group('when updater has an installed patch', () { + const currentPatchNumber = 1; + const nextPatchNumber = 1; + setUp(() { + when(updater.currentPatchNumber).thenReturn(currentPatchNumber); + when(updater.nextPatchNumber).thenReturn(nextPatchNumber); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns correct patch numbers', () async { + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion( + isA().having( + (p) => p.number, + 'number', + currentPatchNumber, + ), + ), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion( + isA().having( + (p) => p.number, + 'number', + nextPatchNumber, + ), + ), + ); + }); + }); + + group( + 'when updater has an installed patch ' + 'and a new downloaded patch', () { + const currentPatchNumber = 1; + const nextPatchNumber = 2; + setUp(() { + when(updater.currentPatchNumber).thenReturn(currentPatchNumber); + when(updater.nextPatchNumber).thenReturn(nextPatchNumber); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns correct patch numbers', () async { + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion( + isA().having( + (p) => p.number, + 'number', + currentPatchNumber, + ), + ), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion( + isA().having( + (p) => p.number, + 'number', + nextPatchNumber, + ), + ), + ); + }); + }); + + group('when an exception occurs trying to read patches', () { + final currentPatchNumberReturnValues = [0, -1]; + setUp(() { + when(updater.currentPatchNumber).thenAnswer((_) { + final value = currentPatchNumberReturnValues.removeAt(0); + if (value < 0) throw Exception('oops'); + return value; + }); + when(updater.nextPatchNumber).thenThrow(Exception('oops')); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $ReadPatchException', () async { + await expectLater( + () => shorebirdUpdater.readCurrentPatch(), + throwsA(isA()), + ); + await expectLater( + () => shorebirdUpdater.readNextPatch(), + throwsA(isA()), + ); + }); + }); + }); + + group('checkForUpdate', () { + group('when updater is unavailable', () { + setUp(() { + when(updater.currentPatchNumber).thenThrow(Exception('oops')); + }); + + test( + 'returns UpdateStatus.unavailable', + overridePrint((_) async { + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.unavailable)), + ); + }), + ); + }); + + group('when updater has an update available', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.checkForUpdate).thenReturn(true); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns UpdateStatus.outdated', () async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.outdated)), + ); + }); + }); + + group('when updater has downloaded an update', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.nextPatchNumber).thenReturn(1); + when(updater.checkForUpdate).thenReturn(false); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns UpdateStatus.restartRequired', () async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.restartRequired)), + ); + }); + }); + + group('when updater installed an update and is up to date', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(1); + when(updater.nextPatchNumber).thenReturn(1); + when(updater.checkForUpdate).thenReturn(false); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('returns UpdateStatus.upToDate', () async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.upToDate)), + ); + }); + }); + }); + + group('update', () { + group('when updater is unavailable', () { + setUp(() { + when(updater.currentPatchNumber).thenThrow(Exception('oops')); + }); + + test( + 'does nothing', + overridePrint((_) async { + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + await expectLater(shorebirdUpdater.update(), completes); + verifyNever(updater.downloadUpdate); + }), + ); + }); + + group('when a nullptr result is returned', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + when(() => updater.update()).thenReturn(nullptr); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'An unknown error occurred.', + ) + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.unknown, + ), + ), + ); + verify(updater.update).called(1); + }); + }); + + group('when no update is available', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = SHOREBIRD_NO_UPDATE; + result.ref.message = 'oops'.toNativeUtf8().cast(); + addTearDown(() { + calloc + ..free(result.ref.message) + ..free(result); + }); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'oops', + ) + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.noUpdate, + ), + ), + ); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + + group('when an error occurs during download', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = SHOREBIRD_UPDATE_HAD_ERROR; + result.ref.message = 'oops'.toNativeUtf8().cast(); + addTearDown(() { + calloc + ..free(result.ref.message) + ..free(result); + }); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'oops', + ) + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.downloadFailed, + ), + ), + ); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + + group('when the downloaded patch is bad', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = SHOREBIRD_UPDATE_IS_BAD_PATCH; + result.ref.message = 'oops'.toNativeUtf8().cast(); + addTearDown(() { + calloc + ..free(result.ref.message) + ..free(result); + }); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having((e) => e.message, 'message', 'oops') + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.installFailed, + ), + ), + ); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + + group('when an unknown error occurs', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = SHOREBIRD_UPDATE_ERROR; + addTearDown(() => calloc.free(result)); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'An unknown error occurred.', + ) + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.unknown, + ), + ), + ); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + + group('when an outdated version of the engine is used', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.nextPatchNumber).thenReturn(1); + when(() => updater.update()).thenThrow(Exception('oops')); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('falls back to downloadUpdate', () async { + await expectLater(shorebirdUpdater.update(), completes); + verify(updater.update).called(1); + verify(updater.downloadUpdate).called(1); + verifyNever(() => updater.freeUpdateResult(any())); + }); + + group('when update fails', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + when(updater.nextPatchNumber).thenReturn(0); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws if legacy update fails', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA().having( + (e) => e.message, + 'message', + ''' +Downloading update failed but reason is unknown due to legacy updater. +Please upgrade the Shorebird Engine for improved error messages.''', + ).having( + (e) => e.reason, + 'reason', + UpdateFailureReason.unknown, + ), + ), + ); + verify(updater.update).called(1); + verify(updater.downloadUpdate).called(1); + verifyNever(() => updater.freeUpdateResult(any())); + }); + }); + }); + + group('when an unsupported status code is returned', () { + setUp(() { + when(() => updater.currentPatchNumber()).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = -42; // invalid status code + result.ref.message = nullptr; + addTearDown(() => calloc.free(result)); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('throws $UpdateException', () async { + await expectLater( + shorebirdUpdater.update, + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'An unknown error occurred.', + ) + .having( + (e) => e.reason, + 'reason', + UpdateFailureReason.unknown, + ), + ), + ); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + + group('when download succeeds', () { + setUp(() { + when(updater.currentPatchNumber).thenReturn(0); + final result = calloc.allocate(sizeOf()); + result.ref.status = SHOREBIRD_UPDATE_INSTALLED; + addTearDown(() => calloc.free(result)); + when(() => updater.update()).thenReturn(result); + shorebirdUpdater = ShorebirdUpdaterImpl(updater, run: run); + }); + + test('completes', () async { + await expectLater(shorebirdUpdater.update(), completes); + verify(updater.update).called(1); + verify(() => updater.freeUpdateResult(any())).called(1); + }); + }); + }); + }); +} diff --git a/shorebird_code_push/test/src/shorebird_updater_test.dart b/shorebird_code_push/test/src/shorebird_updater_test.dart new file mode 100644 index 0000000..4603acb --- /dev/null +++ b/shorebird_code_push/test/src/shorebird_updater_test.dart @@ -0,0 +1,15 @@ +import 'package:shorebird_code_push/shorebird_code_push.dart'; +import 'package:test/test.dart'; + +import '../override_print.dart'; + +void main() { + group(ShorebirdUpdater, () { + test( + 'can be instantiated', + overridePrint((_) { + expect(ShorebirdUpdater.new, returnsNormally); + }), + ); + }); +} diff --git a/shorebird_code_push/test/src/shorebird_updater_web_test.dart b/shorebird_code_push/test/src/shorebird_updater_web_test.dart new file mode 100644 index 0000000..6bdcbb5 --- /dev/null +++ b/shorebird_code_push/test/src/shorebird_updater_web_test.dart @@ -0,0 +1,87 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_code_push/src/shorebird_updater.dart'; +import 'package:shorebird_code_push/src/shorebird_updater_web.dart'; +import 'package:shorebird_code_push/src/updater.dart'; +import 'package:test/test.dart'; + +import '../override_print.dart'; + +class _MockUpdater extends Mock implements Updater {} + +void main() { + group(ShorebirdUpdaterImpl, () { + late Updater updater; + late ShorebirdUpdaterImpl shorebirdUpdater; + + setUp(() { + updater = _MockUpdater(); + }); + + test( + 'logs unavailable error', + overridePrint((logs) { + shorebirdUpdater = ShorebirdUpdaterImpl(updater); + expect( + logs, + contains( + isA().having( + (s) => s, + 'message', + contains( + '''The Shorebird Updater is unavailable in the current environment.''', + ), + ), + ), + ); + }), + ); + + group('isAvailable', () { + test( + 'returns false', + overridePrint((_) { + shorebirdUpdater = ShorebirdUpdaterImpl(updater); + expect(shorebirdUpdater.isAvailable, isFalse); + }), + ); + }); + + group('readPatch', () { + test( + 'returns null', + overridePrint((_) async { + await expectLater( + shorebirdUpdater.readCurrentPatch(), + completion(isNull), + ); + await expectLater( + shorebirdUpdater.readNextPatch(), + completion(isNull), + ); + }), + ); + }); + + group('checkForUpdate', () { + test( + 'returns UpdateStatus.unavailable', + overridePrint((_) async { + await expectLater( + shorebirdUpdater.checkForUpdate(), + completion(equals(UpdateStatus.unavailable)), + ); + }), + ); + }); + + group('update', () { + test( + 'does nothing', + overridePrint((_) async { + await expectLater(shorebirdUpdater.update(), completes); + verifyNever(updater.downloadUpdate); + }), + ); + }); + }); +} diff --git a/shorebird_code_push/test/src/updater_test.dart b/shorebird_code_push/test/src/updater_test.dart index b848851..bc54283 100644 --- a/shorebird_code_push/test/src/updater_test.dart +++ b/shorebird_code_push/test/src/updater_test.dart @@ -1,3 +1,6 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shorebird_code_push/src/generated/updater_bindings.g.dart'; import 'package:shorebird_code_push/src/updater.dart'; @@ -10,6 +13,10 @@ void main() { late UpdaterBindings updaterBindings; late Updater updater; + setUpAll(() { + registerFallbackValue(Pointer.fromAddress(0)); + }); + setUp(() { updaterBindings = _MockUpdaterBindings(); @@ -62,5 +69,25 @@ void main() { verify(() => updaterBindings.shorebird_update()).called(1); }); }); + + group('update', () { + test('calls bindings.shorebird_update_with_result', () { + when( + () => updaterBindings.shorebird_update_with_result(), + ).thenReturn(nullptr); + updater.update(); + verify(() => updaterBindings.shorebird_update_with_result()).called(1); + }); + }); + + group('freeUpdateResult', () { + test('calls bindings.shorebird_free_update_result', () { + final result = calloc.allocate(sizeOf()); + updater.freeUpdateResult(result); + verify( + () => updaterBindings.shorebird_free_update_result(any()), + ).called(1); + }); + }); }); }