Skip to content

[flutter_tools] iOS fallback discovery protocol - #49735

Merged
fluttergithubbot merged 15 commits into
flutter:masterfrom
jonahwilliams:ios_Fallback_discovery
Feb 6, 2020
Merged

fluttergithubbot merged 15 commits into
flutter:masterfrom
jonahwilliams:ios_Fallback_discovery

Conversation

@jonahwilliams

Copy link
Copy Markdown
Contributor

Description

A protocol for discovery of a vmservice on an attached iOS device with multiple fallbacks.

On versions of iOS 13 and greater, libimobiledevice can no longer listen to logs directly. The only way to discover an active observatory is through the mDNS protocol. However, there are a number of circumstances where this breaks down, such as when the device is connected to certain wifi networks or with certain hotspot connections enabled.

Another approach to discover a vmservice is to attempt to assign a specific port and then attempt to connect. This may fail if the port is not available. This port value should be either random, or otherwise generated with application specific input. This reduces the chance of accidentally connecting to another running flutter application.

Finally, if neither of the above approaches works, we can still attempt to parse logs.

To improve the overall resilience of the process, this class combines the three discovery strategies. First it assigns a port and attempts to connect. Then if this fails it falls back to mDNS, then finally attempting to scan logs.

Fixes #46724

WIP because I still need to test the port selection logic and wait for the flag/engine to roll. Also I'm not yet sure if the port forwarder will throw, or if I need to ping the observatory

@fluttergithubbot fluttergithubbot added tool Affects the "flutter" command-line tool. See also t: labels. work in progress; do not review labels Jan 29, 2020
try {
// TODO(jonahwilliams): determine if this succeeds even if there is nothing
// listening.
final int hostPort = await _portForwarder.forward(assumedDevicePort, hostPort: hostVmservicePort);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If this still succeeds when there is no vmservice, I'll update it to make an HTTP get to grab version info or something safe

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually this definitely won't throw. Instead I'll need to poll w/ exponential backoff for a while. Need to run some experiments to determine the best numbers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

Adding @jmagman @christopherfujino for initial thoughts

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

Waiting on an engine roll to start working on this again

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

engine roll has landed and this is working locally

@jonahwilliams
jonahwilliams marked this pull request as ready for review February 3, 2020 16:16
@jonahwilliams

Copy link
Copy Markdown
Contributor Author

I spoke too soon, the flag is not yet in flutter/flutter

// Step 2.5: Generate a potential open port using the provided argument,
// or randomly with the package name as a seed.
final int assumedObservatoryPort = debuggingOptions?.deviceVmServicePort
?? math.Random(packageId.hashCode).nextInt(1000) + 6000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably pick a random port in the standard ephemeral port range 49152 to 65535 (2^15+2^14 to 2^16−1)

https://en.wikipedia.org/wiki/Ephemeral_port

final HttpClientResponse response = await request.close();
if (response.statusCode == HttpStatus.ok) {
final String responseBody = await response.transform(utf8.decoder).join('');
if (responseBody.contains('Dart')) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As discussed offline, this is probably a bit fragile, and we should instead connect to the vmservice to check for an isolate with the right name or root library uri.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

// response page containing a specific string. This logic needs to
// be tightened up to ensure we don't accidentally try to connect
// to some arbitrary server.
final HttpClientRequest request = await _httpClient.getUrl(assumedUri);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The body of this loop could be pulled out to a helper function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

attempts += 1;
}
} on Exception {
_logger.printTrace('Failed to connect directly, falling back to mDNS');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be helpful to print the exception object here, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

} on Exception {
_logger.printTrace('Failed to connect directly, falling back to mDNS');
}
UsageEvent('ios-mdns', 'precheck-failure').send();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider creating a new event type in events.dart. It might also be interesting to record the port number that we tried for here in case there's one that fails a lot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I created ios-handshake as an event type. I don't think it would be worth it to record the port, since we're effectively choosing a random number, we'd need a tremendous amount of data to get any insight into the failures.

return result;
}
} on Exception {
_logger.printTrace('Failed to connect with mDNS, falling back to log scanning');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

This requires flutter-team-archive/engine@7e1d144 to roll into the framework to land - tested locally to verify that the fallback works as expected when multiple apps are run with the same specified port

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

That commit is included in #50177

@zanderso zanderso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm w/ nits and question

}
} on Exception {
// No action, we might have failed to connect.
// The error message will be irrelevant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You might record one of the exception objects to print with the failure trace message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

hostVmservicePort: hostVmservicePort,
);
if (result != null) {
UsageEvent('ios-mdns', 'success').send();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It'll make it a little easier to view this data in analytics if first argument for all of the events is the same. So maybe make this:

UsageEvent('ios-handshake', 'mdns-success').send();

and similarly below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are you worried about changing the existing event names?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No. We're the only ones looking at those events in GA, and they'll kind of mean something different now anyway since the handshake procedure is different, so may as well make the naming uniform.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I kept the first set of events as just success and failure, since I wasn't sure what else to prefix them with

.thenAnswer((Invocation invocation) async => 1);
});

testUsingContext('Selects assumed port if VM service connection is successful', () async {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry if I already asked this, but can these be testWithoutContext() ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think you mentioned it and I completely forgot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ahh, no it was the Usage/event classes need to be refactored. There doesn't seem to be a way to reference any of the helpers without going through the context

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, okay. I'll take on the Usage refactor when I'm back in the office.

@zanderso zanderso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

still lgtm

@mehmetf

mehmetf commented Feb 7, 2020

Copy link
Copy Markdown
Contributor

Jonah, do you imagine this will require a change in google3? or is it expected to work as is?

@jamesderlin FYI.

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

I expect this to work as is

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create vm service handshake for iOS without mDNS

7 participants