feat(shorebird_code_push): track support (#232)

* feat(shorebird_code_push): track support

* cleanup and add todos

* docs

* run ffigen

* use c_char instead of char

* Add channel support

* tests

* tests

* update podfile.lock

* Update example to include tracks selector

---------

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