Remove legacy InspectorPackagerConnection (#44746)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44746

Cleanup after this experiment's removal in D57730921.

Changelog: [Internal]

Reviewed By: hoxyq

Differential Revision: D58017460

fbshipit-source-id: f9328875d6ef9a76599ae560c58cef6caab87a57
This commit is contained in:
Alex Hunt
2024-06-03 06:04:50 -07:00
committed by Facebook GitHub Bot
parent ec1742a6bb
commit c144382316
5 changed files with 3 additions and 661 deletions
@@ -14,7 +14,6 @@
#import <React/RCTCxxInspectorPackagerConnection.h>
#import <React/RCTDefines.h>
#import <React/RCTInspectorPackagerConnection.h>
#import <CommonCrypto/CommonCrypto.h>
#import <jsinspector-modern/InspectorFlags.h>
@@ -181,12 +180,7 @@ static void sendEventToAllConnections(NSString *event)
NSString *key = [inspectorURL absoluteString];
id<RCTInspectorPackagerConnectionProtocol> connection = socketConnections[key];
if (!connection || !connection.isConnected) {
if (facebook::react::jsinspector_modern::InspectorFlags::getInstance().getFuseboxEnabled()) {
connection = [[RCTCxxInspectorPackagerConnection alloc] initWithURL:inspectorURL];
} else {
// TODO(T190163403): Remove legacy RCTInspectorPackagerConnection
connection = [[RCTInspectorPackagerConnection alloc] initWithURL:inspectorURL];
}
connection = [[RCTCxxInspectorPackagerConnection alloc] initWithURL:inspectorURL];
socketConnections[key] = connection;
[connection connect];
@@ -1,325 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTInspectorPackagerConnection.h>
#if RCT_DEV || RCT_REMOTE_PROFILE
#import <React/RCTDefines.h>
#import <React/RCTInspector.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <SocketRocket/SRWebSocket.h>
// This is a port of the Android impl, at
// ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorPackagerConnection.java
// please keep consistent :)
const int RECONNECT_DELAY_MS = 2000;
@interface RCTInspectorPackagerConnection () <SRWebSocketDelegate> {
NSURL *_url;
NSMutableDictionary<NSString *, RCTInspectorLocalConnection *> *_inspectorConnections;
SRWebSocket *_webSocket;
BOOL _closed;
BOOL _suppressConnectionErrors;
}
@end
@interface RCTInspectorRemoteConnection () {
__weak RCTInspectorPackagerConnection *_owningPackagerConnection;
NSString *_pageId;
}
- (instancetype)initWithPackagerConnection:(RCTInspectorPackagerConnection *)owningPackagerConnection
pageId:(NSString *)pageId;
@end
static NSDictionary<NSString *, id> *makePageIdPayload(NSString *pageId)
{
return @{@"pageId" : pageId};
}
@implementation RCTInspectorPackagerConnection
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (instancetype)initWithURL:(NSURL *)url
{
if (self = [super init]) {
_url = url;
_inspectorConnections = [NSMutableDictionary new];
}
return self;
}
- (void)handleProxyMessage:(NSDictionary<NSString *, id> *)message
{
NSString *event = message[@"event"];
NSDictionary *payload = message[@"payload"];
if ([@"getPages" isEqualToString:event]) {
[self sendEvent:event payload:[self pages]];
} else if ([@"wrappedEvent" isEqualToString:event]) {
[self handleWrappedEvent:payload];
} else if ([@"connect" isEqualToString:event]) {
[self handleConnect:payload];
} else if ([@"disconnect" isEqualToString:event]) {
[self handleDisconnect:payload];
} else {
RCTLogError(@"Unknown event: %@", event);
}
}
- (void)sendEventToAllConnections:(NSString *)event
{
for (NSString *pageId in _inspectorConnections) {
[_inspectorConnections[pageId] sendMessage:event];
}
}
- (void)closeAllConnections
{
for (NSString *pageId in _inspectorConnections) {
[[_inspectorConnections objectForKey:pageId] disconnect];
}
[_inspectorConnections removeAllObjects];
}
- (void)handleConnect:(NSDictionary *)payload
{
NSString *pageId = payload[@"pageId"];
RCTInspectorLocalConnection *existingConnection = _inspectorConnections[pageId];
if (existingConnection) {
[_inspectorConnections removeObjectForKey:pageId];
[existingConnection disconnect];
RCTLogWarn(@"Already connected: %@", pageId);
return;
}
RCTInspectorRemoteConnection *remoteConnection =
[[RCTInspectorRemoteConnection alloc] initWithPackagerConnection:self pageId:pageId];
RCTInspectorLocalConnection *inspectorConnection = [RCTInspector connectPage:[pageId integerValue]
forRemoteConnection:remoteConnection];
_inspectorConnections[pageId] = inspectorConnection;
}
- (void)handleDisconnect:(NSDictionary *)payload
{
NSString *pageId = payload[@"pageId"];
RCTInspectorLocalConnection *inspectorConnection = _inspectorConnections[pageId];
if (inspectorConnection) {
[self removeConnectionForPage:pageId];
[inspectorConnection disconnect];
}
}
- (void)removeConnectionForPage:(NSString *)pageId
{
[_inspectorConnections removeObjectForKey:pageId];
}
- (void)handleWrappedEvent:(NSDictionary *)payload
{
NSString *pageId = payload[@"pageId"];
NSString *wrappedEvent = payload[@"wrappedEvent"];
RCTInspectorLocalConnection *inspectorConnection = _inspectorConnections[pageId];
if (!inspectorConnection) {
RCTLogWarn(@"Not connected to page: %@ , failed trying to handle event: %@", pageId, wrappedEvent);
return;
}
[inspectorConnection sendMessage:wrappedEvent];
}
- (NSArray *)pages
{
NSArray<RCTInspectorPage *> *pages = [RCTInspector pages];
NSMutableArray *array = [NSMutableArray arrayWithCapacity:pages.count];
for (RCTInspectorPage *page in pages) {
NSDictionary *jsonPage = @{
@"id" : [@(page.id) stringValue],
@"title" : page.title,
@"app" : [[NSBundle mainBundle] bundleIdentifier],
@"vm" : page.vm,
};
[array addObject:jsonPage];
}
return array;
}
- (void)sendWrappedEvent:(NSString *)pageId message:(NSString *)message
{
NSDictionary *payload = @{
@"pageId" : pageId,
@"wrappedEvent" : message,
};
[self sendEvent:@"wrappedEvent" payload:payload];
}
- (void)sendEvent:(NSString *)name payload:(id)payload
{
NSDictionary *jsonMessage = @{
@"event" : name,
@"payload" : payload,
};
[self sendToPackager:jsonMessage];
}
// analogous to InspectorPackagerConnection.Connection.onFailure(...)
- (void)webSocket:(__unused SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
if (_webSocket) {
[self abort:@"Websocket exception" withCause:error];
}
if (!_closed && [error code] != ECONNREFUSED) {
[self reconnect];
}
}
// analogous to InspectorPackagerConnection.Connection.onMessage(...)
- (void)webSocket:(__unused SRWebSocket *)webSocket didReceiveMessageWithString:(nonnull NSString *)messageText
{
NSError *error = nil;
id parsedJSON = RCTJSONParse(messageText, &error);
if (error) {
RCTLogWarn(@"Unrecognized inspector message, string was not valid JSON: %@", messageText);
return;
}
[self handleProxyMessage:parsedJSON];
}
// analogous to InspectorPackagerConnection.Connection.onClosed(...)
- (void)webSocket:(__unused SRWebSocket *)webSocket
didCloseWithCode:(__unused NSInteger)code
reason:(__unused NSString *)reason
wasClean:(__unused BOOL)wasClean
{
_webSocket = nil;
[self closeAllConnections];
if (!_closed) {
[self reconnect];
}
}
- (bool)isConnected
{
return _webSocket != nil;
}
- (void)connect
{
if (_closed) {
RCTLogError(@"Illegal state: Can't connect after having previously been closed.");
return;
}
// The corresponding android code has a lot of custom config options for
// timeouts, but our previous class, RCTSRWebSocket didn't have the same
// implemented options. Might be worth reinvestigating for SRWebSocket?
_webSocket = [[SRWebSocket alloc] initWithURL:_url];
_webSocket.delegate = self;
[_webSocket open];
}
- (void)reconnect
{
if (_closed) {
RCTLogError(@"Illegal state: Can't reconnect after having previously been closed.");
return;
}
if (_suppressConnectionErrors) {
RCTLogWarn(@"Couldn't connect to packager, will silently retry");
_suppressConnectionErrors = true;
}
__weak RCTInspectorPackagerConnection *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, RECONNECT_DELAY_MS * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
RCTInspectorPackagerConnection *strongSelf = weakSelf;
if (strongSelf && !strongSelf->_closed) {
[strongSelf connect];
}
});
}
- (void)closeQuietly
{
_closed = true;
[self disposeWebSocket];
}
- (void)sendToPackager:(NSDictionary *)messageObject
{
__weak RCTInspectorPackagerConnection *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
RCTInspectorPackagerConnection *strongSelf = weakSelf;
if (strongSelf && !strongSelf->_closed) {
NSError *error;
NSString *messageText = RCTJSONStringify(messageObject, &error);
if (error) {
RCTLogWarn(@"Couldn't send event to packager: %@", error);
} else {
[strongSelf->_webSocket sendString:messageText error:nil];
}
}
});
}
- (void)abort:(NSString *)message withCause:(NSError *)cause
{
// Don't log ECONNREFUSED at all; it's expected in cases where the server isn't listening.
if (![cause.domain isEqual:NSPOSIXErrorDomain] || cause.code != ECONNREFUSED) {
RCTLogInfo(@"Error occurred, shutting down websocket connection: %@ %@", message, cause);
}
[self closeAllConnections];
[self disposeWebSocket];
}
- (void)disposeWebSocket
{
if (_webSocket) {
[_webSocket closeWithCode:1000 reason:@"End of session"];
_webSocket.delegate = nil;
_webSocket = nil;
}
}
@end
@implementation RCTInspectorRemoteConnection
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (instancetype)initWithPackagerConnection:(RCTInspectorPackagerConnection *)owningPackagerConnection
pageId:(NSString *)pageId
{
if (self = [super init]) {
_owningPackagerConnection = owningPackagerConnection;
_pageId = pageId;
}
return self;
}
- (void)onMessage:(NSString *)message
{
[_owningPackagerConnection sendWrappedEvent:_pageId message:message];
}
- (void)onDisconnect
{
RCTInspectorPackagerConnection *owningPackagerConnectionStrong = _owningPackagerConnection;
if (owningPackagerConnectionStrong) {
[owningPackagerConnectionStrong removeConnectionForPage:_pageId];
[owningPackagerConnectionStrong sendEvent:@"disconnect" payload:makePageIdPayload(_pageId)];
}
}
@end
#endif
@@ -2184,13 +2184,6 @@ public final class com/facebook/react/devsupport/InspectorFlags {
public static final fun getFuseboxEnabled ()Z
}
public class com/facebook/react/devsupport/InspectorPackagerConnection : com/facebook/react/devsupport/IInspectorPackagerConnection {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun closeQuietly ()V
public fun connect ()V
public fun sendEventToAllConnections (Ljava/lang/String;)V
}
public class com/facebook/react/devsupport/JSCHeapCapture : com/facebook/fbreact/specs/NativeJSCHeapCaptureSpec {
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
public fun captureComplete (Ljava/lang/String;Ljava/lang/String;)V
@@ -210,14 +210,8 @@ public class DevServerHelper {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (InspectorFlags.getFuseboxEnabled()) {
mInspectorPackagerConnection =
new CxxInspectorPackagerConnection(getInspectorDeviceUrl(), mPackageName);
} else {
// TODO(T190163403): Remove legacy InspectorPackagerConnection
mInspectorPackagerConnection =
new InspectorPackagerConnection(getInspectorDeviceUrl(), mPackageName);
}
mInspectorPackagerConnection =
new CxxInspectorPackagerConnection(getInspectorDeviceUrl(), mPackageName);
mInspectorPackagerConnection.connect();
return null;
}
@@ -1,314 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Inspector;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class InspectorPackagerConnection implements IInspectorPackagerConnection {
private static final String TAG = "InspectorPackagerConnection";
private final Connection mConnection;
private final Map<String, Inspector.LocalConnection> mInspectorConnections;
private final String mPackageName;
public InspectorPackagerConnection(String url, String packageName) {
mConnection = new Connection(url);
mInspectorConnections = new HashMap<>();
mPackageName = packageName;
}
public void connect() {
mConnection.connect();
}
public void closeQuietly() {
mConnection.close();
}
public void sendEventToAllConnections(String event) {
for (Map.Entry<String, Inspector.LocalConnection> inspectorConnectionEntry :
mInspectorConnections.entrySet()) {
Inspector.LocalConnection inspectorConnection = inspectorConnectionEntry.getValue();
inspectorConnection.sendMessage(event);
}
}
void handleProxyMessage(JSONObject message) throws JSONException, IOException {
String event = message.getString("event");
switch (event) {
case "getPages":
sendEvent("getPages", getPages());
break;
case "wrappedEvent":
handleWrappedEvent(message.getJSONObject("payload"));
break;
case "connect":
handleConnect(message.getJSONObject("payload"));
break;
case "disconnect":
handleDisconnect(message.getJSONObject("payload"));
break;
default:
throw new IllegalArgumentException("Unknown event: " + event);
}
}
void closeAllConnections() {
for (Map.Entry<String, Inspector.LocalConnection> entry : mInspectorConnections.entrySet()) {
entry.getValue().disconnect();
}
mInspectorConnections.clear();
}
private void handleConnect(JSONObject payload) throws JSONException {
final String pageId = payload.getString("pageId");
Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId);
if (inspectorConnection != null) {
throw new IllegalStateException("Already connected: " + pageId);
}
try {
// TODO: Use strings for id's too
inspectorConnection =
Inspector.connect(
Integer.parseInt(pageId),
new Inspector.RemoteConnection() {
@Override
public void onMessage(String message) {
try {
sendWrappedEvent(pageId, message);
} catch (JSONException e) {
FLog.w(TAG, "Couldn't send event to packager", e);
}
}
@Override
public void onDisconnect() {
try {
mInspectorConnections.remove(pageId);
sendEvent("disconnect", makePageIdPayload(pageId));
} catch (JSONException e) {
FLog.w(TAG, "Couldn't send event to packager", e);
}
}
});
mInspectorConnections.put(pageId, inspectorConnection);
} catch (Exception e) {
FLog.w(TAG, "Failed to open page: " + pageId, e);
sendEvent("disconnect", makePageIdPayload(pageId));
}
}
private void handleDisconnect(JSONObject payload) throws JSONException {
final String pageId = payload.getString("pageId");
Inspector.LocalConnection inspectorConnection = mInspectorConnections.remove(pageId);
if (inspectorConnection == null) {
return;
}
inspectorConnection.disconnect();
}
private void handleWrappedEvent(JSONObject payload) throws JSONException {
final String pageId = payload.getString("pageId");
String wrappedEvent = payload.getString("wrappedEvent");
Inspector.LocalConnection inspectorConnection = mInspectorConnections.get(pageId);
if (inspectorConnection == null) {
// This tends to happen during reloads, so don't panic.
FLog.w(TAG, "PageID " + pageId + " is disconnected. Dropping event: " + wrappedEvent);
return;
}
inspectorConnection.sendMessage(wrappedEvent);
}
private JSONArray getPages() throws JSONException {
List<Inspector.Page> pages = Inspector.getPages();
JSONArray array = new JSONArray();
for (Inspector.Page page : pages) {
JSONObject jsonPage = new JSONObject();
jsonPage.put("id", String.valueOf(page.getId()));
jsonPage.put("title", page.getTitle());
jsonPage.put("app", mPackageName);
jsonPage.put("vm", page.getVM());
array.put(jsonPage);
}
return array;
}
private void sendWrappedEvent(String pageId, String message) throws JSONException {
JSONObject payload = new JSONObject();
payload.put("pageId", pageId);
payload.put("wrappedEvent", message);
sendEvent("wrappedEvent", payload);
}
private void sendEvent(String name, Object payload) throws JSONException {
JSONObject jsonMessage = new JSONObject();
jsonMessage.put("event", name);
jsonMessage.put("payload", payload);
mConnection.send(jsonMessage);
}
private JSONObject makePageIdPayload(String pageId) throws JSONException {
JSONObject payload = new JSONObject();
payload.put("pageId", pageId);
return payload;
}
private class Connection extends WebSocketListener {
private static final int RECONNECT_DELAY_MS = 2000;
private final String mUrl;
private OkHttpClient mHttpClient;
private @Nullable WebSocket mWebSocket;
private final Handler mHandler;
private boolean mClosed;
private boolean mSuppressConnectionErrors;
public Connection(String url) {
mUrl = url;
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
mWebSocket = webSocket;
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
if (mWebSocket != null) {
abort("Websocket exception", t);
}
if (!mClosed) {
reconnect();
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
try {
handleProxyMessage(new JSONObject(text));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
mWebSocket = null;
closeAllConnections();
if (!mClosed) {
reconnect();
}
}
public void connect() {
if (mClosed) {
throw new IllegalStateException("Can't connect closed client");
}
if (mHttpClient == null) {
mHttpClient =
new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
.build();
}
Request request = new Request.Builder().url(mUrl).build();
mHttpClient.newWebSocket(request, this);
}
private void reconnect() {
if (mClosed) {
throw new IllegalStateException("Can't reconnect closed client");
}
if (!mSuppressConnectionErrors) {
FLog.w(TAG, "Couldn't connect to packager, will silently retry");
mSuppressConnectionErrors = true;
}
mHandler.postDelayed(
new Runnable() {
@Override
public void run() {
// check that we haven't been closed in the meantime
if (!mClosed) {
connect();
}
}
},
RECONNECT_DELAY_MS);
}
public void close() {
mClosed = true;
if (mWebSocket != null) {
try {
mWebSocket.close(1000, "End of session");
} catch (Exception e) {
// swallow, no need to handle it here
}
mWebSocket = null;
}
}
public void send(final JSONObject object) {
new AsyncTask<WebSocket, Void, Void>() {
@Override
protected Void doInBackground(WebSocket... sockets) {
if (sockets == null || sockets.length == 0) {
return null;
}
try {
sockets[0].send(object.toString());
} catch (Exception e) {
FLog.w(TAG, "Couldn't send event to packager", e);
}
return null;
}
}.execute(mWebSocket);
}
private void abort(String message, Throwable cause) {
FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
closeAllConnections();
closeWebSocketQuietly();
}
private void closeWebSocketQuietly() {
if (mWebSocket != null) {
try {
mWebSocket.close(1000, "End of session");
} catch (Exception e) {
// swallow, no need to handle it here
}
mWebSocket = null;
}
}
}
}