Skip to content

Commit 9f5a12c

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Cancel pending Fabric surface starts (#58977)
Summary: Pull Request resolved: #58977 Surface startup is deferred to the main queue. A stop request that arrived before the deferred work previously returned without cancelling it, allowing the surface to start after its owner began tearing down. Track pending starts under the surface mutex so stop can cancel them before registration and startup. Keep attachment and startup serialized, and notify stage changes after releasing the mutex. Changelog: [iOS][Fixed] - Prevent Fabric surfaces from starting after an earlier stop request Reviewed By: christophpurrer Differential Revision: D124262921 fbshipit-source-id: 59d1f8af2900d204df06106b02b92f68356ab7b2
1 parent 9641acd commit 9f5a12c

2 files changed

Lines changed: 106 additions & 18 deletions

File tree

‎packages/react-native/React/Fabric/Surface/RCTFabricSurface.mm‎

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,8 @@ @implementation RCTFabricSurface {
3737
// hence we wrap a value into `optional` to workaround it.
3838
std::optional<SurfaceHandler> _surfaceHandler;
3939

40-
// Protects Surface's start and stop processes.
41-
// Even though SurfaceHandler is tread-safe, it will crash if we try to stop a surface that is not running.
42-
// To make the API easy to use, we check the status of the surface before calling `start` or `stop`,
43-
// and we need this mutex to prevent races.
4440
std::mutex _surfaceMutex;
41+
BOOL _startPending;
4542

4643
// Can be accessed from the main thread only.
4744
RCTSurfaceView *_Nullable _view;
@@ -93,36 +90,45 @@ - (void)dealloc
9390

9491
- (void)start
9592
{
96-
std::lock_guard<std::mutex> lock(_surfaceMutex);
93+
{
94+
std::lock_guard<std::mutex> lock(_surfaceMutex);
9795

98-
if (_surfaceHandler->getStatus() != SurfaceHandler::Status::Registered) {
99-
return;
96+
if (_startPending || _surfaceHandler->getStatus() != SurfaceHandler::Status::Registered) {
97+
return;
98+
}
99+
100+
_startPending = YES;
100101
}
101102

102-
// We need to register a root view component here synchronously because right after
103-
// we start a surface, it can initiate an update that can query the root component.
104103
RCTExecuteOnMainQueue(^{
105-
[self->_surfacePresenter.mountingManager attachSurfaceToView:self.view
106-
surfaceId:self->_surfaceHandler->getSurfaceId()];
107-
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
108-
// This runs two async hops later; a concurrent instance teardown/reload may have unregistered
109-
// the surface since the check in -start. Without this re-check SurfaceHandler::start()
110-
// dereferences a now-null uiManager.
111-
if (self->_surfaceHandler->getStatus() != SurfaceHandler::Status::Registered) {
104+
{
105+
std::lock_guard<std::mutex> lock(self->_surfaceMutex);
106+
107+
if (!self->_startPending || self->_surfaceHandler->getStatus() != SurfaceHandler::Status::Registered) {
108+
self->_startPending = NO;
112109
return;
113110
}
111+
112+
// We need to register a root view component here synchronously because right after
113+
// we start a surface, it can initiate an update that can query the root component.
114+
[self->_surfacePresenter.mountingManager attachSurfaceToView:self.view
115+
surfaceId:self->_surfaceHandler->getSurfaceId()];
114116
self->_surfaceHandler->start();
115-
[self _propagateStageChange];
117+
self->_startPending = NO;
116118

117119
[self->_surfacePresenter setupAnimationDriverWithSurfaceHandler:*self->_surfaceHandler];
118-
});
120+
}
121+
122+
[self _propagateStageChange];
119123
});
120124
}
121125

122126
- (void)stop
123127
{
124128
std::lock_guard<std::mutex> lock(_surfaceMutex);
125129

130+
_startPending = NO;
131+
126132
if (_surfaceHandler->getStatus() != SurfaceHandler::Status::Running) {
127133
return;
128134
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTFabricSurface.h>
9+
#import <React/RCTSurfacePresenter.h>
10+
#import <React/RCTSurfaceView.h>
11+
#import <XCTest/XCTest.h>
12+
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
13+
#import <react/utils/ContextContainer.h>
14+
15+
using facebook::react::ContextContainer;
16+
using facebook::react::RuntimeExecutor;
17+
using facebook::react::RuntimeScheduler;
18+
using facebook::react::RuntimeSchedulerKey;
19+
using facebook::react::SurfaceHandler;
20+
21+
@interface RCTFabricSurfaceLifecycleTests : XCTestCase
22+
@end
23+
24+
@implementation RCTFabricSurfaceLifecycleTests {
25+
std::shared_ptr<RuntimeScheduler> _runtimeScheduler;
26+
RCTSurfacePresenter *_surfacePresenter;
27+
}
28+
29+
- (void)setUp
30+
{
31+
[super setUp];
32+
33+
RuntimeExecutor runtimeExecutor = [](std::function<void(facebook::jsi::Runtime &)> &&) {};
34+
_runtimeScheduler = std::make_shared<RuntimeScheduler>(runtimeExecutor);
35+
36+
auto contextContainer = std::make_shared<ContextContainer>();
37+
contextContainer->insert(RuntimeSchedulerKey, std::weak_ptr<RuntimeScheduler>(_runtimeScheduler));
38+
39+
_surfacePresenter = [[RCTSurfacePresenter alloc] initWithContextContainer:contextContainer
40+
runtimeExecutor:runtimeExecutor
41+
bridgelessBindingsExecutor:std::nullopt];
42+
}
43+
44+
- (void)tearDown
45+
{
46+
[_surfacePresenter suspend];
47+
_surfacePresenter = nil;
48+
_runtimeScheduler.reset();
49+
50+
[super tearDown];
51+
}
52+
53+
- (void)testStopCancelsPendingStart
54+
{
55+
RCTFabricSurface *surface = [[RCTFabricSurface alloc] initWithSurfacePresenter:_surfacePresenter
56+
moduleName:@""
57+
initialProperties:@{}];
58+
XCTestExpectation *lifecycleSettled = [self expectationWithDescription:@"Deferred surface start settled"];
59+
dispatch_queue_t lifecycleQueue = dispatch_queue_create("RCTFabricSurfaceLifecycleTests", DISPATCH_QUEUE_SERIAL);
60+
61+
XCTAssertTrue([NSThread isMainThread]);
62+
dispatch_sync(lifecycleQueue, ^{
63+
[surface start];
64+
[surface stop];
65+
});
66+
dispatch_async(dispatch_get_main_queue(), ^{
67+
[lifecycleSettled fulfill];
68+
});
69+
70+
[self waitForExpectations:@[ lifecycleSettled ] timeout:2];
71+
72+
XCTAssertEqual(surface.surfaceHandler.getStatus(), SurfaceHandler::Status::Registered);
73+
XCTAssertEqual(surface.view.subviews.count, 0u);
74+
75+
[surface start];
76+
XCTAssertEqual(surface.surfaceHandler.getStatus(), SurfaceHandler::Status::Running);
77+
78+
[surface stop];
79+
XCTAssertEqual(surface.surfaceHandler.getStatus(), SurfaceHandler::Status::Registered);
80+
}
81+
82+
@end

0 commit comments

Comments
 (0)