Compare commits
6 Commits
3ac748ff28
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 479de106d7 | |||
| 1f85c4ab1e | |||
| dd213f923c | |||
| 6e9aab2ce7 | |||
| 5d4e9c3396 | |||
| 83df31b60e |
@@ -76,7 +76,7 @@ runs:
|
||||
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
flags: ${{ steps.split.outputs.package_name }}
|
||||
token: ${{ inputs.codecov_token }}
|
||||
|
||||
@@ -82,7 +82,7 @@ runs:
|
||||
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
# We use Codecov's carryforward flags to allow our PR testing to only
|
||||
# run affected packages, but also allow our coverage information from
|
||||
|
||||
@@ -10,7 +10,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: 📚 Git Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: 🐦 Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
|
||||
@@ -41,7 +41,7 @@ runs:
|
||||
run: echo "package_name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
# We use Codecov's carryforward flags to allow our PR testing to only
|
||||
# run affected packages, but also allow our coverage information from
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup Flutter
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
working-directory: ${{ inputs.package_path }}
|
||||
run: flutter test --coverage
|
||||
- if: inputs.has_unit_tests
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
flags: ${{ inputs.package_name }}
|
||||
working-directory: ${{ inputs.package_path }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: 🦀 Upload Artifacts
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: taiki-e/upload-rust-binary-action@v1
|
||||
with:
|
||||
bin: "patch"
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📚 Git Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- uses: dorny/paths-filter@v4
|
||||
name: Build Detection
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📚 Git Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: 🦀 Build ${{ matrix.crate }}
|
||||
uses: ./.github/actions/rust_crate
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📚 Git Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📚 Git Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
shorebird_code_push: ${{ steps.filter.outputs.shorebird_code_push }}
|
||||
shorebird_code_push_example: ${{ steps.filter.outputs.shorebird_code_push_example }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history so dorny/paths-filter can diff on push events.
|
||||
fetch-depth: 0
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
name: CSpell
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: streetsidesoftware/cspell-action@v8
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ oslog = "0.2.0"
|
||||
simple_logger = "5.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.14.0"
|
||||
mockall = "0.15.0"
|
||||
mockito = "1.2.0"
|
||||
mock_instant = "0.6.0"
|
||||
# Gives #[serial] attribute for locking all of our shorebird_init
|
||||
|
||||
@@ -89,8 +89,7 @@ bool shorebird_check_for_downloadable_update(const char *c_channel);
|
||||
* to their own stable account/device identifier. The updater persists this
|
||||
* value in state.json after a successful call.
|
||||
*/
|
||||
SHOREBIRD_EXPORT
|
||||
bool shorebird_set_device_id_override(const char *c_device_id);
|
||||
SHOREBIRD_EXPORT bool shorebird_set_device_id_override(const char *c_device_id);
|
||||
|
||||
/**
|
||||
* Synchronously download an update on the first non-null channel of:
|
||||
|
||||
@@ -169,7 +169,8 @@ pub(crate) fn open_base_lib(apks_dir: &Path, lib_name: &str) -> anyhow::Result<B
|
||||
// https://chromium.googlesource.com/chromium/src/base/+/a5ca5def0453df367b9c42e9817a33d2a21e75fe/android/java/src/org/chromium/base/library_loader/Linker.java
|
||||
// Previously I tried reading libapp.so from from the AssetManager, but
|
||||
// it does show the lib/ directory in the list of assets.
|
||||
// https://github.com/shorebirdtech/updater/pull/6
|
||||
// Historical updater context: this avoids reading libapp.so via
|
||||
// AssetManager.
|
||||
|
||||
// Ideally we would do this apk reading from the C++ side and keep the rust
|
||||
// portable, but we have a zip library here, and don't on the C++ side.
|
||||
@@ -245,7 +246,7 @@ pub fn libapp_path_from_settings(original_libapp_paths: &[String]) -> Result<Pat
|
||||
// path to the libapp.so file. This is true for the current engine, but
|
||||
// may not be true in the future. Better would be for the engine to
|
||||
// pass us the path to the base.apk.
|
||||
// https://github.com/shorebirdtech/shorebird/issues/283
|
||||
// This is fragile because Flutter passes multiple libapp paths here.
|
||||
// This is where the paths are set today:
|
||||
// First path is "libapp.so" (for dlopen), second is a full path:
|
||||
// https://github.com/flutter/engine/blob/a7c9cc58a71c5850be0215ab1997db92cc5e8d3e/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java#L264
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// buckets. They are not `extern "C"`, so cbindgen never emits them.
|
||||
//
|
||||
// Engine-side usage lives at `engine/src/flutter/shell/common/shorebird/updater.cc`
|
||||
// in the Shorebird Flutter monorepo: <https://github.com/shorebirdtech/flutter>.
|
||||
// in the open Flutter fork: <https://github.com/tony-cloud/flutter>.
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -72,7 +72,7 @@ pub struct UpdaterState {
|
||||
struct SerializedState {
|
||||
/// Stable per-install ID. Survives release-version changes; only
|
||||
/// reset when the app is uninstalled. Used for analytics.
|
||||
/// <https://shorebird.dev/privacy/>
|
||||
/// Treat this as privacy-sensitive update-server metadata.
|
||||
client_id: String,
|
||||
/// The release version this cache corresponds to. Mismatch with the
|
||||
/// app's reported release version triggers a wipe of all per-release
|
||||
|
||||
+24
-6
@@ -13,12 +13,7 @@ use std::sync::Mutex;
|
||||
// cbindgen looks for const, ignore these so it doesn't warn about them.
|
||||
|
||||
/// cbindgen:ignore
|
||||
#[cfg(test)]
|
||||
const DEFAULT_BASE_URL: &str = "DEFAULT_BASE_URL should be mocked using mockito::Server";
|
||||
|
||||
/// cbindgen:ignore
|
||||
#[cfg(not(test))]
|
||||
const DEFAULT_BASE_URL: &str = "https://api.shorebird.dev";
|
||||
const DEFAULT_BASE_URL: &str = "http://localhost:8080";
|
||||
|
||||
/// cbindgen:ignore
|
||||
const DEFAULT_CHANNEL: &str = "stable";
|
||||
@@ -279,6 +274,29 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// These tests are serial because they modify global state.
|
||||
#[serial]
|
||||
#[test]
|
||||
fn set_config_defaults_to_open_self_hosted_server() -> Result<()> {
|
||||
testing_reset_config();
|
||||
|
||||
let mut yaml = fake_yaml();
|
||||
yaml.base_url = None;
|
||||
|
||||
set_config(
|
||||
fake_app_config(),
|
||||
Box::new(FakeExternalFileProvider {}),
|
||||
"first_path".into(),
|
||||
&yaml,
|
||||
NetworkHooks::default(),
|
||||
)?;
|
||||
|
||||
let config = super::with_config(|config| Ok(config.clone())).unwrap();
|
||||
assert_eq!(config.base_url, super::DEFAULT_BASE_URL);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// These tests are serial because they modify global state.
|
||||
#[serial]
|
||||
#[test]
|
||||
|
||||
@@ -46,9 +46,8 @@ impl<'de> Deserialize<'de> for EventType {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Any edits to this struct should be made carefully and in accordance
|
||||
/// with our privacy policy:
|
||||
/// <https://docs.shorebird.dev/privacy>
|
||||
/// Any edits to this struct should be made carefully and in accordance with
|
||||
/// the configured update server's privacy policy.
|
||||
/// An event that is sent to the server when a patch is successfully installed.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct PatchEvent {
|
||||
|
||||
@@ -219,17 +219,15 @@ pub struct Patch {
|
||||
pub hash_signature: Option<String>,
|
||||
}
|
||||
|
||||
/// Any edits to this struct should be made carefully and in accordance
|
||||
/// with our privacy policy:
|
||||
/// <https://docs.shorebird.dev/privacy>
|
||||
/// Any edits to this struct should be made carefully and in accordance with
|
||||
/// the configured update server's privacy policy.
|
||||
/// The request body for the patch check endpoint.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PatchCheckRequest {
|
||||
/// The Shorebird app_id built into the shorebird.yaml in the app.
|
||||
/// app_ids are unique to each app and are used to identify the app
|
||||
/// within Shorebird's system (similar to a bundle identifier). They
|
||||
/// within the update server (similar to a bundle identifier). They
|
||||
/// are not secret and are safe to share publicly.
|
||||
/// <https://docs.shorebird.dev/concepts>
|
||||
pub app_id: String,
|
||||
/// The Shorebird channel built into the shorebird.yaml in the app.
|
||||
/// This is not currently used, but intended for future use to allow
|
||||
|
||||
+111
-3
@@ -289,7 +289,7 @@ pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result<bo
|
||||
let (client_id, current_patch_number) = with_state(|state| {
|
||||
Ok((
|
||||
state.client_id().to_string(),
|
||||
state.currently_booting_patch().map(|p| p.number),
|
||||
state.running_patch().map(|p| p.number),
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -338,7 +338,8 @@ fn check_hash(path: &Path, expected_string: &str) -> anyhow::Result<()> {
|
||||
// This is a common error for developers. We could avoid it entirely
|
||||
// by sending the hash of `libapp.so` to the server and having the
|
||||
// server only send updates when the hash matches.
|
||||
// https://github.com/shorebirdtech/updater/issues/56
|
||||
// A hash mismatch usually means the same version number was reused with a
|
||||
// different app binary.
|
||||
if !hash_matches {
|
||||
bail!(
|
||||
"Update rejected: hash mismatch. Update was downloaded but \
|
||||
@@ -424,7 +425,7 @@ fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Resul
|
||||
Ok(PatchCheckRequest::new(
|
||||
&config,
|
||||
&state.client_id(),
|
||||
state.currently_booting_patch().map(|p| p.number),
|
||||
state.running_patch().map(|p| p.number),
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -4086,3 +4087,110 @@ mod multi_engine_tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression tests asserting the patch-check request's `current_patch_number`
|
||||
/// carries the running patch, not the boot breadcrumb (`currently_booting_patch`)
|
||||
/// which `report_launch_success` clears before any patch check fires. Covers
|
||||
/// both construction sites: `check_for_downloadable_update` and `update`.
|
||||
#[cfg(test)]
|
||||
mod patch_check_current_patch_number_tests {
|
||||
use anyhow::Result;
|
||||
use serial_test::serial;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{
|
||||
check_for_downloadable_update,
|
||||
network::{
|
||||
testing_set_network_hooks, PatchCheckResponse, UNEXPECTED_DOWNLOAD, UNEXPECTED_REPORT,
|
||||
},
|
||||
report_launch_start, report_launch_success,
|
||||
test_utils::install_fake_patch,
|
||||
update,
|
||||
updater::tests::init_for_testing,
|
||||
with_state,
|
||||
};
|
||||
|
||||
/// `current_patch_number` was `None` on the wire (the bug we're guarding).
|
||||
const FIELD_OMITTED: i64 = -1;
|
||||
/// The patch-check hook never ran.
|
||||
const HOOK_NOT_CALLED: i64 = i64::MIN;
|
||||
|
||||
/// Last `current_patch_number` seen by the patch-check hook. Shared by
|
||||
/// both tests; safe because they are `#[serial]` (only one runs at a
|
||||
/// time) and `arrange_capturing_hooks` resets it before each check.
|
||||
static CAPTURED: AtomicI64 = AtomicI64::new(HOOK_NOT_CALLED);
|
||||
|
||||
/// Installs patch 1 and completes a full boot, then asserts the steady
|
||||
/// state in which patch checks actually run: the boot breadcrumb is
|
||||
/// cleared, but the process is still running patch 1. Reading
|
||||
/// `currently_booting_patch` here yields `None` — that is precisely the
|
||||
/// regression this guards against.
|
||||
fn boot_patch_one(tmp_dir: &TempDir) -> Result<()> {
|
||||
init_for_testing(tmp_dir, None);
|
||||
install_fake_patch(1)?;
|
||||
report_launch_start()?;
|
||||
report_launch_success()?;
|
||||
with_state(|state| {
|
||||
// Steady state: the boot breadcrumb is cleared, yet patch 1 is
|
||||
// still the running patch.
|
||||
assert!(state.currently_booting_patch().is_none());
|
||||
assert_eq!(state.running_patch().map(|p| p.number), Some(1));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Installs network hooks whose patch-check leg records the request's
|
||||
/// `current_patch_number` into `CAPTURED` (`None` -> `FIELD_OMITTED`) and
|
||||
/// reports no available update.
|
||||
fn arrange_capturing_hooks() {
|
||||
CAPTURED.store(HOOK_NOT_CALLED, Ordering::SeqCst);
|
||||
testing_set_network_hooks(
|
||||
|_url, request| {
|
||||
CAPTURED.store(
|
||||
request
|
||||
.current_patch_number
|
||||
.map(|n| n as i64)
|
||||
.unwrap_or(FIELD_OMITTED),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
Ok(PatchCheckResponse {
|
||||
patch_available: false,
|
||||
patch: None,
|
||||
rolled_back_patch_numbers: None,
|
||||
})
|
||||
},
|
||||
// No update is offered, so neither hook should ever fire.
|
||||
UNEXPECTED_DOWNLOAD,
|
||||
UNEXPECTED_REPORT,
|
||||
);
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn check_for_downloadable_update_sends_running_patch() -> Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
boot_patch_one(&tmp_dir)?;
|
||||
arrange_capturing_hooks();
|
||||
|
||||
check_for_downloadable_update(None)?;
|
||||
|
||||
// 1 = running patch sent; -1 = field omitted (the bug); i64::MIN = hook never ran.
|
||||
assert_eq!(CAPTURED.load(Ordering::SeqCst), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn update_sends_running_patch() -> Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
boot_patch_one(&tmp_dir)?;
|
||||
arrange_capturing_hooks();
|
||||
|
||||
update(None)?;
|
||||
|
||||
// 1 = running patch sent; -1 = field omitted (the bug); i64::MIN = hook never ran.
|
||||
assert_eq!(CAPTURED.load(Ordering::SeqCst), 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,4 @@ For an Updater function to be visible to the Dart code, it must:
|
||||
`dart run ffigen`. ffigen reads only `updater_dart.h`, so engine-only
|
||||
symbols are not bound.
|
||||
1. Android specific: be listed in
|
||||
https://github.com/shorebirdtech/flutter/blob/shorebird/dev/engine/src/flutter/shell/platform/android/android_exports.lst
|
||||
https://github.com/tony-cloud/flutter/blob/tonycloud/dev/engine/src/flutter/shell/platform/android/android_exports.lst
|
||||
|
||||
Reference in New Issue
Block a user