Skip to content

Commit d13b250

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Coordinate inspector and transport shutdown
Summary: Retain the inspector until serialized target and socket cleanup has completed, reject late work during shutdown, and join the dispatcher from its external owner. Keep transport close requests callable from IO callbacks without self-joining or holding the socket lock across a join. Serialize writes and pings on the IO executor, discard expired implementation references, and make external-owner destruction finish cleanup when a closed callback throws. Add focused inspector lifetime and real loopback transport regressions, including active-IO destruction, nested close, late connection completions, and invalid descriptors. Destruction of a socket wrapper on its own IO thread remains unsupported. Changelog: [General][Fixed] - Fix native inspector ownership and transport shutdown races Differential Revision: D124675718
1 parent 0c58083 commit d13b250

8 files changed

Lines changed: 808 additions & 77 deletions

File tree

‎packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,36 @@ Inspector::Inspector(
135135
httpClientFactory_(std::move(httpClientFactory)) {}
136136

137137
Inspector::~Inspector() noexcept {
138-
if (pageId_) {
139-
jsinspector_modern::getInspectorInstance().removePage(*pageId_);
140-
taskDispatchThread_.runSync([weakThis = weak_from_this()]() {
141-
if (auto strongThis = weakThis.lock()) {
142-
strongThis->pageId_.reset();
143-
strongThis->target_.reset();
144-
}
145-
});
138+
shutdown();
139+
}
140+
141+
void Inspector::shutdown() noexcept {
142+
CHECK(!taskDispatchThread_.isOnThread());
143+
if (shuttingDown_.exchange(true)) {
144+
return;
146145
}
146+
taskDispatchThread_.runSync([this]() {
147+
taskDispatchThread_.cancelPendingTasks();
148+
if (pageId_) {
149+
jsinspector_modern::getInspectorInstance().removePage(*pageId_);
150+
pageId_.reset();
151+
}
152+
if (packagerConnection_) {
153+
packagerConnection_->closeQuietly();
154+
packagerConnection_.reset();
155+
}
156+
target_.reset();
157+
hostDelegate_.reset();
158+
});
147159
taskDispatchThread_.quit();
148160
}
149161

162+
void Inspector::runOnInspectorThreadSync(
163+
TaskDispatchThread::TaskFn&& callback) {
164+
CHECK(!shuttingDown_);
165+
taskDispatchThread_.runSync(std::move(callback));
166+
}
167+
150168
void Inspector::connectDebugger(const std::string& inspectorUrl) noexcept {
151169
invokeElsePost([weakThis = weak_from_this(), inspectorUrl]() {
152170
auto self = weakThis.lock();
@@ -205,11 +223,20 @@ void Inspector::ensureHostTarget(
205223
void Inspector::invokeElsePost(
206224
TaskDispatchThread::TaskFn&& callback,
207225
std::chrono::milliseconds delayMs) {
226+
if (shuttingDown_) {
227+
return;
228+
}
208229
if (taskDispatchThread_.isOnThread() &&
209230
delayMs == std::chrono::milliseconds::zero()) {
210231
callback();
211232
} else {
212-
taskDispatchThread_.runAsync(std::move(callback), delayMs);
233+
taskDispatchThread_.runAsync(
234+
[weakThis = weak_from_this(), callback = std::move(callback)]() {
235+
if (auto self = weakThis.lock(); self && !self->shuttingDown_) {
236+
callback();
237+
}
238+
},
239+
delayMs);
213240
}
214241
}
215242

‎packages/react-native/ReactCxxPlatform/react/devsupport/inspector/Inspector.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <react/http/IHttpClient.h>
1515
#include <react/http/IWebSocketClient.h>
1616
#include <react/threading/TaskDispatchThread.h>
17+
#include <atomic>
1718
#include <functional>
1819
#include <memory>
1920
#include <optional>
@@ -37,6 +38,11 @@ class Inspector : public InspectorThread, public std::enable_shared_from_this<In
3738
Inspector(Inspector &&other) = delete;
3839
Inspector &operator=(Inspector &&other) = delete;
3940

41+
// Call from the owner thread before releasing the last owning reference.
42+
void shutdown() noexcept;
43+
44+
void runOnInspectorThreadSync(TaskDispatchThread::TaskFn &&callback);
45+
4046
void connectDebugger(const std::string &inspectorUrl) noexcept;
4147

4248
void ensureHostTarget(
@@ -50,6 +56,7 @@ class Inspector : public InspectorThread, public std::enable_shared_from_this<In
5056
TaskDispatchThread::TaskFn &&callback,
5157
std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) override;
5258

59+
std::atomic<bool> shuttingDown_{false};
5360
TaskDispatchThread taskDispatchThread_{"InspectorThread"};
5461
std::string appName_;
5562
std::string deviceName_;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
#include <gtest/gtest.h>
9+
#include <jsinspector-modern/InspectorInterfaces.h>
10+
#include <react/devsupport/inspector/Inspector.h>
11+
#include <chrono>
12+
#include <thread>
13+
14+
namespace facebook::react {
15+
16+
class InspectorShutdownTest : public ::testing::Test {
17+
protected:
18+
struct SocketState {
19+
IWebSocketClient::OnConnectCallback onConnect;
20+
IWebSocketClient::OnClosedCallback onClosed;
21+
IWebSocketClient::OnMessageCallback onMessage;
22+
std::weak_ptr<Inspector> inspector;
23+
std::thread::id closeThread;
24+
bool ownedDuringClose{false};
25+
int closes{0};
26+
};
27+
28+
class Socket final : public IWebSocketClient {
29+
public:
30+
explicit Socket(std::shared_ptr<SocketState> state)
31+
: state_(std::move(state)) {}
32+
33+
void setOnClosedCallback(OnClosedCallback&& callback) noexcept override {
34+
state_->onClosed = std::move(callback);
35+
}
36+
37+
void setOnMessageCallback(OnMessageCallback&& callback) noexcept override {
38+
state_->onMessage = std::move(callback);
39+
}
40+
41+
void connect(const std::string& /*url*/, OnConnectCallback&& callback)
42+
override {
43+
state_->onConnect = std::move(callback);
44+
}
45+
46+
void close(const std::string& reason) override {
47+
++state_->closes;
48+
state_->closeThread = std::this_thread::get_id();
49+
state_->ownedDuringClose = !state_->inspector.expired();
50+
if (state_->onClosed) {
51+
state_->onClosed(reason);
52+
}
53+
}
54+
55+
void send(const std::string& /*message*/) override {}
56+
void ping() override {}
57+
58+
private:
59+
std::shared_ptr<SocketState> state_;
60+
};
61+
62+
void SetUp() override {
63+
socket = std::make_shared<SocketState>();
64+
inspector = std::make_shared<Inspector>(
65+
"shutdown-test",
66+
"test-device",
67+
[state = socket]() { return std::make_unique<Socket>(state); },
68+
[]() { return std::unique_ptr<IHttpClient>{}; });
69+
socket->inspector = inspector;
70+
}
71+
72+
void TearDown() override {
73+
if (inspector) {
74+
inspector->shutdown();
75+
}
76+
}
77+
78+
std::shared_ptr<SocketState> socket;
79+
std::shared_ptr<Inspector> inspector;
80+
};
81+
82+
TEST_F(InspectorShutdownTest, ShutdownDisposesPageAndSocketWhileOwned) {
83+
auto pageCount = jsinspector_modern::getInspectorInstance().getPages().size();
84+
inspector->ensureHostTarget({}, {});
85+
EXPECT_NE(inspector->inspectorTarget(), nullptr);
86+
inspector->connectDebugger("http://localhost:8081/inspector/device");
87+
std::thread::id inspectorThread;
88+
inspector->runOnInspectorThreadSync(
89+
[&]() { inspectorThread = std::this_thread::get_id(); });
90+
ASSERT_TRUE(socket->onConnect);
91+
EXPECT_EQ(
92+
jsinspector_modern::getInspectorInstance().getPages().size(),
93+
pageCount + 1);
94+
95+
inspector->shutdown();
96+
97+
EXPECT_EQ(inspector->inspectorTarget(), nullptr);
98+
EXPECT_EQ(
99+
jsinspector_modern::getInspectorInstance().getPages().size(), pageCount);
100+
EXPECT_EQ(socket->closes, 1);
101+
EXPECT_EQ(socket->closeThread, inspectorThread);
102+
EXPECT_TRUE(socket->ownedDuringClose);
103+
EXPECT_EQ(inspector.use_count(), 1);
104+
inspector->shutdown();
105+
EXPECT_EQ(socket->closes, 1);
106+
inspector.reset();
107+
EXPECT_TRUE(socket->inspector.expired());
108+
}
109+
110+
TEST_F(
111+
InspectorShutdownTest,
112+
ShutdownDropsDelayedOwnersAndRejectsLateCallbacks) {
113+
InspectorThread& dispatch = *inspector;
114+
bool ran = false;
115+
dispatch.invokeElsePost(
116+
[owner = inspector, &ran]() { ran = true; }, std::chrono::hours(1));
117+
118+
inspector->shutdown();
119+
120+
EXPECT_FALSE(ran);
121+
EXPECT_EQ(inspector.use_count(), 1);
122+
dispatch.invokeElsePost([&]() { ran = true; });
123+
dispatch.invokeElsePost([&]() { ran = true; }, std::chrono::hours(1));
124+
inspector->connectDebugger("http://localhost:8081/inspector/device");
125+
EXPECT_FALSE(ran);
126+
EXPECT_FALSE(socket->onConnect);
127+
inspector.reset();
128+
EXPECT_TRUE(socket->inspector.expired());
129+
}
130+
131+
TEST_F(
132+
InspectorShutdownTest,
133+
LateTransportFailureCannotReconnectAfterShutdown) {
134+
inspector->connectDebugger("http://localhost:8081/inspector/device");
135+
inspector->runOnInspectorThreadSync([]() {});
136+
ASSERT_TRUE(socket->onConnect);
137+
auto onConnect = socket->onConnect;
138+
auto onMessage = socket->onMessage;
139+
140+
inspector->shutdown();
141+
onConnect(false, "late failure");
142+
onMessage(R"({"event":"getPages"})");
143+
144+
EXPECT_EQ(socket->closes, 1);
145+
EXPECT_EQ(inspector.use_count(), 1);
146+
inspector.reset();
147+
EXPECT_TRUE(socket->inspector.expired());
148+
}
149+
150+
} // namespace facebook::react

0 commit comments

Comments
 (0)