first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
/*
* 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 <UIKit/UIKit.h>
@class RCTLoadingProgress;
@protocol RCTDevLoadingViewProtocol <NSObject>
+ (void)setEnabled:(BOOL)enabled;
- (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor;
- (void)showWithURL:(NSURL *)URL;
- (void)updateProgress:(RCTLoadingProgress *)progress;
- (void)hide;
@end

View File

@@ -0,0 +1,11 @@
/*
* 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/RCTDefines.h>
RCT_EXTERN void RCTDevLoadingViewSetEnabled(BOOL enabled);
RCT_EXTERN BOOL RCTDevLoadingViewGetEnabled(void);

View File

@@ -0,0 +1,24 @@
/*
* 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 "RCTDevLoadingViewSetEnabled.h"
#if RCT_DEV_MENU
static BOOL isDevLoadingViewEnabled = YES;
#else
static BOOL isDevLoadingViewEnabled = NO;
#endif
void RCTDevLoadingViewSetEnabled(BOOL enabled)
{
isDevLoadingViewEnabled = enabled;
}
BOOL RCTDevLoadingViewGetEnabled(void)
{
return isDevLoadingViewEnabled;
}

View File

@@ -0,0 +1,24 @@
/*
* 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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTDefines.h>
#import <React/RCTInspectorPackagerConnection.h>
#if RCT_DEV || RCT_REMOTE_PROFILE
@interface RCTInspectorDevServerHelper : NSObject
+ (id<RCTInspectorPackagerConnectionProtocol>)connectWithBundleURL:(NSURL *)bundleURL;
+ (void)disableDebugger;
+ (BOOL)isPackagerDisconnected;
+ (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessage;
@end
#endif

View File

@@ -0,0 +1,201 @@
/*
* 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/RCTInspectorDevServerHelper.h>
#if RCT_DEV || RCT_REMOTE_PROFILE
#import <React/RCTLog.h>
#import <UIKit/UIKit.h>
#import <React/RCTCxxInspectorPackagerConnection.h>
#import <React/RCTDefines.h>
#import <CommonCrypto/CommonCrypto.h>
#import <jsinspector-modern/InspectorFlags.h>
static NSString *const kDebuggerMsgDisable = @"{ \"id\":1,\"method\":\"Debugger.disable\" }";
static NSString *getServerHost(NSURL *bundleURL)
{
NSNumber *port = @8081;
NSString *portStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"RCT_METRO_PORT"];
if ((portStr != nullptr) && [portStr length] > 0) {
port = [NSNumber numberWithInt:[portStr intValue]];
}
if ([bundleURL port] != nullptr) {
port = [bundleURL port];
}
NSString *host = [bundleURL host];
if (host == nullptr) {
host = @"localhost";
}
// this is consistent with the Android implementation, where http:// is the
// hardcoded implicit scheme for the debug server. Note, packagerURL
// technically looks like it could handle schemes/protocols other than HTTP,
// so rather than force HTTP, leave it be for now, in case someone is relying
// on that ability when developing against iOS.
return [NSString stringWithFormat:@"%@:%@", host, port];
}
static NSString *getSHA256(NSString *string)
{
const char *str = string.UTF8String;
unsigned char result[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(str, (CC_LONG)strlen(str), result);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0],
result[1],
result[2],
result[3],
result[4],
result[5],
result[6],
result[7],
result[8],
result[9],
result[10],
result[11],
result[12],
result[13],
result[14],
result[15],
result[16],
result[17],
result[18],
result[19]];
}
// Returns an opaque ID which is stable for the current combination of device and app, stable across installs,
// and unique across devices.
static NSString *getInspectorDeviceId()
{
// A bundle ID uniquely identifies a single app throughout the system. [Source: Apple docs]
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
#if TARGET_OS_IPHONE
// An alphanumeric string that uniquely identifies a device to the app's vendor. [Source: Apple docs]
NSString *identifierForVendor = [[UIDevice currentDevice] identifierForVendor].UUIDString;
#else
// macOS does not support UIDevice. Use an empty string, with the assumption
// that we are only building to the current system.
NSString *identifierForVendor = @"";
#endif
auto &inspectorFlags = facebook::react::jsinspector_modern::InspectorFlags::getInstance();
NSString *rawDeviceId = [NSString stringWithFormat:@"apple-%@-%@-%s",
identifierForVendor,
bundleId,
inspectorFlags.getFuseboxEnabled() ? "fusebox" : "legacy"];
return getSHA256(rawDeviceId);
}
static NSURL *getInspectorDeviceUrl(NSURL *bundleURL)
{
auto &inspectorFlags = facebook::react::jsinspector_modern::InspectorFlags::getInstance();
BOOL isProfilingBuild = inspectorFlags.getIsProfilingBuild();
NSString *escapedDeviceName = [[[UIDevice currentDevice] name]
stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet];
NSString *escapedAppName = [[[NSBundle mainBundle] bundleIdentifier]
stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet];
NSString *escapedInspectorDeviceId = [getInspectorDeviceId()
stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet];
return [NSURL
URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@&device=%@&profiling=%@",
getServerHost(bundleURL),
escapedDeviceName,
escapedAppName,
escapedInspectorDeviceId,
isProfilingBuild ? @"true" : @"false"]];
}
@implementation RCTInspectorDevServerHelper
RCT_NOT_IMPLEMENTED(-(instancetype)init)
static NSMutableDictionary<NSString *, id<RCTInspectorPackagerConnectionProtocol>> *socketConnections = nil;
static void sendEventToAllConnections(NSString *event)
{
for (NSString *socketId in socketConnections) {
[socketConnections[socketId] sendEventToAllConnections:event];
}
}
+ (BOOL)isPackagerDisconnected
{
for (NSString *socketId in socketConnections) {
if ([socketConnections[socketId] isConnected]) {
return false;
}
}
return true;
}
+ (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessage
{
NSString *escapedInspectorDeviceId = [getInspectorDeviceId()
stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?device=%@",
getServerHost(bundleURL),
escapedInspectorDeviceId]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
[[[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(
__unused NSData *_Nullable data, __unused NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error != nullptr) {
RCTLogWarn(@"%@", errorMessage);
}
}] resume];
}
+ (void)disableDebugger
{
auto &inspectorFlags = facebook::react::jsinspector_modern::InspectorFlags::getInstance();
if (!inspectorFlags.getFuseboxEnabled()) {
sendEventToAllConnections(kDebuggerMsgDisable);
}
}
+ (id<RCTInspectorPackagerConnectionProtocol>)connectWithBundleURL:(NSURL *)bundleURL
{
NSURL *inspectorURL = getInspectorDeviceUrl(bundleURL);
// Note, using a static dictionary isn't really the greatest design, but
// the packager connection does the same thing, so it's at least consistent.
// This is a static map that holds different inspector clients per the inspectorURL
if (socketConnections == nil) {
socketConnections = [NSMutableDictionary new];
}
NSString *key = [inspectorURL absoluteString];
id<RCTInspectorPackagerConnectionProtocol> connection = socketConnections[key];
if ((connection == nullptr) || !connection.isConnected) {
connection = [[RCTCxxInspectorPackagerConnection alloc] initWithURL:inspectorURL];
socketConnections[key] = connection;
[connection connect];
}
return connection;
}
@end
#endif

View File

@@ -0,0 +1,29 @@
/*
* 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 <Foundation/Foundation.h>
#ifdef __cplusplus
#import <jsinspector-modern/ReactCdp.h>
using RCTInspectorNetworkListener = facebook::react::jsinspector_modern::NetworkRequestListener;
using RCTInspectorNetworkExecutor = facebook::react::jsinspector_modern::ScopedExecutor<RCTInspectorNetworkListener>;
using RCTInspectorLoadNetworkResourceRequest = facebook::react::jsinspector_modern::LoadNetworkResourceRequest;
/**
* A helper class that wraps around NSURLSession to make network requests.
*/
@interface RCTInspectorNetworkHelper : NSObject
- (instancetype)init;
- (void)loadNetworkResourceWithParams:(const RCTInspectorLoadNetworkResourceRequest &)params
executor:(RCTInspectorNetworkExecutor)executor;
@end
#endif

