123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- /*
- * Copyright (c) Facebook, Inc. and its 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
- #import <React/RCTDefines.h>
- #import <React/RCTInspector.h>
- #import <React/RCTLog.h>
- #import <React/RCTSRWebSocket.h>
- #import <React/RCTUtils.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;
- @implementation RCTBundleStatus
- @end
- @interface RCTInspectorPackagerConnection () <RCTSRWebSocketDelegate> {
- NSURL *_url;
- NSMutableDictionary<NSString *, RCTInspectorLocalConnection *> *_inspectorConnections;
- RCTSRWebSocket *_webSocket;
- dispatch_queue_t _jsQueue;
- BOOL _closed;
- BOOL _suppressConnectionErrors;
- RCTBundleStatusProvider _bundleStatusProvider;
- }
- @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];
- _jsQueue = dispatch_queue_create("com.facebook.react.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
- }
- return self;
- }
- - (void)setBundleStatusProvider:(RCTBundleStatusProvider)bundleStatusProvider
- {
- _bundleStatusProvider = bundleStatusProvider;
- }
- - (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];
- RCTBundleStatusProvider statusProvider = _bundleStatusProvider;
- RCTBundleStatus *bundleStatus = statusProvider == nil ? nil : statusProvider();
- for (RCTInspectorPage *page in pages) {
- NSDictionary *jsonPage = @{
- @"id" : [@(page.id) stringValue],
- @"title" : page.title,
- @"app" : [[NSBundle mainBundle] bundleIdentifier],
- @"vm" : page.vm,
- @"isLastBundleDownloadSuccess" : bundleStatus == nil ? [NSNull null]
- : @(bundleStatus.isLastBundleDownloadSuccess),
- @"bundleUpdateTimestamp" : bundleStatus == nil ? [NSNull null]
- : @((long)bundleStatus.bundleUpdateTimestamp * 1000),
- };
- [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 RCTSRWebSocket *)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 RCTSRWebSocket *)webSocket didReceiveMessage:(id)opaqueMessage
- {
- // warn but don't die on unrecognized messages
- if (![opaqueMessage isKindOfClass:[NSString class]]) {
- RCTLogWarn(@"Unrecognized inspector message, object is of type: %@", [opaqueMessage class]);
- return;
- }
- NSString *messageText = opaqueMessage;
- 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 RCTSRWebSocket *)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 it appears the iOS RCTSRWebSocket API doesn't have the same
- // implemented options.
- _webSocket = [[RCTSRWebSocket alloc] initWithURL:_url];
- [_webSocket setDelegateDispatchQueue:_jsQueue];
- _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(_jsQueue, ^{
- 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 send:messageText];
- }
- }
- });
- }
- - (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
|