RCTInspectorPackagerConnection.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. /*
  2. * Copyright (c) Facebook, Inc. and its 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. #import <React/RCTInspectorPackagerConnection.h>
  8. #if RCT_DEV
  9. #import <React/RCTDefines.h>
  10. #import <React/RCTInspector.h>
  11. #import <React/RCTLog.h>
  12. #import <React/RCTSRWebSocket.h>
  13. #import <React/RCTUtils.h>
  14. // This is a port of the Android impl, at
  15. // ReactAndroid/src/main/java/com/facebook/react/devsupport/InspectorPackagerConnection.java
  16. // please keep consistent :)
  17. const int RECONNECT_DELAY_MS = 2000;
  18. @implementation RCTBundleStatus
  19. @end
  20. @interface RCTInspectorPackagerConnection () <RCTSRWebSocketDelegate> {
  21. NSURL *_url;
  22. NSMutableDictionary<NSString *, RCTInspectorLocalConnection *> *_inspectorConnections;
  23. RCTSRWebSocket *_webSocket;
  24. dispatch_queue_t _jsQueue;
  25. BOOL _closed;
  26. BOOL _suppressConnectionErrors;
  27. RCTBundleStatusProvider _bundleStatusProvider;
  28. }
  29. @end
  30. @interface RCTInspectorRemoteConnection () {
  31. __weak RCTInspectorPackagerConnection *_owningPackagerConnection;
  32. NSString *_pageId;
  33. }
  34. - (instancetype)initWithPackagerConnection:(RCTInspectorPackagerConnection *)owningPackagerConnection
  35. pageId:(NSString *)pageId;
  36. @end
  37. static NSDictionary<NSString *, id> *makePageIdPayload(NSString *pageId)
  38. {
  39. return @{@"pageId" : pageId};
  40. }
  41. @implementation RCTInspectorPackagerConnection
  42. RCT_NOT_IMPLEMENTED(-(instancetype)init)
  43. - (instancetype)initWithURL:(NSURL *)url
  44. {
  45. if (self = [super init]) {
  46. _url = url;
  47. _inspectorConnections = [NSMutableDictionary new];
  48. _jsQueue = dispatch_queue_create("com.facebook.react.WebSocketExecutor", DISPATCH_QUEUE_SERIAL);
  49. }
  50. return self;
  51. }
  52. - (void)setBundleStatusProvider:(RCTBundleStatusProvider)bundleStatusProvider
  53. {
  54. _bundleStatusProvider = bundleStatusProvider;
  55. }
  56. - (void)handleProxyMessage:(NSDictionary<NSString *, id> *)message
  57. {
  58. NSString *event = message[@"event"];
  59. NSDictionary *payload = message[@"payload"];
  60. if ([@"getPages" isEqualToString:event]) {
  61. [self sendEvent:event payload:[self pages]];
  62. } else if ([@"wrappedEvent" isEqualToString:event]) {
  63. [self handleWrappedEvent:payload];
  64. } else if ([@"connect" isEqualToString:event]) {
  65. [self handleConnect:payload];
  66. } else if ([@"disconnect" isEqualToString:event]) {
  67. [self handleDisconnect:payload];
  68. } else {
  69. RCTLogError(@"Unknown event: %@", event);
  70. }
  71. }
  72. - (void)sendEventToAllConnections:(NSString *)event
  73. {
  74. for (NSString *pageId in _inspectorConnections) {
  75. [_inspectorConnections[pageId] sendMessage:event];
  76. }
  77. }
  78. - (void)closeAllConnections
  79. {
  80. for (NSString *pageId in _inspectorConnections) {
  81. [[_inspectorConnections objectForKey:pageId] disconnect];
  82. }
  83. [_inspectorConnections removeAllObjects];
  84. }
  85. - (void)handleConnect:(NSDictionary *)payload
  86. {
  87. NSString *pageId = payload[@"pageId"];
  88. RCTInspectorLocalConnection *existingConnection = _inspectorConnections[pageId];
  89. if (existingConnection) {
  90. [_inspectorConnections removeObjectForKey:pageId];
  91. [existingConnection disconnect];
  92. RCTLogWarn(@"Already connected: %@", pageId);
  93. return;
  94. }
  95. RCTInspectorRemoteConnection *remoteConnection =
  96. [[RCTInspectorRemoteConnection alloc] initWithPackagerConnection:self pageId:pageId];
  97. RCTInspectorLocalConnection *inspectorConnection = [RCTInspector connectPage:[pageId integerValue]
  98. forRemoteConnection:remoteConnection];
  99. _inspectorConnections[pageId] = inspectorConnection;
  100. }
  101. - (void)handleDisconnect:(NSDictionary *)payload
  102. {
  103. NSString *pageId = payload[@"pageId"];
  104. RCTInspectorLocalConnection *inspectorConnection = _inspectorConnections[pageId];
  105. if (inspectorConnection) {
  106. [self removeConnectionForPage:pageId];
  107. [inspectorConnection disconnect];
  108. }
  109. }
  110. - (void)removeConnectionForPage:(NSString *)pageId
  111. {
  112. [_inspectorConnections removeObjectForKey:pageId];
  113. }
  114. - (void)handleWrappedEvent:(NSDictionary *)payload
  115. {
  116. NSString *pageId = payload[@"pageId"];
  117. NSString *wrappedEvent = payload[@"wrappedEvent"];
  118. RCTInspectorLocalConnection *inspectorConnection = _inspectorConnections[pageId];
  119. if (!inspectorConnection) {
  120. RCTLogWarn(@"Not connected to page: %@ , failed trying to handle event: %@", pageId, wrappedEvent);
  121. return;
  122. }
  123. [inspectorConnection sendMessage:wrappedEvent];
  124. }
  125. - (NSArray *)pages
  126. {
  127. NSArray<RCTInspectorPage *> *pages = [RCTInspector pages];
  128. NSMutableArray *array = [NSMutableArray arrayWithCapacity:pages.count];
  129. RCTBundleStatusProvider statusProvider = _bundleStatusProvider;
  130. RCTBundleStatus *bundleStatus = statusProvider == nil ? nil : statusProvider();
  131. for (RCTInspectorPage *page in pages) {
  132. NSDictionary *jsonPage = @{
  133. @"id" : [@(page.id) stringValue],
  134. @"title" : page.title,
  135. @"app" : [[NSBundle mainBundle] bundleIdentifier],
  136. @"vm" : page.vm,
  137. @"isLastBundleDownloadSuccess" : bundleStatus == nil ? [NSNull null]
  138. : @(bundleStatus.isLastBundleDownloadSuccess),
  139. @"bundleUpdateTimestamp" : bundleStatus == nil ? [NSNull null]
  140. : @((long)bundleStatus.bundleUpdateTimestamp * 1000),
  141. };
  142. [array addObject:jsonPage];
  143. }
  144. return array;
  145. }
  146. - (void)sendWrappedEvent:(NSString *)pageId message:(NSString *)message
  147. {
  148. NSDictionary *payload = @{
  149. @"pageId" : pageId,
  150. @"wrappedEvent" : message,
  151. };
  152. [self sendEvent:@"wrappedEvent" payload:payload];
  153. }
  154. - (void)sendEvent:(NSString *)name payload:(id)payload
  155. {
  156. NSDictionary *jsonMessage = @{
  157. @"event" : name,
  158. @"payload" : payload,
  159. };
  160. [self sendToPackager:jsonMessage];
  161. }
  162. // analogous to InspectorPackagerConnection.Connection.onFailure(...)
  163. - (void)webSocket:(__unused RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
  164. {
  165. if (_webSocket) {
  166. [self abort:@"Websocket exception" withCause:error];
  167. }
  168. if (!_closed && [error code] != ECONNREFUSED) {
  169. [self reconnect];
  170. }
  171. }
  172. // analogous to InspectorPackagerConnection.Connection.onMessage(...)
  173. - (void)webSocket:(__unused RCTSRWebSocket *)webSocket didReceiveMessage:(id)opaqueMessage
  174. {
  175. // warn but don't die on unrecognized messages
  176. if (![opaqueMessage isKindOfClass:[NSString class]]) {
  177. RCTLogWarn(@"Unrecognized inspector message, object is of type: %@", [opaqueMessage class]);
  178. return;
  179. }
  180. NSString *messageText = opaqueMessage;
  181. NSError *error = nil;
  182. id parsedJSON = RCTJSONParse(messageText, &error);
  183. if (error) {
  184. RCTLogWarn(@"Unrecognized inspector message, string was not valid JSON: %@", messageText);
  185. return;
  186. }
  187. [self handleProxyMessage:parsedJSON];
  188. }
  189. // analogous to InspectorPackagerConnection.Connection.onClosed(...)
  190. - (void)webSocket:(__unused RCTSRWebSocket *)webSocket
  191. didCloseWithCode:(__unused NSInteger)code
  192. reason:(__unused NSString *)reason
  193. wasClean:(__unused BOOL)wasClean
  194. {
  195. _webSocket = nil;
  196. [self closeAllConnections];
  197. if (!_closed) {
  198. [self reconnect];
  199. }
  200. }
  201. - (bool)isConnected
  202. {
  203. return _webSocket != nil;
  204. }
  205. - (void)connect
  206. {
  207. if (_closed) {
  208. RCTLogError(@"Illegal state: Can't connect after having previously been closed.");
  209. return;
  210. }
  211. // The corresponding android code has a lot of custom config options for
  212. // timeouts, but it appears the iOS RCTSRWebSocket API doesn't have the same
  213. // implemented options.
  214. _webSocket = [[RCTSRWebSocket alloc] initWithURL:_url];
  215. [_webSocket setDelegateDispatchQueue:_jsQueue];
  216. _webSocket.delegate = self;
  217. [_webSocket open];
  218. }
  219. - (void)reconnect
  220. {
  221. if (_closed) {
  222. RCTLogError(@"Illegal state: Can't reconnect after having previously been closed.");
  223. return;
  224. }
  225. if (_suppressConnectionErrors) {
  226. RCTLogWarn(@"Couldn't connect to packager, will silently retry");
  227. _suppressConnectionErrors = true;
  228. }
  229. __weak RCTInspectorPackagerConnection *weakSelf = self;
  230. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, RECONNECT_DELAY_MS * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
  231. RCTInspectorPackagerConnection *strongSelf = weakSelf;
  232. if (strongSelf && !strongSelf->_closed) {
  233. [strongSelf connect];
  234. }
  235. });
  236. }
  237. - (void)closeQuietly
  238. {
  239. _closed = true;
  240. [self disposeWebSocket];
  241. }
  242. - (void)sendToPackager:(NSDictionary *)messageObject
  243. {
  244. __weak RCTInspectorPackagerConnection *weakSelf = self;
  245. dispatch_async(_jsQueue, ^{
  246. RCTInspectorPackagerConnection *strongSelf = weakSelf;
  247. if (strongSelf && !strongSelf->_closed) {
  248. NSError *error;
  249. NSString *messageText = RCTJSONStringify(messageObject, &error);
  250. if (error) {
  251. RCTLogWarn(@"Couldn't send event to packager: %@", error);
  252. } else {
  253. [strongSelf->_webSocket send:messageText];
  254. }
  255. }
  256. });
  257. }
  258. - (void)abort:(NSString *)message withCause:(NSError *)cause
  259. {
  260. // Don't log ECONNREFUSED at all; it's expected in cases where the server isn't listening.
  261. if (![cause.domain isEqual:NSPOSIXErrorDomain] || cause.code != ECONNREFUSED) {
  262. RCTLogInfo(@"Error occurred, shutting down websocket connection: %@ %@", message, cause);
  263. }
  264. [self closeAllConnections];
  265. [self disposeWebSocket];
  266. }
  267. - (void)disposeWebSocket
  268. {
  269. if (_webSocket) {
  270. [_webSocket closeWithCode:1000 reason:@"End of session"];
  271. _webSocket.delegate = nil;
  272. _webSocket = nil;
  273. }
  274. }
  275. @end
  276. @implementation RCTInspectorRemoteConnection
  277. RCT_NOT_IMPLEMENTED(-(instancetype)init)
  278. - (instancetype)initWithPackagerConnection:(RCTInspectorPackagerConnection *)owningPackagerConnection
  279. pageId:(NSString *)pageId
  280. {
  281. if (self = [super init]) {
  282. _owningPackagerConnection = owningPackagerConnection;
  283. _pageId = pageId;
  284. }
  285. return self;
  286. }
  287. - (void)onMessage:(NSString *)message
  288. {
  289. [_owningPackagerConnection sendWrappedEvent:_pageId message:message];
  290. }
  291. - (void)onDisconnect
  292. {
  293. RCTInspectorPackagerConnection *owningPackagerConnectionStrong = _owningPackagerConnection;
  294. if (owningPackagerConnectionStrong) {
  295. [owningPackagerConnectionStrong removeConnectionForPage:_pageId];
  296. [owningPackagerConnectionStrong sendEvent:@"disconnect" payload:makePageIdPayload(_pageId)];
  297. }
  298. }
  299. @end
  300. #endif