View File

@@ -0,0 +1,115 @@
/*
* 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 "RCTInspectorNetworkHelper.h"
#import <React/RCTLog.h>
using ListenerBlock = void (^)(RCTInspectorNetworkListener *);
@interface RCTInspectorNetworkHelper () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, void (^)(ListenerBlock)> *executorsByTaskId;
- (void)withListenerForTask:(NSURLSessionTask *)task execute:(ListenerBlock)block;
@end
@implementation RCTInspectorNetworkHelper
- (instancetype)init
{
self = [super init];
if (self != nullptr) {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.executorsByTaskId = [NSMutableDictionary new];
}
return self;
}
- (void)loadNetworkResourceWithParams:(const RCTInspectorLoadNetworkResourceRequest &)params
executor:(RCTInspectorNetworkExecutor)executor
{
NSString *urlString = [NSString stringWithCString:params.url.c_str() encoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlString];
auto executorBlock = ^(ListenerBlock func) {
executor([=](RCTInspectorNetworkListener &listener) { func(&listener); });
};
if (url == nil) {
executorBlock(^(RCTInspectorNetworkListener *listener) {
listener->onError([NSString stringWithFormat:@"Not a valid URL: %@", urlString].UTF8String);
});
return;
}
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
[urlRequest setHTTPMethod:@"GET"];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:urlRequest];
__weak NSURLSessionDataTask *weakDataTask = dataTask;
executorBlock(^(RCTInspectorNetworkListener *listener) {
listener->setCancelFunction([weakDataTask]() { [weakDataTask cancel]; });
});
// Store the executor as a block per task.
self.executorsByTaskId[@(dataTask.taskIdentifier)] = executorBlock;
[dataTask resume];
}
- (void)withListenerForTask:(NSURLSessionTask *)task execute:(ListenerBlock)block
{
void (^executor)(ListenerBlock) = self.executorsByTaskId[@(task.taskIdentifier)];
if (executor != nullptr) {
executor(block);
}
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
auto callbackWithHeadersOrError = (^(RCTInspectorNetworkListener *listener) {
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
std::map<std::string, std::string> headersMap;
for (NSString *key in httpResponse.allHeaderFields) {
headersMap[[key UTF8String]] = [[httpResponse.allHeaderFields objectForKey:key] UTF8String];
}
completionHandler(NSURLSessionResponseAllow);
listener->onHeaders(httpResponse.statusCode, headersMap);
} else {
listener->onError("Unsupported response type");
completionHandler(NSURLSessionResponseCancel);
}
});
[self withListenerForTask:dataTask execute:callbackWithHeadersOrError];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
auto callbackWithData = ^(RCTInspectorNetworkListener *listener) {
listener->onData(std::string_view(static_cast<const char *>(data.bytes), data.length));
};
[self withListenerForTask:dataTask execute:callbackWithData];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
auto callbackWithCompletionOrError = ^(RCTInspectorNetworkListener *listener) {
if (error != nil) {
listener->onError(error.localizedDescription.UTF8String);
} else {
listener->onCompletion();
}
};
[self withListenerForTask:task execute:callbackWithCompletionOrError];
}
@end

View File

@@ -0,0 +1,26 @@
/*
* 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 <Foundation/Foundation.h>
// This is a subset of jsinspector_modern::HostTargetMetadata with ObjC types,
// containing the members implemented by getHostMetadata.
@interface CommonHostMetadata : NSObject
@property (nonatomic, strong) NSString *appDisplayName;
@property (nonatomic, strong) NSString *appIdentifier;
@property (nonatomic, strong) NSString *deviceName;
@property (nonatomic, strong) NSString *platform;
@property (nonatomic, strong) NSString *reactNativeVersion;
@end
@interface RCTInspectorUtils : NSObject
+ (CommonHostMetadata *)getHostMetadata;
@end

View File

@@ -0,0 +1,50 @@
/*
* 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 "RCTInspectorUtils.h"
#import <React/RCTConstants.h>
#import <React/RCTVersion.h>
#import <UIKit/UIKit.h>
@implementation CommonHostMetadata
@end
@implementation RCTInspectorUtils
+ (CommonHostMetadata *)getHostMetadata
{
#if TARGET_OS_IPHONE
UIDevice *device = [UIDevice currentDevice];
NSString *deviceName = [device name];
#else
// macOS does not support UIDevice. Use System Configuration. This API
// returns a nullable value, but is non-blocking (compared with
// `[NSHost currentHost]`) and is ideal since deviceName is optional.
NSString *deviceName = (__bridge NSString *)SCDynamicStoreCopyComputerName(nil, nil);
#endif // TARGET_OS_IPHONE
auto version = RCTGetReactNativeVersion();
CommonHostMetadata *metadata = [[CommonHostMetadata alloc] init];
metadata.appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleNameKey];
metadata.appIdentifier = [[NSBundle mainBundle] bundleIdentifier];
metadata.platform = RCTPlatformName;
metadata.deviceName = deviceName;
metadata.reactNativeVersion = [NSString stringWithFormat:@"%i.%i.%i%@",
[version[@"major"] intValue],
[version[@"minor"] intValue],
[version[@"patch"] intValue],
[version[@"prerelease"] isKindOfClass:[NSNull class]]
? @""
: [@"-" stringByAppendingString:version[@"prerelease"]]];
return metadata;
}
@end

View File

@@ -0,0 +1,45 @@
/*
* 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/RCTDefines.h>
#if RCT_DEV // Only supported in dev mode
@class RCTPackagerClientResponder;
@class RCTReconnectingWebSocket;
#if defined(__cplusplus)
extern "C" {
#endif
extern const int RCT_PACKAGER_CLIENT_PROTOCOL_VERSION;
#if defined(__cplusplus)
}
#endif
@protocol RCTPackagerClientMethod <NSObject>
- (void)handleRequest:(NSDictionary<NSString *, id> *)params withResponder:(RCTPackagerClientResponder *)responder;
- (void)handleNotification:(NSDictionary<NSString *, id> *)params;
@optional
/** By default object will receive its methods on the main queue, unless this method is overridden. */
- (dispatch_queue_t)methodQueue;
@end
@interface RCTPackagerClientResponder : NSObject
- (instancetype)initWithId:(id)msgId socket:(RCTReconnectingWebSocket *)socket;
- (void)respondWithResult:(id)result;
- (void)respondWithError:(id)error;
@end
#endif

