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
+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);
});
});