[flutter_tool] Print a helpful message on some mDNS failures (#47157)

This commit is contained in:
Zachary Anderson 2019-12-16 14:57:29 -08:00 committed by GitHub
parent ba73cfc149
commit a72cca137d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 15 deletions

View file

@ -26,8 +26,22 @@
/// increase the API surface that we have to test in Flutter tools, and the APIs
/// in `dart:io` can sometimes be hard to use in tests.
import 'dart:async';
import 'dart:io' as io show exit, IOSink, Process, ProcessInfo, ProcessSignal,
stderr, stdin, Stdin, StdinException, Stdout, stdout;
import 'dart:io' as io
show
exit,
InternetAddress,
InternetAddressType,
IOSink,
NetworkInterface,
Process,
ProcessInfo,
ProcessSignal,
stderr,
stdin,
Stdin,
StdinException,
Stdout,
stdout;
import 'package:meta/meta.dart';
@ -60,6 +74,7 @@ export 'dart:io'
IOException,
IOSink,
// Link NO! Use `file_system.dart`
// NetworkInterface NO! Use `io.dart`
pid,
// Platform NO! use `platform.dart`
Process,
@ -259,3 +274,66 @@ class _DefaultProcessInfo implements ProcessInfo {
@override
int get maxRss => io.ProcessInfo.maxRss;
}
/// The return type for [listNetworkInterfaces].
class NetworkInterface implements io.NetworkInterface {
NetworkInterface(this._delegate);
final io.NetworkInterface _delegate;
@override
List<io.InternetAddress> get addresses => _delegate.addresses;
@override
int get index => _delegate.index;
@override
String get name => _delegate.name;
@override
String toString() => "NetworkInterface('$name', $addresses)";
}
typedef NetworkInterfaceLister = Future<List<NetworkInterface>> Function({
bool includeLoopback,
bool includeLinkLocal,
io.InternetAddressType type,
});
NetworkInterfaceLister _networkInterfaceListerOverride;
// Tests can set up a non-default network interface lister.
@visibleForTesting
void setNetworkInterfaceLister(NetworkInterfaceLister lister) {
_networkInterfaceListerOverride = lister;
}
@visibleForTesting
void resetNetworkInterfaceLister() {
_networkInterfaceListerOverride = null;
}
/// This calls [NetworkInterface.list] from `dart:io` unless it is overridden by
/// [setNetworkInterfaceLister] for a test. If it is overridden for a test,
/// it should be reset with [resetNetworkInterfaceLister].
Future<List<NetworkInterface>> listNetworkInterfaces({
bool includeLoopback = false,
bool includeLinkLocal = false,
io.InternetAddressType type = io.InternetAddressType.any,
}) async {
if (_networkInterfaceListerOverride != null) {
return _networkInterfaceListerOverride(
includeLoopback: includeLoopback,
includeLinkLocal: includeLinkLocal,
type: type,
);
}
final List<io.NetworkInterface> interfaces = await io.NetworkInterface.list(
includeLoopback: includeLoopback,
includeLinkLocal: includeLinkLocal,
type: type,
);
return interfaces.map(
(io.NetworkInterface interface) => NetworkInterface(interface),
).toList();
}

View file

@ -10,8 +10,10 @@ import 'package:multicast_dns/multicast_dns.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart';
import 'build_info.dart';
import 'device.dart';
import 'globals.dart';
import 'reporting/reporting.dart';
/// A wrapper around [MDnsClient] to find a Dart observatory instance.
class MDnsObservatoryDiscovery {
@ -146,20 +148,71 @@ class MDnsObservatoryDiscovery {
applicationId: applicationId,
deviceVmservicePort: deviceVmservicePort,
);
Uri observatoryUri;
if (result != null) {
final String host = usesIpv6
? InternetAddress.loopbackIPv6.address
: InternetAddress.loopbackIPv4.address;
observatoryUri = await buildObservatoryUri(
device,
host,
result.port,
hostVmservicePort,
result.authCode,
);
if (result == null) {
await _checkForIPv4LinkLocal(device);
return null;
}
final String host = usesIpv6
? InternetAddress.loopbackIPv6.address
: InternetAddress.loopbackIPv4.address;
return await buildObservatoryUri(
device,
host,
result.port,
hostVmservicePort,
result.authCode,
);
}
// If there's not an ipv4 link local address in `NetworkInterfaces.list`,
// then request user interventions with a `printError()` if possible.
Future<void> _checkForIPv4LinkLocal(Device device) async {
printTrace(
'mDNS query failed. Checking for an interface with a ipv4 link local address.'
);
final List<NetworkInterface> interfaces = await listNetworkInterfaces(
includeLinkLocal: true,
type: InternetAddressType.IPv4,
);
if (logger.isVerbose) {
_logInterfaces(interfaces);
}
final bool hasIPv4LinkLocal = interfaces.any(
(NetworkInterface interface) => interface.addresses.any(
(InternetAddress address) => address.isLinkLocal,
),
);
if (hasIPv4LinkLocal) {
printTrace('An interface with an ipv4 link local address was found.');
return;
}
final TargetPlatform targetPlatform = await device.targetPlatform;
switch (targetPlatform) {
case TargetPlatform.ios:
UsageEvent('ios-mdns', 'no-ipv4-link-local').send();
printError(
'The mDNS query for an attached iOS device failed. It may '
'be necessary to disable the "Personal Hotspot" on the device. '
'See https://github.com/flutter/flutter/issues/46698 for details.'
);
break;
default:
printTrace('No interface with an ipv4 link local address was found.');
break;
}
}
void _logInterfaces(List<NetworkInterface> interfaces) {
for (NetworkInterface interface in interfaces) {
if (logger.isVerbose) {
printTrace('Found interface "${interface.name}":');
for (InternetAddress address in interface.addresses) {
final String linkLocal = address.isLinkLocal ? 'link local' : '';
printTrace('\tBound address: "${address.address}" $linkLocal');
}
}
}
return observatoryUri;
}
}

View file

@ -82,6 +82,20 @@ void main() {
test('test_api defines the Declarer in a known place', () {
expect(Zone.current[#test.declarer], isNotNull);
});
test('listNetworkInterfaces() uses overrides', () async {
setNetworkInterfaceLister(
({
bool includeLoopback,
bool includeLinkLocal,
InternetAddressType type,
}) async => <NetworkInterface>[],
);
expect(await listNetworkInterfaces(), isEmpty);
resetNetworkInterfaceLister();
});
}
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}

View file

@ -4,17 +4,33 @@
import 'dart:async';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:mockito/mockito.dart';
import 'package:multicast_dns/multicast_dns.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/mocks.dart';
void main() {
group('mDNS Discovery', () {
final int year3000 = DateTime(3000).millisecondsSinceEpoch;
setUp(() {
setNetworkInterfaceLister(
({
bool includeLoopback,
bool includeLinkLocal,
InternetAddressType type,
}) async => <NetworkInterface>[],
);
});
tearDown(() {
resetNetworkInterfaceLister();
});
MDnsClient getMockClient(
List<PtrResourceRecord> ptrRecords,
Map<String, List<SrvResourceRecord>> srvResponse, {
@ -48,6 +64,18 @@ void main() {
expect(port, isNull);
});
testUsingContext('Prints helpful message when there is no ipv4 link local address.', () async {
final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final Uri uri = await portDiscovery.getObservatoryUri(
'',
MockIOSDevice(),
);
expect(uri, isNull);
expect(testLogger.errorText, contains('Personal Hotspot'));
});
testUsingContext('One port available, no appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[