Skip to content

Commit

Permalink
Implemented automatic IP detection for iOS
Browse files Browse the repository at this point in the history
Summary:
Implemented automatic IP detection for iOS, based on #6345 and #6362.
As the previous pull requests did, this works by writing the IP address of the host to a file.
Closes #8091

Differential Revision: D3427657

Pulled By: javache

fbshipit-source-id: 3f534c9b32c4d6fb9615fc2e2c3c3aef421454c5
  • Loading branch information
nathanajah authored and Facebook Github Bot 5 committed Jun 13, 2016
1 parent f9e26b3 commit 8c29a52
Show file tree
Hide file tree
Showing 3 changed files with 22 additions and 27 deletions.
13 changes: 11 additions & 2 deletions React/Base/RCTBundleURLProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@
static NSString *const kRCTEnableMinificationKey = @"RCT_enableMinification";

static NSString *const kDefaultPort = @"8081";
static NSString *ipGuess;

@implementation RCTBundleURLProvider

#if RCT_DEV
+ (void)initialize
{
NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
NSString *ip = [NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:nil];
ipGuess = [ip stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
}
#endif

- (NSDictionary *)defaults
{
static NSDictionary *defaults;
Expand Down Expand Up @@ -75,8 +85,7 @@ - (BOOL)isPackagerRunning:(NSString *)host

- (NSString *)guessPackagerHost
{
NSString *host = @"localhost";
//TODO: Implement automatic IP address detection
NSString *host = ipGuess ?: @"localhost";
if ([self isPackagerRunning:host]) {
return host;
}
Expand Down
27 changes: 2 additions & 25 deletions local-cli/generator-ios/templates/app/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import "AppDelegate.h"

#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"

@implementation AppDelegate
Expand All @@ -17,31 +18,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
{
NSURL *jsCodeLocation;

/**
* Loading JavaScript code - uncomment the one you want.
*
* OPTION 1
* Load from development server. Start the server from the repository root:
*
* $ npm start
*
* To run on device, change `localhost` to the IP address of your computer
* (you can get this by typing `ifconfig` into the terminal and selecting the
* `inet` value under `en0:`) and make sure your computer and iOS device are
* on the same Wi-Fi network.
*/

jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];

/**
* OPTION 2
* Load from pre-bundled file on disk. The static bundle is automatically
* generated by the "Bundle React Native code and images" build step when
* running the project on an actual device or running the project on the
* simulator in the "Release" build configuration.
*/

// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"<%= name %>"
Expand Down
9 changes: 9 additions & 0 deletions packager/react-native-xcode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ type $NODE_BINARY >/dev/null 2>&1 || nodejs_not_found
set -x
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH

if [[ "$CONFIGURATION" = "Debug" && "$PLATFORM_NAME" != "iphonesimulator" ]]; then
PLISTBUDDY='/usr/libexec/PlistBuddy'
PLIST=$TARGET_BUILD_DIR/$INFOPLIST_PATH
IP=$(ipconfig getifaddr en0)
$PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:localhost:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" $PLIST
$PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:$IP.xip.io:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" $PLIST
echo "$IP.xip.io" > "$DEST/ip.txt"
fi

$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \
--entry-file index.ios.js \
--platform ios \
Expand Down

20 comments on commit 8c29a52

@benvium
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff, this is a huge improvement.

@zhileichen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, save a ton of time for me

@flexzuu
Copy link

@flexzuu flexzuu commented on 8c29a52 Jul 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to know if this works because i can't get to work reloading from a dev server on my device. In the plist.info i only get an entrance for localhost. I am in the same network. Testing with a fresh init of a project. And is there a way to still set the host url staticly?

Edit: If i use the old way of defining the ip: jsCodeLocation = [NSURL URLWithString:@"http://192.168.178.24:8081/index.ios.bundle?platform=ios&dev=true"];
It does not work. I even added the ip to App Transport Security Settings Exception Domains with NSTemporaryExceptionAllowsInsecureHTTPLoads = true.

PS: I can access the bundle from that url from my phones safari browser.

@nathanajah
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to set the IP statically by using
[[RCTBundleURLProvider sharedSettings] setJsLocation:@"your_ip_here"].

Can you set a breakpoint and see what is the return value of packagerServerRoot in RCTBundleURLProvider.m? If the ip detection works correctly, it should return the ip address.

The reason why IP address exception doesn't work is that numerical IP address doesn't work with ATS exceptions. The workaround that I did to enable that is to use <ip_address>.xip.io instead, which should redirect to the IP address specified.

If you still can't get it to work, can you try enabling NSAllowArbitraryLoads?

@flexzuu
Copy link

@flexzuu flexzuu commented on 8c29a52 Jul 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks fro the quick reply. I am not that farmiliar with ObjectiveC or the XCode Workflow so i maybe messed up your instructions.

For Static IP i tried to just replace jsCodeLocation = [NSURL URLWithString:@"http://192.168.178.24:8081/index.ios.bundle?platform=ios&dev=true"] with [[RCTBundleURLProvider sharedSettings] setJsLocation:@"192.168.178.24"] but it is not working.
I get the _fbBatchedBridge is undefined Error.

With the standard generated AppDelegate.m and adding NSAllowArbitraryLoads it works completely fine. I suppose (and checked via Breakpoint) the ip is picked up correctly but your workarround with the ip is not working for me.

@flexzuu
Copy link

@flexzuu flexzuu commented on 8c29a52 Jul 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As addition i faced some issues today. I wanted to test it again so i started a new project and added only the NSAllowArbitraryLoads but it did not work.
I have really no idea what i did in the other project that it started to work.
The current behavior i have is that the app works on the device but only through a static bundle. Not through a served file. I would like to have some kind of toggle to switch between the two randomly appearing behaviors.

@nathanajah
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I get it.
setJsLocation uses the NSUserDefaults API, which means that unless it's cleared explicitly, the jsLocation will persist throughout different versions of the app inside the device.

So even though you removed the setJsLocation line, the location that has already been set will still be set.
To clear the actual jsLocation and use the IP detection, you can either call
[[RCTBundleURLProvider sharedSettings] setJsLocation:nil] or
[[RCTBundleURLProvider sharedSettings] resetToDefaults].

This might be something that we'll need to change to prevent confusion.

So what happened is that:

  1. Automatic IP detection without NSAllowArbitraryLoads doesn't work
  2. Automatic IP detection with NSAllowArbitraryLoads doesn't work
  3. setJsLocation without NSAllowArbitraryLoads doesn't work
  4. setJsLocation with NSAllowArbitraryLoads works

The reason for 3 and 4 is clear - it's because numerical IP address ATS exceptions doesn't work, so let's focus on 1 and 2. Is your device connected to the internet when you were testing, or was it just a local network?

@flexzuu
Copy link

@flexzuu flexzuu commented on 8c29a52 Jul 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks for the nice overview i undstand now quite good how it should work. My device is connected via mobile network to the internet and via WLAN to the local network, which is connected to the internet via my router.


Are we sure Automatic IP detection is working at all? Because maybe i interfered with the testing while i tried to set the ip via [[RCTBundleURLProvider sharedSettings] setJsLocation:@"192.168.178.24"]. I did not understand that the ip will be remaining after i remove the line so i thought it worked without it, but as we see in the new project it does not work without it.


My current workflow will be:

  1. Set NSAllowArbitraryLoads to true
    If i am developing i use [[RCTBundleURLProvider sharedSettings] setJsLocation:@"192.168.178.24"]to set the ip.
    If i am out in the wild testing and using the app I use [[RCTBundleURLProvider sharedSettings] resetToDefaults] to get back the offline package that does not require a server.

The optimal workflow would be to just set a flag and we use:

  1. a server and autofind the ip
  2. a server and explicitly set the ip
  3. a bundled version of the js stuff so we don't need a server running.

Maybe just use 3 as a fallback that we can choose on the device the last synced bundle and turn of refreshing from a server. Now if the server goes off and you try to refresh on the device there is no way to get back to a working version.

@Bglgithub
Copy link

@Bglgithub Bglgithub commented on 8c29a52 Jul 11, 2016 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ide
Copy link
Contributor

@ide ide commented on 8c29a52 Jul 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just use 3 as a fallback that we can choose on the device the last synced bundle and turn of refreshing from a server.

We shouldn't do this during development -- it's important that you get an error if the bundle failed to load rather than loading a stale copy and confusing the developer.

@flexzuu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't do this during development -- it's important that you get an error if the bundle failed to load rather than loading a stale copy and confusing the developer.

I meant a really transparent way. Like just adding a Button to dev menu that says load on device bundle / load server bundle and you can switch transparently between the modes. And the Fallback i meant was just to add a hint if you are not connected to the server you can use a on device bundle if its intended that the server is not responding.

@nathanajah
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's possible that the device actually used the mobile network instead of the local connection?
Due to do workaround that we did using xip.io (accessing 192.168.178.24.xip.io instead of the IP address directly to circumvent the numeric IP address limitation in ATS), the device does need to connect to the internet to access the IP.

@npomfret
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've run react-native upgrade on my project and now I can't load JS files dynamically, only the offline bundle works, and that's the default now. Also I can't debug remotely. Any tips on how I should debug what's going on?

@nathanajah
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To debug, can you try to set a breakpoint in xcode on jsCodeLocation = [[RCTBundleURLProvider ... and see if jsCodeLocation is set correctly?
How is your networking setup?

Meanwhile you can use a workaround to set static IP:
Add
[[RCTBundleURLProvider sharedSettings] setJsLocation:@"your_ip_here"] before
jsCodeLocation = [[RCTBundleURLProvider ...
and add NSAllowArbitraryLoads in NSAppTransportSecurity in info.plist.

@esthor
Copy link

@esthor esthor commented on 8c29a52 Jul 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting an error when building and running for release: "[error][tid:com.facebook.react.JavaScript] Expected listener to be a function." Any ideas? Thanks!

@EchoLawrence
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nathanajah : thanks, your solution worked greatly.

@strefethen
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note here that ipconfig getifaddr en0 looks for the IP Address of a specific device. In my case it was ethernet which I had unplugged and when the above mentioned problems started for me until I plugged my cable back in. Without the ethernet cable plugged in the IP.txt file was created with only ".xip.io" and was missing the actual IP address so things failed.

That aside, @nathanajah thanks for getting this work done. Much appreciated.

@npomfret
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think the problem is then having more than one working network interface. The node process binds arbitrarily to the 1st one, which isn't necessarily the wifi. The physical device is only connected to wifi. And so when they aren't hitting the same network interface then obviously they can't talk t each other. So you still need to specify the ip address in AppDelegate.m. So...

  #ifdef DEBUG
    [[RCTBundleURLProvider sharedSettings] setJsLocation:@"ip_here"];
  #endif

@mosesoak
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to say thanks and 💯 for getting this in the build finally! We had written our own fastlane command to add a run script phase with our custom ip-setter, it was a huge time drain. This is a great piece of missing functionality.

Out of curiosity where is Android on being able to run on device? I remember that was a big reason why they wouldn't consider merging this before...

@NijaahnandhRV
Copy link

@NijaahnandhRV NijaahnandhRV commented on 8c29a52 Jun 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, My app gets crashed when i run it. Can anyone please help me out with this issue

screen shot 2018-06-07 at 7 18 23 pm

Please sign in to comment.