Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions engine/src/flutter/shell/common/shorebird/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ if (shorebird_updater_supported) {
# This provides a testable abstraction layer that can be mocked for testing.
source_set("updater") {
sources = [
"protected_data.cc",
"protected_data.h",
"updater.cc",
"updater.h",
]
Expand Down
28 changes: 28 additions & 0 deletions engine/src/flutter/shell/common/shorebird/protected_data.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "flutter/shell/common/shorebird/protected_data.h"

#include <utility>

namespace flutter {
namespace shorebird {

namespace {

// Default gate implementation. Invokes `start_fn` synchronously on the
// caller's thread. Used on every platform that does not install its own
// gate: desktop, Android, embedded, and tests.
class ImmediateProtectedDataGate : public ProtectedDataGate {
public:
void StartWhenAvailable(std::function<void()> start_fn) override {
start_fn();
}
void CancelPending() override {}
};

} // namespace

std::unique_ptr<ProtectedDataGate> MakeImmediateProtectedDataGate() {
return std::make_unique<ImmediateProtectedDataGate>();
}

} // namespace shorebird
} // namespace flutter
70 changes: 70 additions & 0 deletions engine/src/flutter/shell/common/shorebird/protected_data.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_PROTECTED_DATA_H_
#define FLUTTER_SHELL_COMMON_SHOREBIRD_PROTECTED_DATA_H_

#include <functional>
#include <memory>

namespace flutter {
namespace shorebird {

/// Abstraction over "wait until the platform is ready to let the Shorebird
/// updater write its state files, then start the update."
///
/// On iOS, files under `Library/Application Support/` inherit the default
/// `NSFileProtectionCompleteUntilFirstUserAuthentication` class. Before the
/// user has unlocked the device for the first time since boot, the OS
/// refuses writes under that directory with EPERM/EACCES and any update
/// the updater starts during that window will fail at the state-write
/// step. The iOS implementation of this interface defers the start until
/// `UIApplication.protectedDataAvailable` is true (either immediately, if
/// it already is, or via a one-shot observer registered with
/// `NSNotificationCenter` for
/// `UIApplicationProtectedDataDidBecomeAvailableNotification`).
///
/// Every other platform uses the default implementation, which starts
/// immediately. There is nothing to wait for on Android, desktop, or
/// embedded.
///
/// Ownership: an instance of `ProtectedDataGate` is owned by the
/// `Updater` singleton. Installing a new gate via
/// `Updater::SetProtectedDataGate` cancels any pending start on the
/// previous gate.
///
/// Thread safety: implementations must be safe to call from any thread.
/// `start_fn` may be invoked on a different thread than the one that
/// called `StartWhenAvailable` (the iOS impl dispatches to the main
/// queue).
///
/// `start_fn` warnings:
/// - Must be non-blocking — it is expected to hand real work off to a
/// background worker, as `Updater::StartUpdateThread()` does.
/// - Must not capture anything with shorter-than-process lifetime. The
/// gate may hold `start_fn` indefinitely (until the user unlocks the
/// device for the first time since boot). Capturing a short-lived
/// pointer or reference in the lambda is a dangling-reference bug
/// waiting to happen.
class ProtectedDataGate {
public:
virtual ~ProtectedDataGate() = default;

/// Arrange for `start_fn` to be invoked when the platform is ready.
/// Implementations may invoke it synchronously. If a previous
/// `start_fn` on this gate is still pending, the implementation must
/// cancel it before accepting the new one — there is only ever one
/// pending start per gate.
virtual void StartWhenAvailable(std::function<void()> start_fn) = 0;

/// Cancel any pending invocation from a prior `StartWhenAvailable`
/// call. No-op if none is pending. Safe to call during destruction.
virtual void CancelPending() = 0;
};

/// Creates the default `ProtectedDataGate` used on every platform that
/// does not install its own. Invokes `start_fn` synchronously;
/// `CancelPending` is a no-op.
std::unique_ptr<ProtectedDataGate> MakeImmediateProtectedDataGate();

} // namespace shorebird
} // namespace flutter

#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_PROTECTED_DATA_H_
20 changes: 16 additions & 4 deletions engine/src/flutter/shell/common/shorebird/shorebird.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,14 @@ bool ConfigureShorebird(const ShorebirdConfigArgs& args,
}

if (shorebird::Updater::Instance().ShouldAutoUpdate()) {
FML_LOG(INFO) << "Starting Shorebird update";
shorebird::Updater::Instance().StartUpdateThread();
// Route the kickoff through the Updater's ProtectedDataGate. On
// platforms with the default immediate gate this is equivalent to
// calling StartUpdateThread() directly. On iOS the installed gate
// waits for UIApplication.protectedDataAvailable so the updater
// does not try to write its state file under Library/Application
// Support/ and fail with EPERM/EACCES before first unlock after
// boot. See protected_data.h.
shorebird::Updater::Instance().StartUpdateThreadWhenReady();
} else {
FML_LOG(INFO)
<< "Shorebird auto_update disabled, not checking for updates.";
Expand Down Expand Up @@ -264,8 +270,14 @@ void ConfigureShorebird(std::string code_cache_path,
}

if (shorebird::Updater::Instance().ShouldAutoUpdate()) {
FML_LOG(INFO) << "Starting Shorebird update";
shorebird::Updater::Instance().StartUpdateThread();
// Route the kickoff through the Updater's ProtectedDataGate. On
// platforms with the default immediate gate this is equivalent to
// calling StartUpdateThread() directly. On iOS the installed gate
// waits for UIApplication.protectedDataAvailable so the updater
// does not try to write its state file under Library/Application
// Support/ and fail with EPERM/EACCES before first unlock after
// boot. See protected_data.h.
shorebird::Updater::Instance().StartUpdateThreadWhenReady();
} else {
FML_LOG(INFO)
<< "Shorebird auto_update disabled, not checking for updates.";
Expand Down
129 changes: 128 additions & 1 deletion engine/src/flutter/shell/common/shorebird/shorebird_unittests.cc
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
#include "flutter/shell/common/shorebird/shorebird.h"

#include <memory>

#include "flutter/shell/common/shorebird/protected_data.h"
#include "flutter/shell/common/shorebird/updater.h"
#include "gtest/gtest.h"

namespace flutter {
namespace testing {

TEST(Shorebird, GetValueFromYamlValueExists) {
std::string yaml = "appid: com.example.app\nversion: 1.0.0\n";
std::string key = "appid";
Expand All @@ -17,5 +22,127 @@ TEST(Shorebird, GetValueFromYamlValueDoesNotExist) {
std::string value = GetValueFromYaml(yaml, key);
EXPECT_EQ(value, "");
}

// ----- ProtectedDataGate / Updater integration tests -----

// A test gate that captures the pending start_fn instead of invoking
// it, and tracks CancelPending calls.
class CapturingProtectedDataGate : public shorebird::ProtectedDataGate {
public:
void StartWhenAvailable(std::function<void()> start_fn) override {
++start_when_available_calls;
// Cancel any previous pending start before accepting the new one.
pending = std::move(start_fn);
}
void CancelPending() override {
++cancel_pending_calls;
pending = nullptr;
}

void FirePending() {
ASSERT_TRUE(static_cast<bool>(pending));
auto to_invoke = std::move(pending);
to_invoke();
}

int start_when_available_calls = 0;
int cancel_pending_calls = 0;
std::function<void()> pending;
};

class ShorebirdUpdaterGateTest : public ::testing::Test {
protected:
void SetUp() override {
// Give each test a fresh MockUpdater as the singleton so that
// StartUpdateThread() calls from the gate do not hit the real
// Rust updater.
shorebird::Updater::SetInstanceForTesting(
std::make_unique<shorebird::MockUpdater>());
}
void TearDown() override { shorebird::Updater::ResetInstanceForTesting(); }
};

TEST_F(ShorebirdUpdaterGateTest, DefaultGateStartsImmediately) {
auto& updater = shorebird::Updater::Instance();
auto* mock = dynamic_cast<shorebird::MockUpdater*>(&updater);
ASSERT_NE(mock, nullptr);

// No gate has been explicitly installed; the default immediate gate
// should be used.
updater.StartUpdateThreadWhenReady();
EXPECT_EQ(mock->start_update_thread_count(), 1);
}

TEST_F(ShorebirdUpdaterGateTest, InstalledGateReceivesStartFn) {
auto& updater = shorebird::Updater::Instance();
auto* mock = dynamic_cast<shorebird::MockUpdater*>(&updater);
ASSERT_NE(mock, nullptr);

auto gate = std::make_unique<CapturingProtectedDataGate>();
auto* gate_ptr = gate.get();
updater.SetProtectedDataGate(std::move(gate));

updater.StartUpdateThreadWhenReady();

// The gate was asked but has not fired — the mock should not have
// been called yet.
EXPECT_EQ(gate_ptr->start_when_available_calls, 1);
EXPECT_EQ(mock->start_update_thread_count(), 0);

// Firing the gate's pending callback should invoke
// StartUpdateThread on the mock.
gate_ptr->FirePending();
EXPECT_EQ(mock->start_update_thread_count(), 1);
}

TEST_F(ShorebirdUpdaterGateTest, CancelPendingUpdateStartForwardsToGate) {
auto& updater = shorebird::Updater::Instance();
auto gate = std::make_unique<CapturingProtectedDataGate>();
auto* gate_ptr = gate.get();
updater.SetProtectedDataGate(std::move(gate));

updater.StartUpdateThreadWhenReady();
EXPECT_EQ(gate_ptr->cancel_pending_calls, 0);

updater.CancelPendingUpdateStart();
EXPECT_EQ(gate_ptr->cancel_pending_calls, 1);
EXPECT_FALSE(static_cast<bool>(gate_ptr->pending));
}

TEST_F(ShorebirdUpdaterGateTest, ReplacingGateCancelsPreviousPending) {
auto& updater = shorebird::Updater::Instance();

auto first = std::make_unique<CapturingProtectedDataGate>();
auto* first_ptr = first.get();
updater.SetProtectedDataGate(std::move(first));
updater.StartUpdateThreadWhenReady();

EXPECT_EQ(first_ptr->start_when_available_calls, 1);
EXPECT_EQ(first_ptr->cancel_pending_calls, 0);

// Replacing the gate should cancel pending on the one being
// replaced. The old gate pointer is about to be destroyed — observe
// the cancel call before replacement.
auto second = std::make_unique<CapturingProtectedDataGate>();
updater.SetProtectedDataGate(std::move(second));
EXPECT_EQ(first_ptr->cancel_pending_calls, 1);
}

TEST_F(ShorebirdUpdaterGateTest, SettingGateToNullptrRestoresImmediateGate) {
auto& updater = shorebird::Updater::Instance();
auto* mock = dynamic_cast<shorebird::MockUpdater*>(&updater);
ASSERT_NE(mock, nullptr);

updater.SetProtectedDataGate(
std::make_unique<CapturingProtectedDataGate>());
// Install nullptr to request restoration of the immediate gate.
updater.SetProtectedDataGate(nullptr);

updater.StartUpdateThreadWhenReady();
// The immediate gate fires synchronously, so the mock should have
// been started exactly once.
EXPECT_EQ(mock->start_update_thread_count(), 1);
}

} // namespace testing
} // namespace flutter
} // namespace flutter
26 changes: 26 additions & 0 deletions engine/src/flutter/shell/common/shorebird/updater.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ void Updater::ResetLaunchStateForTesting() {
launch_completed_.store(false);
}

void Updater::SetProtectedDataGate(std::unique_ptr<ProtectedDataGate> gate) {
// Cancel any pending start on the previous gate so we don't leak
// observers or fire into a torn-down gate.
if (protected_data_gate_) {
protected_data_gate_->CancelPending();
}
protected_data_gate_ =
gate ? std::move(gate) : MakeImmediateProtectedDataGate();
}

void Updater::StartUpdateThreadWhenReady() {
if (!protected_data_gate_) {
protected_data_gate_ = MakeImmediateProtectedDataGate();
}
protected_data_gate_->StartWhenAvailable([] {
FML_LOG(INFO) << "Starting Shorebird update";
Updater::Instance().StartUpdateThread();
});
}

void Updater::CancelPendingUpdateStart() {
if (protected_data_gate_) {
protected_data_gate_->CancelPending();
}
}

void Updater::ReportLaunchStart() {
// Guard: only the first engine in a process should promote next_boot →
// current_boot in the Rust updater. See class-level comment for rationale.
Expand Down
39 changes: 39 additions & 0 deletions engine/src/flutter/shell/common/shorebird/updater.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <string>
#include <vector>

#include "flutter/shell/common/shorebird/protected_data.h"

namespace flutter {
namespace shorebird {

Expand Down Expand Up @@ -104,6 +106,38 @@ class Updater {
virtual bool ShouldAutoUpdate() = 0;
virtual void StartUpdateThread() = 0;

/// Installs a `ProtectedDataGate` used by `StartUpdateThreadWhenReady`
/// to decide when the updater thread may actually run. Passing
/// `nullptr` restores the default immediate gate.
///
/// If a previous gate had a pending `StartWhenAvailable` callback,
/// its `CancelPending()` is called before the gate is replaced so
/// we do not leak observers.
///
/// Intended to be called once per process during engine
/// initialization by the platform embedder (e.g. iOS installs a gate
/// that waits for `UIApplication.protectedDataAvailable`). Must be
/// called before any concurrent access; the updater does not
/// synchronize gate installation against `StartUpdateThreadWhenReady`.
void SetProtectedDataGate(std::unique_ptr<ProtectedDataGate> gate);

/// Starts the updater thread as soon as the installed
/// `ProtectedDataGate` says it is safe to do so. On platforms with
/// the default immediate gate this is equivalent to calling
/// `StartUpdateThread()` directly. On iOS (or any platform that has
/// installed a deferring gate) the actual `StartUpdateThread()` call
/// is delayed until the gate fires.
///
/// Can be called while a previous start is still pending on the gate;
/// the gate will cancel the prior pending start before queuing a
/// new one.
void StartUpdateThreadWhenReady();

/// Cancels any pending `StartUpdateThreadWhenReady` that is still
/// waiting on the gate. No-op if nothing is pending. Does not affect
/// an updater thread that has already been started.
void CancelPendingUpdateStart();

// Singleton access
static Updater& Instance();

Expand All @@ -130,6 +164,11 @@ class Updater {
// Once-per-process guards for launch lifecycle.
static std::atomic<bool> launch_started_;
static std::atomic<bool> launch_completed_;

// Gate used by StartUpdateThreadWhenReady / CancelPendingUpdateStart.
// Initialized lazily to the immediate gate on first access; replaced
// via SetProtectedDataGate. Never null after first access.
std::unique_ptr<ProtectedDataGate> protected_data_gate_;
};

/// No-op implementation for unsupported platforms.
Expand Down
2 changes: 2 additions & 0 deletions engine/src/flutter/shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterSemanticsScrollView.mm",
"framework/Source/FlutterSharedApplication.h",
"framework/Source/FlutterSharedApplication.mm",
"framework/Source/FlutterShorebirdProtectedDataGate.h",
"framework/Source/FlutterShorebirdProtectedDataGate.mm",
"framework/Source/FlutterSpellCheckPlugin.h",
"framework/Source/FlutterSpellCheckPlugin.mm",
"framework/Source/FlutterTextInputDelegate.h",
Expand Down
Loading
Loading