diff --git a/lib/browser/.eslintrc b/lib/browser/.eslintrc index 3fb64fc9..db3133ab 100644 --- a/lib/browser/.eslintrc +++ b/lib/browser/.eslintrc @@ -13,6 +13,7 @@ "sourceType": "module" }, "rules": { + "no-console": 0, "strict": 0 } } diff --git a/lib/browser/index.js b/lib/browser/index.js index d160af58..12b1325b 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -18,6 +18,7 @@ 'use strict'; +import { NativeModules } from 'react-native'; import { keys, propTypes, objectTypes } from './constants'; import * as lists from './lists'; import * as objects from './objects'; @@ -25,6 +26,7 @@ import * as results from './results'; import * as rpc from './rpc'; import * as util from './util'; +const {debugHosts, debugPort} = NativeModules.Realm; const listenersKey = Symbol(); rpc.registerTypeConverter(objectTypes.LIST, lists.create); @@ -162,5 +164,24 @@ Object.defineProperties(Realm, { }, }); -// The session ID refers to the Realm constructor object in the RPC server. -Realm[keys.id] = rpc.createSession(); +for (let i = 0, len = debugHosts.length; i < len; i++) { + try { + // The session ID refers to the Realm constructor object in the RPC server. + Realm[keys.id] = rpc.createSession(debugHosts[i] + ':' + debugPort); + break; + } catch (e) { + // Only throw exception after all hosts have been tried. + if (i < len - 1) { + continue; + } + + // Log the original exception for debugging purposes. + console.error(e); + + throw new Error( + 'Realm failed to connect to the embedded debug server inside the app. ' + + 'If attempting to use Chrome debugging from a device, ensure the device is ' + + 'reachable on the same network as this machine.' + ); + } +} diff --git a/lib/browser/rpc.js b/lib/browser/rpc.js index de010510..4b800812 100644 --- a/lib/browser/rpc.js +++ b/lib/browser/rpc.js @@ -21,12 +21,11 @@ import * as base64 from './base64'; import { keys, objectTypes } from './constants'; -const DEVICE_HOST = 'localhost:8082'; - const {id: idKey, realm: realmKey} = keys; const typeConverters = {}; let XMLHttpRequest = global.originalXMLHttpRequest || global.XMLHttpRequest; +let sessionHost; let sessionId; // Check if XMLHttpRequest has been overridden, and get the native one if that's the case. @@ -45,8 +44,17 @@ export function registerTypeConverter(type, handler) { typeConverters[type] = handler; } -export function createSession() { - sessionId = sendRequest('create_session'); +export function createSession(host) { + let oldHost = sessionHost; + + try { + sessionHost = host; + sessionId = sendRequest('create_session'); + } catch (e) { + sessionHost = oldHost; + throw e; + } + return sessionId; } @@ -158,11 +166,15 @@ function deserializeDict(realmId, info) { } function sendRequest(command, data) { + if (!sessionHost) { + throw new Error('Must first create RPC session with a valid host'); + } + data = Object.assign({}, data, sessionId ? {sessionId} : null); let body = JSON.stringify(data); let request = new XMLHttpRequest(); - let url = 'http://' + DEVICE_HOST + '/' + command; + let url = 'http://' + sessionHost + '/' + command; request.open('POST', url, false); request.send(body); diff --git a/react-native/RealmReact.mm b/react-native/RealmReact.mm index 7f929c2c..d6d6728c 100644 --- a/react-native/RealmReact.mm +++ b/react-native/RealmReact.mm @@ -24,7 +24,11 @@ #import "shared_realm.hpp" #import +#import #import +#import +#import +#import #if DEBUG #import @@ -32,6 +36,8 @@ #import #import #import "rpc.hpp" + +#define WEB_SERVER_PORT 8082 #endif @interface NSObject () @@ -118,6 +124,23 @@ extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executo return dispatch_get_main_queue(); } +- (NSDictionary *)constantsToExport { +#if DEBUG +#if TARGET_IPHONE_SIMULATOR + NSArray *hosts = @[@"localhost"]; +#else + NSArray *hosts = [self getIPAddresses]; +#endif + + return @{ + @"debugHosts": hosts, + @"debugPort": @(WEB_SERVER_PORT), + }; +#else + return @{}; +#endif +} + - (void)addListenerForEvent:(NSString *)eventName handler:(RealmReactEventHandler)handler { NSMutableOrderedSet *handlers = _eventHandlers[eventName]; if (!handlers) { @@ -138,6 +161,60 @@ RCT_REMAP_METHOD(emit, emitEvent:(NSString *)eventName withObject:(id)object) { } #if DEBUG +- (NSArray *)getIPAddresses { + static const char * const wifiInterface = "en0"; + + struct ifaddrs *ifaddrs; + if (getifaddrs(&ifaddrs)) { + NSLog(@"Failed to get interface addresses: %s", strerror(errno)); + return @[]; + } + + NSMutableArray *ipAddresses = [[NSMutableArray alloc] init]; + char host[INET6_ADDRSTRLEN]; + + for (struct ifaddrs *ifaddr = ifaddrs; ifaddr; ifaddr = ifaddr->ifa_next) { + if ((ifaddr->ifa_flags & IFF_LOOPBACK) || !(ifaddr->ifa_flags & IFF_UP)) { + // Ignore loopbacks and interfaces that aren't up. + continue; + } + + struct sockaddr *addr = ifaddr->ifa_addr; + if (addr->sa_family == AF_INET) { + // Ignore link-local ipv4 addresses. + in_addr_t sin_addr = ((struct sockaddr_in *)addr)->sin_addr.s_addr; + if (IN_LOOPBACK(sin_addr) || IN_LINKLOCAL(sin_addr) || IN_ZERONET(sin_addr)) { + continue; + } + } + else if (addr->sa_family == AF_INET6) { + // Ignore link-local ipv6 addresses. + struct in6_addr *sin6_addr = &((struct sockaddr_in6 *)addr)->sin6_addr; + if (IN6_IS_ADDR_LOOPBACK(sin6_addr) || IN6_IS_ADDR_LINKLOCAL(sin6_addr) || IN6_IS_ADDR_UNSPECIFIED(sin6_addr)) { + continue; + } + } + else { + // Ignore addresses that are not ipv4 or ipv6. + continue; + } + + if (strcmp(ifaddr->ifa_name, wifiInterface)) { + // Ignore non-wifi addresses. + continue; + } + if (int error = getnameinfo(addr, addr->sa_len, host, sizeof(host), NULL, 0, NI_NUMERICHOST)) { + NSLog(@"Couldn't resolve host name for address: %s", gai_strerror(error)); + continue; + } + + [ipAddresses addObject:@(host)]; + } + + freeifaddrs(ifaddrs); + return [ipAddresses copy]; +} + - (void)startRPC { [GCDWebServer setLogLevel:3]; _webServer = [[GCDWebServer alloc] init]; @@ -178,7 +255,7 @@ RCT_REMAP_METHOD(emit, emitEvent:(NSString *)eventName withObject:(id)object) { return response; }]; - [_webServer startWithPort:8082 bonjourName:nil]; + [_webServer startWithPort:WEB_SERVER_PORT bonjourName:nil]; return; } diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java index db1e2557..60c2e292 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -101,8 +101,7 @@ public class RealmAnalytics { } public static boolean shouldExecute() { - return System.getenv("REALM_DISABLE_ANALYTICS") == null - && (isRunningOnGenymotion() || isRunningOnStockEmulator()); + return System.getenv("REALM_DISABLE_ANALYTICS") == null && isRunningOnEmulator(); } private void send() { @@ -152,12 +151,9 @@ public class RealmAnalytics { .replaceAll("%OS_VERSION%", System.getProperty("os.version")); } - private static boolean isRunningOnGenymotion() { - return Build.FINGERPRINT.contains("vbox"); - } - - private static boolean isRunningOnStockEmulator() { - return Build.FINGERPRINT.contains("generic"); + public static boolean isRunningOnEmulator() { + // Check if running in Genymotion or on the stock emulator. + return Build.FINGERPRINT.contains("vbox") || Build.FINGERPRINT.contains("generic"); } /** diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index 07ebfeb8..b304fdb6 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -9,8 +9,15 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.soloader.SoLoader; import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -58,7 +65,18 @@ public class RealmReactModule extends ReactContextBaseJavaModule { if (!isContextInjected()) { startWebServer(); } - return Collections.EMPTY_MAP; + + List hosts; + if (RealmAnalytics.isRunningOnEmulator()) { + hosts = Arrays.asList(new String[]{"localhost"}); + } else { + hosts = getIPAddresses(); + } + + HashMap constants = new HashMap(); + constants.put("debugHosts", hosts); + constants.put("debugPort", DEFAULT_PORT); + return constants; } @Override @@ -67,6 +85,35 @@ public class RealmReactModule extends ReactContextBaseJavaModule { stopWebServer(); } + private List getIPAddresses() { + ArrayList ipAddresses = new ArrayList(); + + try { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + if (networkInterface.isLoopback() || !networkInterface.isUp()) { + continue; + } + + Enumeration addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isAnyLocalAddress()) { + continue; + } + + ipAddresses.add(address.getHostAddress()); + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + + return ipAddresses; + } + private void startWebServer() { setupChromeDebugModeRealmJsContext(); webServer = new AndroidWebServer(DEFAULT_PORT);