diff --git a/engine/src/flutter/runtime/shorebird/BUILD.gn b/engine/src/flutter/runtime/shorebird/BUILD.gn index 81bf3f588b3f8..1f856e36d3f48 100644 --- a/engine/src/flutter/runtime/shorebird/BUILD.gn +++ b/engine/src/flutter/runtime/shorebird/BUILD.gn @@ -11,10 +11,6 @@ source_set("patch_cache") { deps = [ "//flutter/fml", "//flutter/runtime:libdart", + "//flutter/shell/common/shorebird:updater", ] - - # For shorebird_report_launch_start() in updater.h - # The include path "third_party/updater/library/include/updater.h" is relative - # to //flutter/, which is already in the default include path. - include_dirs = [ "//flutter" ] } diff --git a/engine/src/flutter/runtime/shorebird/patch_cache.cc b/engine/src/flutter/runtime/shorebird/patch_cache.cc index 5610eb3393d60..b835d01b98639 100644 --- a/engine/src/flutter/runtime/shorebird/patch_cache.cc +++ b/engine/src/flutter/runtime/shorebird/patch_cache.cc @@ -9,7 +9,8 @@ #include "flutter/fml/logging.h" #include "flutter/fml/mapping.h" #include "flutter/runtime/shorebird/patch_mapping.h" -#include "third_party/updater/library/include/updater.h" +#include "flutter/shell/common/shorebird/updater.h" +#include "third_party/dart/runtime/include/dart_api.h" namespace flutter { @@ -161,7 +162,7 @@ std::shared_ptr TryLoadFromPatch( if (symbol == kIsolateDataSymbol) { std::call_once(launch_start_flag, []() { FML_LOG(INFO) << "Reporting launch start for patch"; - shorebird_report_launch_start(); + shorebird::Updater::Instance().ReportLaunchStart(); }); return PatchMapping::CreateIsolateData(cache_entry); } else { diff --git a/engine/src/flutter/shell/common/BUILD.gn b/engine/src/flutter/shell/common/BUILD.gn index 45d48d8ac8873..11af400ec6290 100644 --- a/engine/src/flutter/shell/common/BUILD.gn +++ b/engine/src/flutter/shell/common/BUILD.gn @@ -148,13 +148,12 @@ source_set("common") { "//flutter/lib/ui", "//flutter/runtime", "//flutter/shell/common:base64", + "//flutter/shell/common/shorebird:updater", "//flutter/shell/geometry", "//flutter/shell/profiling", "//flutter/skia", ] - include_dirs = [ "//flutter/updater" ] - if (impeller_supports_rendering) { sources += [ "snapshot_controller_impeller.cc", @@ -163,31 +162,6 @@ source_set("common") { deps += [ "//flutter/impeller" ] } - - # Needed to compile flutter_tester for macOS. - if (host_os == "mac" && target_os == "mac") { - if (target_cpu == "arm64") { - libs = [ "//flutter/third_party/updater/target/aarch64-apple-darwin/release/libupdater.a" ] - } else if (target_cpu == "x64") { - libs = [ "//flutter/third_party/updater/target/x86_64-apple-darwin/release/libupdater.a" ] - } - } - - # Needed to compile flutter_tester for Windows. - if (host_os == "win" && target_os == "win") { - if (target_cpu == "x64") { - libs = [ - "userenv.lib", - "//flutter/third_party/updater/target/x86_64-pc-windows-msvc/release/updater.lib", - ] - } - } - - if (host_os == "linux" && target_os == "linux") { - if (target_cpu == "x64") { - libs = [ "//flutter/third_party/updater/target/x86_64-unknown-linux-gnu/release/libupdater.a" ] - } - } } # These are in their own source_set to avoid a dependency cycle with //common/graphics @@ -364,6 +338,7 @@ if (enable_unittests) { "//flutter/common/graphics", "//flutter/display_list/testing:display_list_testing", "//flutter/shell/common:base64", + "//flutter/shell/common/shorebird:updater", "//flutter/shell/profiling:profiling_unittests", "//flutter/shell/version", "//flutter/testing:fixture_test", diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index 662a0f8393491..cb523c5985d44 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -46,7 +46,7 @@ #include "third_party/skia/include/core/SkGraphics.h" #include "third_party/tonic/common/log.h" -#include "third_party/updater/library/include/updater.h" +#include "flutter/shell/common/shorebird/updater.h" namespace flutter { @@ -523,14 +523,13 @@ Shell::Shell(DartVMRef vm, is_gpu_disabled_sync_switch_(new fml::SyncSwitch(is_gpu_disabled)), weak_factory_gpu_(nullptr), weak_factory_(this) { - // FIXME: This is probably the wrong place to hook into. -#if SHOREBIRD_PLATFORM_SUPPORTED + // Report launch status to Shorebird updater for crash recovery tracking. + // On unsupported platforms, NoOpUpdater handles these calls gracefully. if (!vm_) { - shorebird_report_launch_failure(); + shorebird::Updater::Instance().ReportLaunchFailure(); } else { - shorebird_report_launch_success(); + shorebird::Updater::Instance().ReportLaunchSuccess(); } -#endif FML_CHECK(!settings.enable_software_rendering || !settings.enable_impeller) << "Software rendering is incompatible with Impeller."; if (!settings.enable_impeller && settings.warn_on_impeller_opt_out) { diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index bb57adc49e470..6bb7907b3936e 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -52,6 +52,8 @@ #include "third_party/skia/include/codec/SkCodecAnimation.h" #include "third_party/tonic/converter/dart_converter.h" +#include "flutter/shell/common/shorebird/updater.h" + #ifdef SHELL_ENABLE_VULKAN #include "flutter/vulkan/vulkan_application.h" // nogncheck #endif @@ -5107,6 +5109,66 @@ TEST_F(ShellTest, ShoulDiscardLayerTreeIfFrameIsSizedIncorrectly) { DestroyShell(std::move(shell), task_runners); } +// Test that Shell creation triggers the Shorebird Updater's ReportLaunchSuccess +// call. This is important for the crash recovery mechanism to work correctly. +TEST_F(ShellTest, ShorebirdUpdaterReportLaunchSuccessOnShellCreation) { + // Install a mock updater before creating the shell + auto mock = std::make_unique(); + auto* mock_ptr = mock.get(); + shorebird::Updater::SetInstanceForTesting(std::move(mock)); + + EXPECT_EQ(mock_ptr->launch_success_count(), 0); + EXPECT_EQ(mock_ptr->launch_failure_count(), 0); + + auto settings = CreateSettingsForFixture(); + auto task_runners = GetTaskRunnersForFixture(); + auto shell = CreateShell(settings, task_runners); + ASSERT_TRUE(shell); + + // Shell constructor should have called ReportLaunchSuccess + EXPECT_EQ(mock_ptr->launch_success_count(), 1); + EXPECT_EQ(mock_ptr->launch_failure_count(), 0); + + // Verify the call was logged + const auto& log = mock_ptr->call_log(); + EXPECT_TRUE(std::find(log.begin(), log.end(), "ReportLaunchSuccess") != + log.end()); + + DestroyShell(std::move(shell), task_runners); + + // Clean up - reset the updater instance + shorebird::Updater::ResetInstanceForTesting(); +} + +// Test that creating multiple shells only calls ReportLaunchSuccess for each +// shell. This verifies that each Shell reports its own launch status. +TEST_F(ShellTest, ShorebirdUpdaterReportLaunchSuccessForMultipleShells) { + auto mock = std::make_unique(); + auto* mock_ptr = mock.get(); + shorebird::Updater::SetInstanceForTesting(std::move(mock)); + + EXPECT_EQ(mock_ptr->launch_success_count(), 0); + + auto settings = CreateSettingsForFixture(); + + // Create first shell + auto task_runners1 = GetTaskRunnersForFixture(); + auto shell1 = CreateShell(settings, task_runners1); + ASSERT_TRUE(shell1); + EXPECT_EQ(mock_ptr->launch_success_count(), 1); + + // Create second shell + auto task_runners2 = GetTaskRunnersForFixture(); + auto shell2 = CreateShell(settings, task_runners2); + ASSERT_TRUE(shell2); + EXPECT_EQ(mock_ptr->launch_success_count(), 2); + + DestroyShell(std::move(shell1), task_runners1); + DestroyShell(std::move(shell2), task_runners2); + + shorebird::Updater::ResetInstanceForTesting(); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/BUILD.gn b/engine/src/flutter/shell/common/shorebird/BUILD.gn index dc2fc5515bd7c..2b1e29c03512d 100644 --- a/engine/src/flutter/shell/common/shorebird/BUILD.gn +++ b/engine/src/flutter/shell/common/shorebird/BUILD.gn @@ -15,6 +15,56 @@ source_set("snapshots_data_handle") { ] } +# C++ wrapper around the Rust updater C API. +# This provides a testable abstraction layer that can be mocked for testing. +source_set("updater") { + sources = [ + "updater.cc", + "updater.h", + ] + + deps = [ "//flutter/fml" ] + + # For the Rust updater C API (shorebird_report_launch_start, etc.) + include_dirs = [ "//flutter" ] + + # Link the Rust updater static library based on target platform. + if (is_android) { + if (target_cpu == "arm") { + libs = [ "//flutter/third_party/updater/target/armv7-linux-androideabi/release/libupdater.a" ] + } else if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-linux-android/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-linux-android/release/libupdater.a" ] + } else if (target_cpu == "x86") { + libs = [ "//flutter/third_party/updater/target/i686-linux-android/release/libupdater.a" ] + } + } else if (is_ios) { + if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-apple-ios/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-apple-ios/release/libupdater.a" ] + } + } else if (is_mac) { + if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-apple-darwin/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-apple-darwin/release/libupdater.a" ] + } + } else if (is_win) { + if (target_cpu == "x64") { + libs = [ + "userenv.lib", + "//flutter/third_party/updater/target/x86_64-pc-windows-msvc/release/updater.lib", + ] + } + } else if (is_linux) { + if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-unknown-linux-gnu/release/libupdater.a" ] + } + } +} + source_set("shorebird") { sources = [ "shorebird.cc", @@ -23,14 +73,13 @@ source_set("shorebird") { deps = [ ":snapshots_data_handle", + ":updater", "//flutter/fml", "//flutter/runtime", "//flutter/runtime:libdart", "//flutter/shell/common", "//flutter/shell/platform/embedder:embedder_headers", ] - - include_dirs = [ "//flutter/updater" ] } if (enable_unittests) { @@ -45,14 +94,14 @@ if (enable_unittests) { "patch_cache_unittests.cc", "shorebird_unittests.cc", "snapshots_data_handle_unittests.cc", + "updater_unittests.cc", ] - # This only includes snapshots_data_handle and not shorebird because - # shorebird fails to link due to a missing updater lib. deps = [ ":shorebird", ":shorebird_fixtures", ":snapshots_data_handle", + ":updater", "//flutter/runtime", "//flutter/runtime/shorebird:patch_cache", "//flutter/testing", diff --git a/engine/src/flutter/shell/common/shorebird/shorebird.cc b/engine/src/flutter/shell/common/shorebird/shorebird.cc index d040e07e0285d..71d336e90f8b0 100644 --- a/engine/src/flutter/shell/common/shorebird/shorebird.cc +++ b/engine/src/flutter/shell/common/shorebird/shorebird.cc @@ -19,13 +19,12 @@ #include "flutter/runtime/dart_vm.h" #include "flutter/shell/common/shell.h" #include "flutter/shell/common/shorebird/snapshots_data_handle.h" +#include "flutter/shell/common/shorebird/updater.h" #include "flutter/shell/common/switches.h" #include "fml/logging.h" #include "shell/platform/embedder/embedder.h" #include "third_party/dart/runtime/include/dart_tools_api.h" -#include "third_party/updater/library/include/updater.h" - // Namespaced to avoid Google style warnings. namespace flutter { @@ -80,7 +79,7 @@ class FileCallbacksImpl { static void Close(void* file); }; -FileCallbacks ShorebirdFileCallbacks() { +shorebird::FileCallbacks ShorebirdFileCallbacks() { return { .open = FileCallbacksImpl::Open, .read = FileCallbacksImpl::Read, @@ -137,33 +136,22 @@ bool ConfigureShorebird(const ShorebirdConfigArgs& args, {shorebird_updater_dir_name}, fml::FilePermission::kReadWrite); - bool init_result; - // Using a block to make AppParameters lifetime explicit. - { - AppParameters app_parameters; - // Combine version and version_code into a single string. - // We could also pass these separately through to the updater if needed. - auto release_version = args.release_version.version; - if (!args.release_version.build_number.empty()) { - release_version += "+" + args.release_version.build_number; - } - - app_parameters.release_version = release_version.c_str(); - app_parameters.code_cache_dir = code_cache_dir.c_str(); - app_parameters.app_storage_dir = app_storage_dir.c_str(); + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + auto release_version = args.release_version.version; + if (!args.release_version.build_number.empty()) { + release_version += "+" + args.release_version.build_number; + } - // https://stackoverflow.com/questions/26032039/convert-vectorstring-into-char-c - std::vector c_paths{}; - c_paths.push_back(args.release_app_library_path.c_str()); - // Do not modify application_library_paths or c_strings will invalidate. + shorebird::AppConfig config; + config.release_version = release_version; + config.original_libapp_paths = {args.release_app_library_path}; + config.app_storage_dir = app_storage_dir; + config.code_cache_dir = code_cache_dir; + config.file_callbacks = ShorebirdFileCallbacks(); + config.yaml_config = args.shorebird_yaml; - app_parameters.original_libapp_paths = c_paths.data(); - app_parameters.original_libapp_paths_size = c_paths.size(); - - // shorebird_init copies from app_parameters and shorebirdYaml. - init_result = shorebird_init(&app_parameters, ShorebirdFileCallbacks(), - args.shorebird_yaml.c_str()); - } + bool init_result = shorebird::Updater::Instance().Init(config); // We do not support synchronous updates on launch, it's a terrible UX. // Users can implement custom check-for-updates using @@ -171,11 +159,10 @@ bool ConfigureShorebird(const ShorebirdConfigArgs& args, // https://github.com/shorebirdtech/shorebird/issues/950 FML_LOG(INFO) << "Checking for active patch"; - shorebird_validate_next_boot_patch(); - char* c_active_path = shorebird_next_boot_patch_path(); - if (c_active_path != NULL) { - patch_path = c_active_path; - shorebird_free_string(c_active_path); + shorebird::Updater::Instance().ValidateNextBootPatch(); + std::string active_path = shorebird::Updater::Instance().NextBootPatchPath(); + if (!active_path.empty()) { + patch_path = active_path; FML_LOG(INFO) << "Shorebird updater: patch path: " << patch_path; } else { FML_LOG(INFO) << "Shorebird updater: no active patch."; @@ -189,9 +176,9 @@ bool ConfigureShorebird(const ShorebirdConfigArgs& args, return false; } - if (shorebird_should_auto_update()) { + if (shorebird::Updater::Instance().ShouldAutoUpdate()) { FML_LOG(INFO) << "Starting Shorebird update"; - shorebird_start_update_thread(); + shorebird::Updater::Instance().StartUpdateThread(); } else { FML_LOG(INFO) << "Shorebird auto_update disabled, not checking for updates."; @@ -226,31 +213,17 @@ void ConfigureShorebird(std::string code_cache_path, {shorebird_updater_dir_name}, fml::FilePermission::kReadWrite); - bool init_result; - // Using a block to make AppParameters lifetime explicit. - { - AppParameters app_parameters; - // Combine version and version_code into a single string. - // We could also pass these separately through to the updater if needed. - auto release_version = version + "+" + version_code; - app_parameters.release_version = release_version.c_str(); - app_parameters.code_cache_dir = code_cache_dir.c_str(); - app_parameters.app_storage_dir = app_storage_dir.c_str(); - - // https://stackoverflow.com/questions/26032039/convert-vectorstring-into-char-c - std::vector c_paths{}; - for (const auto& string : settings.application_library_paths) { - c_paths.push_back(string.c_str()); - } - // Do not modify application_library_paths or c_strings will invalidate. - - app_parameters.original_libapp_paths = c_paths.data(); - app_parameters.original_libapp_paths_size = c_paths.size(); + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + shorebird::AppConfig config; + config.release_version = version + "+" + version_code; + config.original_libapp_paths = settings.application_library_paths; + config.app_storage_dir = app_storage_dir; + config.code_cache_dir = code_cache_dir; + config.file_callbacks = ShorebirdFileCallbacks(); + config.yaml_config = shorebird_yaml; - // shorebird_init copies from app_parameters and shorebirdYaml. - init_result = shorebird_init(&app_parameters, ShorebirdFileCallbacks(), - shorebird_yaml.c_str()); - } + bool init_result = shorebird::Updater::Instance().Init(config); // We do not support synchronous updates on launch, it's a terrible UX. // Users can implement custom check-for-updates using @@ -262,11 +235,9 @@ void ConfigureShorebird(std::string code_cache_path, SetBaseSnapshot(settings); #endif - shorebird_validate_next_boot_patch(); - char* c_active_path = shorebird_next_boot_patch_path(); - if (c_active_path != NULL) { - std::string active_path = c_active_path; - shorebird_free_string(c_active_path); + shorebird::Updater::Instance().ValidateNextBootPatch(); + std::string active_path = shorebird::Updater::Instance().NextBootPatchPath(); + if (!active_path.empty()) { FML_LOG(INFO) << "Shorebird updater: active path: " << active_path; #if SHOREBIRD_USE_INTERPRETER @@ -292,9 +263,9 @@ void ConfigureShorebird(std::string code_cache_path, return; } - if (shorebird_should_auto_update()) { + if (shorebird::Updater::Instance().ShouldAutoUpdate()) { FML_LOG(INFO) << "Starting Shorebird update"; - shorebird_start_update_thread(); + shorebird::Updater::Instance().StartUpdateThread(); } else { FML_LOG(INFO) << "Shorebird auto_update disabled, not checking for updates."; diff --git a/engine/src/flutter/shell/common/shorebird/updater.cc b/engine/src/flutter/shell/common/shorebird/updater.cc new file mode 100644 index 0000000000000..84de941ec2d40 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater.cc @@ -0,0 +1,166 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/updater.h" + +#include "flutter/fml/logging.h" + +#if SHOREBIRD_PLATFORM_SUPPORTED +#include "third_party/updater/library/include/updater.h" +#endif + +namespace flutter { +namespace shorebird { + +// Static member definitions +std::unique_ptr Updater::instance_; +std::mutex Updater::instance_mutex_; + +Updater& Updater::Instance() { + std::lock_guard lock(instance_mutex_); + if (!instance_) { +#if SHOREBIRD_PLATFORM_SUPPORTED + instance_ = std::make_unique(); +#else + instance_ = std::make_unique(); +#endif + } + return *instance_; +} + +void Updater::SetInstanceForTesting(std::unique_ptr instance) { + std::lock_guard lock(instance_mutex_); + instance_ = std::move(instance); +} + +void Updater::ResetInstanceForTesting() { + std::lock_guard lock(instance_mutex_); + instance_.reset(); +} + +#if SHOREBIRD_PLATFORM_SUPPORTED +// RealUpdater implementation - wraps the Rust C API + +bool RealUpdater::Init(const AppConfig& config) { + // Convert paths to C strings + std::vector c_paths; + c_paths.reserve(config.original_libapp_paths.size()); + for (const auto& path : config.original_libapp_paths) { + c_paths.push_back(path.c_str()); + } + + AppParameters params; + params.release_version = config.release_version.c_str(); + params.original_libapp_paths = c_paths.data(); + params.original_libapp_paths_size = static_cast(c_paths.size()); + params.app_storage_dir = config.app_storage_dir.c_str(); + params.code_cache_dir = config.code_cache_dir.c_str(); + + // Convert our FileCallbacks to the Rust struct + ::FileCallbacks rust_callbacks; + rust_callbacks.open = config.file_callbacks.open; + rust_callbacks.read = config.file_callbacks.read; + rust_callbacks.seek = config.file_callbacks.seek; + rust_callbacks.close = config.file_callbacks.close; + + return shorebird_init(¶ms, rust_callbacks, config.yaml_config.c_str()); +} + +void RealUpdater::ValidateNextBootPatch() { + shorebird_validate_next_boot_patch(); +} + +std::string RealUpdater::NextBootPatchPath() { + char* c_path = shorebird_next_boot_patch_path(); + if (c_path == nullptr) { + return ""; + } + std::string path(c_path); + shorebird_free_string(c_path); + return path; +} + +void RealUpdater::ReportLaunchStart() { + shorebird_report_launch_start(); +} + +void RealUpdater::ReportLaunchSuccess() { + shorebird_report_launch_success(); +} + +void RealUpdater::ReportLaunchFailure() { + shorebird_report_launch_failure(); +} + +bool RealUpdater::ShouldAutoUpdate() { + return shorebird_should_auto_update(); +} + +void RealUpdater::StartUpdateThread() { + shorebird_start_update_thread(); +} +#endif // SHOREBIRD_PLATFORM_SUPPORTED + +// MockUpdater implementation - for testing + +bool MockUpdater::Init(const AppConfig& config) { + init_count_++; + last_release_version_ = config.release_version; + last_yaml_config_ = config.yaml_config; + call_log_.push_back("Init"); + return init_result_; +} + +void MockUpdater::ValidateNextBootPatch() { + validate_count_++; + call_log_.push_back("ValidateNextBootPatch"); +} + +std::string MockUpdater::NextBootPatchPath() { + call_log_.push_back("NextBootPatchPath"); + return next_boot_patch_path_; +} + +void MockUpdater::ReportLaunchStart() { + launch_start_count_++; + call_log_.push_back("ReportLaunchStart"); +} + +void MockUpdater::ReportLaunchSuccess() { + launch_success_count_++; + call_log_.push_back("ReportLaunchSuccess"); +} + +void MockUpdater::ReportLaunchFailure() { + launch_failure_count_++; + call_log_.push_back("ReportLaunchFailure"); +} + +bool MockUpdater::ShouldAutoUpdate() { + call_log_.push_back("ShouldAutoUpdate"); + return should_auto_update_; +} + +void MockUpdater::StartUpdateThread() { + start_update_thread_count_++; + call_log_.push_back("StartUpdateThread"); +} + +void MockUpdater::Reset() { + init_count_ = 0; + validate_count_ = 0; + launch_start_count_ = 0; + launch_success_count_ = 0; + launch_failure_count_ = 0; + start_update_thread_count_ = 0; + init_result_ = true; + should_auto_update_ = false; + next_boot_patch_path_.clear(); + last_release_version_.clear(); + last_yaml_config_.clear(); + call_log_.clear(); +} + +} // namespace shorebird +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/updater.h b/engine/src/flutter/shell/common/shorebird/updater.h new file mode 100644 index 0000000000000..f6fa5a9a2f6d9 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater.h @@ -0,0 +1,191 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ + +#include +#include +#include +#include +#include +#include + +namespace flutter { +namespace shorebird { + +/// File callbacks for iOS patch loading. +/// Mirrors the FileCallbacks struct from the Rust updater. +struct FileCallbacks { + void* (*open)(void); + uintptr_t (*read)(void* file_handle, uint8_t* buffer, uintptr_t count); + int64_t (*seek)(void* file_handle, int64_t offset, int32_t whence); + void (*close)(void* file_handle); +}; + +/// Configuration for initializing the Shorebird updater. +struct AppConfig { + /// Version string for this release (e.g., "1.0.0+1"). + std::string release_version; + + /// Paths to the original AOT libraries (libapp.so on Android, App.framework + /// on iOS). + std::vector original_libapp_paths; + + /// Directory for persistent updater state (survives app updates). + std::string app_storage_dir; + + /// Directory for cached artifacts (cleared on app updates). + std::string code_cache_dir; + + /// Callbacks for iOS patch file access (can be null callbacks on Android). + FileCallbacks file_callbacks; + + /// YAML configuration from shorebird.yaml. + std::string yaml_config; +}; + +/// Abstract interface for the Shorebird updater. +/// +/// This abstraction allows for: +/// 1. Mocking in tests without requiring the real Rust library +/// 2. Future migration from Rust to C++ implementation +/// 3. Test instrumentation (call counting, logging) +class Updater { + public: + virtual ~Updater() = default; + + /// Initialize the updater with configuration. + /// @param config Configuration containing release version, paths, and + /// callbacks + /// @return true if initialization succeeded + virtual bool Init(const AppConfig& config) = 0; + + /// Validate the next boot patch. If invalid, falls back to last good state. + virtual void ValidateNextBootPatch() = 0; + + /// Get the path to the patch that will boot on next run. + /// @return Path to patch, or empty string if no patch available + virtual std::string NextBootPatchPath() = 0; + + // Boot lifecycle methods + virtual void ReportLaunchStart() = 0; + virtual void ReportLaunchSuccess() = 0; + virtual void ReportLaunchFailure() = 0; + + // Update checking + virtual bool ShouldAutoUpdate() = 0; + virtual void StartUpdateThread() = 0; + + // Singleton access + static Updater& Instance(); + + // Test support - allows injecting a mock implementation + static void SetInstanceForTesting(std::unique_ptr instance); + static void ResetInstanceForTesting(); + + protected: + Updater() = default; + + private: + static std::unique_ptr instance_; + static std::mutex instance_mutex_; +}; + +/// No-op implementation for unsupported platforms. +/// All methods are safe to call but do nothing. +class NoOpUpdater : public Updater { + public: + NoOpUpdater() = default; + ~NoOpUpdater() override = default; + + bool Init(const AppConfig& config) override { return true; } + void ValidateNextBootPatch() override {} + std::string NextBootPatchPath() override { return ""; } + void ReportLaunchStart() override {} + void ReportLaunchSuccess() override {} + void ReportLaunchFailure() override {} + bool ShouldAutoUpdate() override { return false; } + void StartUpdateThread() override {} +}; + +#if SHOREBIRD_PLATFORM_SUPPORTED +/// Production implementation that wraps the Rust updater C API. +/// Only available on supported platforms (Android, iOS, macOS, Windows, Linux). +class RealUpdater : public Updater { + public: + RealUpdater() = default; + ~RealUpdater() override = default; + + bool Init(const AppConfig& config) override; + void ValidateNextBootPatch() override; + std::string NextBootPatchPath() override; + void ReportLaunchStart() override; + void ReportLaunchSuccess() override; + void ReportLaunchFailure() override; + bool ShouldAutoUpdate() override; + void StartUpdateThread() override; +}; +#endif // SHOREBIRD_PLATFORM_SUPPORTED + +/// Mock implementation for testing. +/// Tracks call counts and can be queried to verify behavior. +class MockUpdater : public Updater { + public: + MockUpdater() = default; + ~MockUpdater() override = default; + + bool Init(const AppConfig& config) override; + void ValidateNextBootPatch() override; + std::string NextBootPatchPath() override; + void ReportLaunchStart() override; + void ReportLaunchSuccess() override; + void ReportLaunchFailure() override; + bool ShouldAutoUpdate() override; + void StartUpdateThread() override; + + // Test accessors + int init_count() const { return init_count_; } + int validate_count() const { return validate_count_; } + int launch_start_count() const { return launch_start_count_; } + int launch_success_count() const { return launch_success_count_; } + int launch_failure_count() const { return launch_failure_count_; } + int start_update_thread_count() const { return start_update_thread_count_; } + const std::vector& call_log() const { return call_log_; } + + // Last init parameters (for verification) + const std::string& last_release_version() const { + return last_release_version_; + } + const std::string& last_yaml_config() const { return last_yaml_config_; } + + // Test configuration + void set_init_result(bool value) { init_result_ = value; } + void set_should_auto_update(bool value) { should_auto_update_ = value; } + void set_next_boot_patch_path(const std::string& path) { + next_boot_patch_path_ = path; + } + + // Reset all counters and logs + void Reset(); + + private: + int init_count_ = 0; + int validate_count_ = 0; + int launch_start_count_ = 0; + int launch_success_count_ = 0; + int launch_failure_count_ = 0; + int start_update_thread_count_ = 0; + bool init_result_ = true; + bool should_auto_update_ = false; + std::string next_boot_patch_path_; + std::string last_release_version_; + std::string last_yaml_config_; + std::vector call_log_; +}; + +} // namespace shorebird +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ diff --git a/engine/src/flutter/shell/common/shorebird/updater_unittests.cc b/engine/src/flutter/shell/common/shorebird/updater_unittests.cc new file mode 100644 index 0000000000000..97fb6ef2129f2 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater_unittests.cc @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/updater.h" + +#include + +#include "gtest/gtest.h" + +namespace flutter { +namespace shorebird { +namespace testing { + +class UpdaterTest : public ::testing::Test { + protected: + void SetUp() override { + // Install a mock for each test + auto mock = std::make_unique(); + mock_ = mock.get(); + Updater::SetInstanceForTesting(std::move(mock)); + } + + void TearDown() override { + mock_ = nullptr; + Updater::ResetInstanceForTesting(); + } + + MockUpdater* mock_ = nullptr; +}; + +TEST_F(UpdaterTest, MockUpdaterTracksLaunchStartCalls) { + EXPECT_EQ(mock_->launch_start_count(), 0); + + Updater::Instance().ReportLaunchStart(); + EXPECT_EQ(mock_->launch_start_count(), 1); + + Updater::Instance().ReportLaunchStart(); + EXPECT_EQ(mock_->launch_start_count(), 2); +} + +TEST_F(UpdaterTest, MockUpdaterTracksLaunchSuccessCalls) { + EXPECT_EQ(mock_->launch_success_count(), 0); + + Updater::Instance().ReportLaunchSuccess(); + EXPECT_EQ(mock_->launch_success_count(), 1); +} + +TEST_F(UpdaterTest, MockUpdaterTracksLaunchFailureCalls) { + EXPECT_EQ(mock_->launch_failure_count(), 0); + + Updater::Instance().ReportLaunchFailure(); + EXPECT_EQ(mock_->launch_failure_count(), 1); +} + +TEST_F(UpdaterTest, MockUpdaterTracksShouldAutoUpdate) { + mock_->set_should_auto_update(false); + EXPECT_FALSE(Updater::Instance().ShouldAutoUpdate()); + + mock_->set_should_auto_update(true); + EXPECT_TRUE(Updater::Instance().ShouldAutoUpdate()); +} + +TEST_F(UpdaterTest, MockUpdaterTracksStartUpdateThreadCalls) { + EXPECT_EQ(mock_->start_update_thread_count(), 0); + + Updater::Instance().StartUpdateThread(); + EXPECT_EQ(mock_->start_update_thread_count(), 1); +} + +TEST_F(UpdaterTest, MockUpdaterCallLogRecordsSequence) { + EXPECT_TRUE(mock_->call_log().empty()); + + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ShouldAutoUpdate(); + Updater::Instance().ReportLaunchSuccess(); + + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 3u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ShouldAutoUpdate"); + EXPECT_EQ(log[2], "ReportLaunchSuccess"); +} + +TEST_F(UpdaterTest, MockUpdaterResetClearsState) { + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + mock_->set_should_auto_update(true); + + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_success_count(), 1); + EXPECT_TRUE(mock_->ShouldAutoUpdate()); + + mock_->Reset(); + + EXPECT_EQ(mock_->launch_start_count(), 0); + EXPECT_EQ(mock_->launch_success_count(), 0); + // Check call_log before ShouldAutoUpdate() since the method adds to call_log + EXPECT_TRUE(mock_->call_log().empty()); + EXPECT_FALSE(mock_->ShouldAutoUpdate()); +} + +// Test that demonstrates the std::once_flag pattern works correctly. +// This is the same pattern used in TryLoadFromPatch. +TEST_F(UpdaterTest, OncePerProcessPatternOnlyCallsOnce) { + static std::once_flag test_flag; + int call_count = 0; + + auto simulate_patch_load = [&]() { + std::call_once(test_flag, [&]() { + call_count++; + Updater::Instance().ReportLaunchStart(); + }); + }; + + // Simulate multiple engines loading patches + simulate_patch_load(); // Engine 1 + simulate_patch_load(); // Engine 2 + simulate_patch_load(); // Engine 3 + + EXPECT_EQ(call_count, 1); + EXPECT_EQ(mock_->launch_start_count(), 1); +} + +} // namespace testing +} // namespace shorebird +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 7beb4ee4d8fb6..066e16ae09567 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -185,8 +185,6 @@ source_set("flutter_shell_native_src") { public_configs = [ "//flutter:config" ] - include_dirs = [ "//flutter/updater" ] - defines = [] libs = [ @@ -194,17 +192,6 @@ source_set("flutter_shell_native_src") { "EGL", "GLESv2", ] - if (target_cpu == "arm") { - libs += [ "//flutter/third_party/updater/target/armv7-linux-androideabi/release/libupdater.a" ] - } else if (target_cpu == "arm64") { - libs += [ "//flutter/third_party/updater/target/aarch64-linux-android/release/libupdater.a" ] - } else if (target_cpu == "x64") { - libs += [ "//flutter/third_party/updater/target/x86_64-linux-android/release/libupdater.a" ] - } else if (target_cpu == "x86") { - libs += [ "//flutter/third_party/updater/target/i686-linux-android/release/libupdater.a" ] - } else { - assert(false, "Unsupported target_cpu") - } } action("gen_android_build_config_java") { diff --git a/engine/src/flutter/shell/platform/android/flutter_main.cc b/engine/src/flutter/shell/platform/android/flutter_main.cc index d881ca39efd3f..45e369f5b3a9d 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.cc +++ b/engine/src/flutter/shell/platform/android/flutter_main.cc @@ -30,8 +30,6 @@ #include "impeller/toolkit/android/proc_table.h" #include "txt/platform.h" -#include "third_party/updater/library/include/updater.h" - namespace flutter { constexpr int kMinimumAndroidApiLevelForImpeller = 29; diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index fd167854107b1..777b6ae4a64bb 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -75,14 +75,6 @@ source_set("flutter_framework_source") { "//build/config/ios:ios_application_extension", ] - if (target_cpu == "arm64") { - libs = [ "//flutter/third_party/updater/target/aarch64-apple-ios/release/libupdater.a" ] - } else if (target_cpu == "x64") { - libs = [ "//flutter/third_party/updater/target/x86_64-apple-ios/release/libupdater.a" ] - } else { - assert(false, "Unsupported target_cpu") - } - sources = [ "framework/Source/FlutterAppDelegate.mm", "framework/Source/FlutterAppDelegate_Internal.h",