View File

@@ -0,0 +1,65 @@
/*
* 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/RCTPackagerClient.h>
#import <React/RCTLog.h>
#import <React/RCTReconnectingWebSocket.h>
#import <React/RCTUtils.h>
#if RCT_DEV // Only supported in dev mode
const int RCT_PACKAGER_CLIENT_PROTOCOL_VERSION = 2;
@implementation RCTPackagerClientResponder {
id _msgId;
__weak RCTReconnectingWebSocket *_socket;
}
- (instancetype)initWithId:(id)msgId socket:(RCTReconnectingWebSocket *)socket
{
if (self = [super init]) {
_msgId = msgId;
_socket = socket;
}
return self;
}
- (void)respondWithResult:(id)result
{
NSDictionary<NSString *, id> *msg = @{
@"version" : @(RCT_PACKAGER_CLIENT_PROTOCOL_VERSION),
@"id" : _msgId,
@"result" : result,
};
NSError *jsError = nil;
NSString *message = RCTJSONStringify(msg, &jsError);
if (jsError) {
RCTLogError(@"%@ failed to stringify message with error %@", [self class], jsError);
} else {
[_socket send:message];
}
}
- (void)respondWithError:(id)error
{
NSDictionary<NSString *, id> *msg = @{
@"version" : @(RCT_PACKAGER_CLIENT_PROTOCOL_VERSION),
@"id" : _msgId,
@"error" : error,
};
NSError *jsError = nil;
NSString *message = RCTJSONStringify(msg, &jsError);
if (jsError) {
RCTLogError(@"%@ failed to stringify message with error %@", [self class], jsError);
} else {
[_socket send:message];
}
}
@end
#endif

View File

@@ -0,0 +1,78 @@
/*
* 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 <Foundation/Foundation.h>
#import <React/RCTDefines.h>
#if RCT_DEV
NS_ASSUME_NONNULL_BEGIN
@protocol RCTPackagerClientMethod;
@class RCTPackagerClientResponder;
typedef uint32_t RCTHandlerToken;
typedef void (^RCTNotificationHandler)(NSDictionary<NSString *, id> *);
typedef void (^RCTRequestHandler)(NSDictionary<NSString *, id> *, RCTPackagerClientResponder *);
typedef void (^RCTConnectedHandler)(void);
/** Encapsulates singleton connection to React Native packager. */
@interface RCTPackagerConnection : NSObject
+ (instancetype)sharedPackagerConnection;
/**
* Registers a handler for a notification broadcast from the packager. An
* example is "reload" - an instruction to reload from the packager.
* If multiple notification handlers are registered for the same method, they
* will all be invoked sequentially.
*/
- (RCTHandlerToken)addNotificationHandler:(RCTNotificationHandler)handler
queue:(dispatch_queue_t)queue
forMethod:(NSString *)method;
/**
* Registers a handler for a request from the packager. An example is
* pokeSamplingProfiler; it asks for profile data from the client.
* Only one handler can be registered for a given method; calling this
* displaces any previous request handler registered for that method.
*/
- (RCTHandlerToken)addRequestHandler:(RCTRequestHandler)handler
queue:(dispatch_queue_t)queue
forMethod:(NSString *)method;
/**
* Registers a handler that runs at most once, when the connection to the
* packager has been established. The handler will be dispatched immediately
* if the connection is already established.
*/
- (RCTHandlerToken)addConnectedHandler:(RCTConnectedHandler)handler queue:(dispatch_queue_t)queue;
/** Removes a handler. Silently does nothing if the token is not valid. */
- (void)removeHandler:(RCTHandlerToken)token;
/** Disconnects and removes all handlers. */
- (void)stop;
/** Reconnect with given packager server, if packagerServerHostPort has changed. */
- (void)reconnect:(NSString *)packagerServerHostPort;
/**
* Historically no distinction was made between notification and request
* handlers. If you use this method, it will be registered as *both* a
* notification handler *and* a request handler. You should migrate to the
* new block-based API instead.
*/
- (void)addHandler:(id<RCTPackagerClientMethod>)handler
forMethod:(NSString *)method __deprecated_msg("Use addRequestHandler or addNotificationHandler instead");
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,333 @@
/*
* 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/RCTPackagerConnection.h>
#import <objc/runtime.h>
#import <algorithm>
#import <vector>
#import <React/RCTAssert.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTConstants.h>
#import <React/RCTConvert.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <React/RCTPackagerClient.h>
#import <React/RCTReconnectingWebSocket.h>
#import <React/RCTReloadCommand.h>
#import <React/RCTUtils.h>
#if RCT_DEV
#import <SocketRocket/SRWebSocket.h>
@interface RCTPackagerConnection () <RCTReconnectingWebSocketDelegate>
@end
template <typename Handler>
struct Registration {
NSString *method;
Handler handler;
dispatch_queue_t queue;
uint32_t token;
};
@implementation RCTPackagerConnection {
std::mutex _mutex; // protects all ivars
RCTReconnectingWebSocket *_socket;
BOOL _socketConnected;
NSString *_serverHostPortForSocket;
NSString *_serverSchemeForSocket;
id _bundleURLChangeObserver;
id _reloadWithPotentiallyNewURLObserver;
uint32_t _nextToken;
std::vector<Registration<RCTNotificationHandler>> _notificationRegistrations;
std::vector<Registration<RCTRequestHandler>> _requestRegistrations;
std::vector<Registration<RCTConnectedHandler>> _connectedRegistrations;
}
+ (instancetype)sharedPackagerConnection
{
static RCTPackagerConnection *connection;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
connection = [RCTPackagerConnection new];
});
return connection;
}
- (instancetype)init
{
if (self = [super init]) {
_nextToken = 1; // Prevent randomly erasing a handler if you pass a bogus 0 token
_serverHostPortForSocket = [[RCTBundleURLProvider sharedSettings] packagerServerHostPort];
_serverSchemeForSocket = [[RCTBundleURLProvider sharedSettings] packagerScheme];
_socket = socketForLocation(_serverHostPortForSocket, _serverSchemeForSocket);
_socket.delegate = self;
[_socket start];
RCTPackagerConnection *const __weak weakSelf = self;
_bundleURLChangeObserver =
[[NSNotificationCenter defaultCenter] addObserverForName:RCTBundleURLProviderUpdatedNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *_Nonnull __unused note) {
[weakSelf bundleURLSettingsChanged];
}];
_reloadWithPotentiallyNewURLObserver =
[[NSNotificationCenter defaultCenter] addObserverForName:RCTTriggerReloadCommandNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *_Nonnull __unused note) {
[weakSelf bundleURLSettingsChanged];
}];
}
return self;
}
static RCTReconnectingWebSocket *socketForLocation(NSString *const serverHostPort, NSString *scheme)
{
NSString *serverHost;
NSString *serverPort;
NSArray *locationComponents = [serverHostPort componentsSeparatedByString:@":"];
if ([locationComponents count] > 0) {
serverHost = locationComponents[0];
}
if ([locationComponents count] > 1) {
serverPort = locationComponents[1];
}
if (![scheme length]) {
scheme = @"http";
}
NSURLComponents *const components = [NSURLComponents new];
components.host = serverHost ?: @"localhost";
components.scheme = scheme;
components.port = serverPort ? @(serverPort.integerValue) : @(kRCTBundleURLProviderDefaultPort);
components.path = @"/message";
components.queryItems = @[ [NSURLQueryItem queryItemWithName:@"role" value:RCTPlatformName] ];
static dispatch_queue_t queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("com.facebook.RCTPackagerConnectionQueue", DISPATCH_QUEUE_SERIAL);
});
return [[RCTReconnectingWebSocket alloc] initWithURL:components.URL queue:queue];
}
- (void)stop
{
std::lock_guard<std::mutex> l(_mutex);
if (_socket == nil) {
// Already stopped
return;
}
[[NSNotificationCenter defaultCenter] removeObserver:_bundleURLChangeObserver];
_bundleURLChangeObserver = nil;
[[NSNotificationCenter defaultCenter] removeObserver:_reloadWithPotentiallyNewURLObserver];
_reloadWithPotentiallyNewURLObserver = nil;
_socketConnected = NO;
[_socket stop];
_socket = nil;
_notificationRegistrations.clear();
_requestRegistrations.clear();
}
- (void)reconnect:(NSString *)packagerServerHostPort
{
std::lock_guard<std::mutex> l(_mutex);
if (_socket == nil) {
return; // already stopped
}
NSString *const serverScheme = [[RCTBundleURLProvider sharedSettings] packagerScheme];
if ([packagerServerHostPort isEqual:_serverHostPortForSocket] && [serverScheme isEqual:_serverSchemeForSocket]) {
return; // unchanged
}
_socket.delegate = nil;
[_socket stop];
_serverHostPortForSocket = packagerServerHostPort;
_serverSchemeForSocket = serverScheme;
_socket = socketForLocation(packagerServerHostPort, serverScheme);
_socket.delegate = self;
[_socket start];
}
- (void)bundleURLSettingsChanged
{
// Will only reconnect if `packagerServerHostPort` has actually changed
[self reconnect:[[RCTBundleURLProvider sharedSettings] packagerServerHostPort]];
}
- (RCTHandlerToken)addNotificationHandler:(RCTNotificationHandler)handler
queue:(dispatch_queue_t)queue
forMethod:(NSString *)method
{
std::lock_guard<std::mutex> l(_mutex);
const auto token = _nextToken++;
_notificationRegistrations.push_back({method, handler, queue, token});
return token;
}
- (RCTHandlerToken)addRequestHandler:(RCTRequestHandler)handler
queue:(dispatch_queue_t)queue
forMethod:(NSString *)method
{
std::lock_guard<std::mutex> l(_mutex);
const auto token = _nextToken++;
_requestRegistrations.push_back({method, handler, queue, token});
return token;
}
- (RCTHandlerToken)addConnectedHandler:(RCTConnectedHandler)handler queue:(dispatch_queue_t)queue
{
std::lock_guard<std::mutex> l(_mutex);
if (_socketConnected) {
dispatch_async(queue, ^{
handler();
});
return 0; // _nextToken starts at 1, so 0 is a no-op token
} else {
const auto token = _nextToken++;
_connectedRegistrations.push_back({nil, handler, queue, token});
return token;
}
}
- (void)removeHandler:(RCTHandlerToken)token
{
std::lock_guard<std::mutex> l(_mutex);
eraseRegistrationsWithToken(_notificationRegistrations, token);
eraseRegistrationsWithToken(_requestRegistrations, token);
eraseRegistrationsWithToken(_connectedRegistrations, token);
}
template <typename Handler>
static void eraseRegistrationsWithToken(std::vector<Registration<Handler>> &registrations, RCTHandlerToken token)
{
registrations.erase(
std::remove_if(
registrations.begin(), registrations.end(), [&token](const auto &reg) { return reg.token == token; }),
registrations.end());
}
- (void)addHandler:(id<RCTPackagerClientMethod>)handler forMethod:(NSString *)method
{
dispatch_queue_t queue =
[handler respondsToSelector:@selector(methodQueue)] ? [handler methodQueue] : dispatch_get_main_queue();
[self
addNotificationHandler:^(NSDictionary<NSString *, id> *notification) {
[handler handleNotification:notification];
}
queue:queue
forMethod:method];
[self
addRequestHandler:^(NSDictionary<NSString *, id> *request, RCTPackagerClientResponder *responder) {
[handler handleRequest:request withResponder:responder];
}
queue:queue
forMethod:method];
}
static BOOL isSupportedVersion(NSNumber *version)
{
NSArray<NSNumber *> *const kSupportedVersions = @[ @(RCT_PACKAGER_CLIENT_PROTOCOL_VERSION) ];
return [kSupportedVersions containsObject:version];
}
#pragma mark - RCTReconnectingWebSocketDelegate
- (void)reconnectingWebSocketDidOpen:(__unused RCTReconnectingWebSocket *)webSocket
{
std::vector<Registration<RCTConnectedHandler>> registrations;
{
std::lock_guard<std::mutex> l(_mutex);
_socketConnected = YES;
registrations = _connectedRegistrations;
_connectedRegistrations.clear();
}
for (const auto &registration : registrations) {
// Beware: don't capture the reference to handler in a dispatched block!
RCTConnectedHandler handler = registration.handler;
dispatch_async(registration.queue, ^{
handler();
});
}
}
- (void)reconnectingWebSocket:(RCTReconnectingWebSocket *)webSocket didReceiveMessage:(id)message
{
NSError *error = nil;
NSDictionary<NSString *, id> *msg = RCTJSONParse(message, &error);
if (error) {
RCTLogError(@"%@ failed to parse message with error %@\n<message>\n%@\n</message>", [self class], error, msg);
return;
}
if (!isSupportedVersion(msg[@"version"])) {
RCTLogError(@"%@ received message with not supported version %@", [self class], msg[@"version"]);
return;
}
NSString *const method = msg[@"method"];
NSDictionary<NSString *, id> *const params = msg[@"params"];
id messageId = msg[@"id"];
if (messageId) { // Request
const std::vector<Registration<RCTRequestHandler>> registrations(
registrationsWithMethod(_mutex, _requestRegistrations, method));
if (registrations.empty()) {
RCTLogError(@"No handler found for packager method %@", msg[@"method"]);
[[[RCTPackagerClientResponder alloc] initWithId:messageId socket:webSocket]
respondWithError:[NSString stringWithFormat:@"No handler found for packager method %@", msg[@"method"]]];
} else {
// If there are multiple matching request registrations, only one can win;
// otherwise the packager would get multiple responses. Choose the last one.
RCTRequestHandler handler = registrations.back().handler;
dispatch_async(registrations.back().queue, ^{
handler(params, [[RCTPackagerClientResponder alloc] initWithId:messageId socket:webSocket]);
});
}
} else { // Notification
const std::vector<Registration<RCTNotificationHandler>> registrations(
registrationsWithMethod(_mutex, _notificationRegistrations, method));
for (const auto &registration : registrations) {
// Beware: don't capture the reference to handler in a dispatched block!
RCTNotificationHandler handler = registration.handler;
dispatch_async(registration.queue, ^{
handler(params);
});
}
}
}
- (void)reconnectingWebSocketDidClose:(__unused RCTReconnectingWebSocket *)webSocket
{
std::lock_guard<std::mutex> l(_mutex);
_socketConnected = NO;
}
template <typename Handler>
static std::vector<Registration<Handler>>
registrationsWithMethod(std::mutex &mutex, const std::vector<Registration<Handler>> &registrations, NSString *method)
{
std::lock_guard<std::mutex> l(mutex); // Scope lock acquisition to prevent deadlock when calling out
std::vector<Registration<Handler>> matches;
for (const auto &reg : registrations) {
if ([reg.method isEqual:method]) {
matches.push_back(reg);
}
}
return matches;
}
@end
#endif

View File

@@ -0,0 +1,15 @@
/*
* 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 <Foundation/Foundation.h>
@interface RCTPausedInDebuggerOverlayController : NSObject
- (void)showWithMessage:(NSString *)message onResume:(void (^)(void))onResume;
- (void)hide;
@end

View File

@@ -0,0 +1,136 @@
/*
* 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/RCTUtils.h>
#include <UIKit/UIKit.h>
#import "RCTPausedInDebuggerOverlayController.h"
@interface RCTPausedInDebuggerViewController : UIViewController
@property (nonatomic, copy) void (^onResume)(void);
@property (nonatomic, strong) NSString *message;
@end
@interface RCTPausedInDebuggerOverlayController ()
@property (nonatomic, strong) UIWindow *alertWindow;
@end
@implementation RCTPausedInDebuggerViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *dimmingView = [[UIView alloc] init];
dimmingView.translatesAutoresizingMaskIntoConstraints = NO;
dimmingView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2];
[self.view addSubview:dimmingView];
[NSLayoutConstraint activateConstraints:@[
[dimmingView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[dimmingView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[dimmingView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[dimmingView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor]
]];
UILabel *messageLabel = [[UILabel alloc] init];
messageLabel.text = self.message;
messageLabel.textAlignment = NSTextAlignmentCenter;
messageLabel.numberOfLines = 0;
messageLabel.font = [UIFont boldSystemFontOfSize:16];
messageLabel.textColor = [UIColor blackColor];
messageLabel.translatesAutoresizingMaskIntoConstraints = NO;
UIView *messageContainer = [[UIView alloc] init];
[messageContainer addSubview:messageLabel];
[NSLayoutConstraint activateConstraints:@[
[messageLabel.topAnchor constraintEqualToAnchor:messageContainer.topAnchor constant:-1],
[messageLabel.bottomAnchor constraintEqualToAnchor:messageContainer.bottomAnchor],
[messageLabel.leadingAnchor constraintEqualToAnchor:messageContainer.leadingAnchor constant:16],
[messageLabel.trailingAnchor constraintEqualToAnchor:messageContainer.trailingAnchor],
]];
UIButton *resumeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *image = [UIImage systemImageNamed:@"forward.frame.fill"];
[resumeButton setImage:image forState:UIControlStateNormal];
[resumeButton setImage:image forState:UIControlStateDisabled];
resumeButton.tintColor = [UIColor colorWithRed:0.37 green:0.37 blue:0.37 alpha:1];
resumeButton.enabled = NO;
[NSLayoutConstraint activateConstraints:@[
[resumeButton.widthAnchor constraintEqualToConstant:48],
[resumeButton.heightAnchor constraintEqualToConstant:46],
]];
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ messageContainer, resumeButton ]];
stackView.backgroundColor = [UIColor colorWithRed:1 green:1 blue:0.757 alpha:1];
stackView.layer.cornerRadius = 12;
stackView.layer.borderWidth = 2;
stackView.layer.borderColor = [UIColor colorWithRed:0.71 green:0.71 blue:0.62 alpha:1].CGColor;
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.distribution = UIStackViewDistributionFill;
stackView.alignment = UIStackViewAlignmentCenter;
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleResume:)];
[stackView addGestureRecognizer:gestureRecognizer];
stackView.userInteractionEnabled = YES;
[self.view addSubview:stackView];
[NSLayoutConstraint activateConstraints:@[
[stackView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:12],
[stackView.centerXAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.centerXAnchor],
]];
stackView.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
}
- (void)handleResume:(UITapGestureRecognizer *)recognizer
{
self.onResume();
}
@end
@implementation RCTPausedInDebuggerOverlayController
- (UIWindow *)alertWindow
{
if (_alertWindow == nil) {
_alertWindow = [[UIWindow alloc] initWithWindowScene:RCTKeyWindow().windowScene];
if (_alertWindow != nullptr) {
_alertWindow.rootViewController = [UIViewController new];
_alertWindow.windowLevel = UIWindowLevelAlert + 1;
}
}
return _alertWindow;
}
- (void)showWithMessage:(NSString *)message onResume:(void (^)(void))onResume
{
[self hide];
RCTPausedInDebuggerViewController *view = [[RCTPausedInDebuggerViewController alloc] init];
view.modalPresentationStyle = UIModalPresentationOverFullScreen;
view.message = message;
view.onResume = onResume;
[self.alertWindow makeKeyAndVisible];
[self.alertWindow.rootViewController presentViewController:view animated:NO completion:nil];
}
- (void)hide
{
[_alertWindow setHidden:YES];
_alertWindow.windowScene = nil;
_alertWindow = nil;
}
@end