Unverified Commit 0b24a5a2 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

Implement mdns for `flutter run` (#40447)

parent 431b82fd
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
import 'dart:async'; import 'dart:async';
import 'package:multicast_dns/multicast_dns.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart'; import '../base/context.dart';
...@@ -21,6 +19,7 @@ import '../fuchsia/fuchsia_device.dart'; ...@@ -21,6 +19,7 @@ import '../fuchsia/fuchsia_device.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/devices.dart'; import '../ios/devices.dart';
import '../ios/simulators.dart'; import '../ios/simulators.dart';
import '../mdns_discovery.dart';
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import '../resident_runner.dart'; import '../resident_runner.dart';
...@@ -207,7 +206,7 @@ class AttachCommand extends FlutterCommand { ...@@ -207,7 +206,7 @@ class AttachCommand extends FlutterCommand {
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback; final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
bool attachLogger = false; bool attachLogger = false;
if (devicePort == null && debugUri == null) { if (devicePort == null && debugUri == null) {
if (device is FuchsiaDevice) { if (device is FuchsiaDevice) {
attachLogger = true; attachLogger = true;
final String module = argResults['module']; final String module = argResults['module'];
...@@ -229,10 +228,11 @@ class AttachCommand extends FlutterCommand { ...@@ -229,10 +228,11 @@ class AttachCommand extends FlutterCommand {
rethrow; rethrow;
} }
} else if ((device is IOSDevice) || (device is IOSSimulator)) { } else if ((device is IOSDevice) || (device is IOSSimulator)) {
final MDnsObservatoryDiscoveryResult result = await MDnsObservatoryDiscovery().query(applicationId: appId); observatoryUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri(
if (result != null) { appId,
observatoryUri = await _buildObservatoryUri(device, hostname, result.port, result.authCode); device,
} usesIpv6,
);
} }
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery. // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) { if (observatoryUri == null) {
...@@ -254,8 +254,13 @@ class AttachCommand extends FlutterCommand { ...@@ -254,8 +254,13 @@ class AttachCommand extends FlutterCommand {
} }
} }
} else { } else {
observatoryUri = await _buildObservatoryUri(device, observatoryUri = await buildObservatoryUri(
debugUri?.host ?? hostname, devicePort ?? debugUri.port, debugUri?.path); device,
debugUri?.host ?? hostname,
devicePort ?? debugUri.port,
observatoryPort,
debugUri?.path,
);
} }
try { try {
final bool useHot = getBuildInfo().isDebug; final bool useHot = getBuildInfo().isDebug;
...@@ -337,22 +342,6 @@ class AttachCommand extends FlutterCommand { ...@@ -337,22 +342,6 @@ class AttachCommand extends FlutterCommand {
} }
Future<void> _validateArguments() async { } Future<void> _validateArguments() async { }
Future<Uri> _buildObservatoryUri(Device device,
String host, int devicePort, [String authCode]) async {
String path = '/';
if (authCode != null) {
path = authCode;
}
// Not having a trailing slash can cause problems in some situations.
// Ensure that there's one present.
if (!path.endsWith('/')) {
path += '/';
}
final int localPort = observatoryPort
?? await device.portForwarder.forward(devicePort);
return Uri(scheme: 'http', host: host, port: localPort, path: path);
}
} }
class HotRunnerFactory { class HotRunnerFactory {
...@@ -383,132 +372,3 @@ class HotRunnerFactory { ...@@ -383,132 +372,3 @@ class HotRunnerFactory {
ipv6: ipv6, ipv6: ipv6,
); );
} }
class MDnsObservatoryDiscoveryResult {
MDnsObservatoryDiscoveryResult(this.port, this.authCode);
final int port;
final String authCode;
}
/// A wrapper around [MDnsClient] to find a Dart observatory instance.
class MDnsObservatoryDiscovery {
/// Creates a new [MDnsObservatoryDiscovery] object.
///
/// The [client] parameter will be defaulted to a new [MDnsClient] if null.
/// The [applicationId] parameter may be null, and can be used to
/// automatically select which application to use if multiple are advertising
/// Dart observatory ports.
MDnsObservatoryDiscovery({MDnsClient mdnsClient})
: client = mdnsClient ?? MDnsClient();
/// The [MDnsClient] used to do a lookup.
final MDnsClient client;
static const String dartObservatoryName = '_dartobservatory._tcp.local';
/// Executes an mDNS query for a Dart Observatory.
///
/// The [applicationId] parameter may be used to specify which application
/// to find. For Android, it refers to the package name; on iOS, it refers to
/// the bundle ID.
///
/// If it is not null, this method will find the port and authentication code
/// of the Dart Observatory for that application. If it cannot find a Dart
/// Observatory matching that application identifier, it will call
/// [throwToolExit].
///
/// If it is null and there are multiple ports available, the user will be
/// prompted with a list of available observatory ports and asked to select
/// one.
///
/// If it is null and there is only one available instance of Observatory,
/// it will return that instance's information regardless of what application
/// the Observatory instance is for.
Future<MDnsObservatoryDiscoveryResult> query({String applicationId}) async {
printStatus('Checking for advertised Dart observatories...');
try {
await client.start();
final List<PtrResourceRecord> pointerRecords = await client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final List<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet()
.toList();
String domainName;
if (applicationId != null) {
for (String name in uniqueDomainNames) {
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = name;
break;
}
}
if (domainName == null) {
throwToolExit('Did not find a observatory port advertised for $applicationId.');
}
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple observatory ports available.');
buffer.writeln('Rerun this command with one of the following passed in as the appId:');
buffer.writeln('');
for (final String uniqueDomainName in uniqueDomainNames) {
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
}
throwToolExit(buffer.toString());
} else {
domainName = pointerRecords[0].domainName;
}
printStatus('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate.
final List<SrvResourceRecord> srv = await client
.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName),
)
.toList();
if (srv.isEmpty) {
return null;
}
if (srv.length > 1) {
printError('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
printStatus('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
?.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String authCode = '';
const String authCodePrefix = 'authCode=';
String raw = txt.first.text;
// TXT has a format of [<length byte>, text], so if the length is 2,
// that means that TXT is empty.
if (raw.length > 2) {
// Remove length byte from raw txt.
raw = raw.substring(1);
if (raw.startsWith(authCodePrefix)) {
authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) {
authCode += '/';
}
}
}
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
} finally {
client.stop();
}
}
}
...@@ -45,6 +45,7 @@ import 'macos/cocoapods_validator.dart'; ...@@ -45,6 +45,7 @@ import 'macos/cocoapods_validator.dart';
import 'macos/macos_workflow.dart'; import 'macos/macos_workflow.dart';
import 'macos/xcode.dart'; import 'macos/xcode.dart';
import 'macos/xcode_validator.dart'; import 'macos/xcode_validator.dart';
import 'mdns_discovery.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
import 'run_hot.dart'; import 'run_hot.dart';
import 'version.dart'; import 'version.dart';
...@@ -101,6 +102,7 @@ Future<T> runInContext<T>( ...@@ -101,6 +102,7 @@ Future<T> runInContext<T>(
LinuxWorkflow: () => const LinuxWorkflow(), LinuxWorkflow: () => const LinuxWorkflow(),
Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(), Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(),
MacOSWorkflow: () => const MacOSWorkflow(), MacOSWorkflow: () => const MacOSWorkflow(),
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(),
OperatingSystemUtils: () => OperatingSystemUtils(), OperatingSystemUtils: () => OperatingSystemUtils(),
ProcessUtils: () => ProcessUtils(), ProcessUtils: () => ProcessUtils(),
SimControl: () => SimControl(), SimControl: () => SimControl(),
......
...@@ -19,6 +19,7 @@ import '../build_info.dart'; ...@@ -19,6 +19,7 @@ import '../build_info.dart';
import '../convert.dart'; import '../convert.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../mdns_discovery.dart';
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
...@@ -345,7 +346,6 @@ class IOSDevice extends Device { ...@@ -345,7 +346,6 @@ class IOSDevice extends Device {
ipv6: ipv6, ipv6: ipv6,
); );
} }
final int installationResult = await IOSDeploy.instance.runApp( final int installationResult = await IOSDeploy.instance.runApp(
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
...@@ -363,16 +363,36 @@ class IOSDevice extends Device { ...@@ -363,16 +363,36 @@ class IOSDevice extends Device {
return LaunchResult.succeeded(); return LaunchResult.succeeded();
} }
Uri localUri;
try { try {
printTrace('Application launched on the device. Waiting for observatory port.'); printTrace('Application launched on the device. Waiting for observatory port.');
final Uri localUri = await observatoryDiscovery.uri; localUri = await MDnsObservatoryDiscovery.instance.getObservatoryUri(
return LaunchResult.succeeded(observatoryUri: localUri); package.id,
this,
ipv6,
debuggingOptions.observatoryPort,
);
if (localUri != null) {
return LaunchResult.succeeded(observatoryUri: localUri);
}
} catch (error) {
printError('Failed to establish a debug connection with $id: $error');
}
// Fallback to manual protocol discovery
printTrace('mDNS lookup failed, attempting fallback to reading device log.');
try {
printTrace('Waiting for observatory port.');
localUri = await observatoryDiscovery.uri;
if (localUri != null) {
return LaunchResult.succeeded(observatoryUri: localUri);
}
} catch (error) { } catch (error) {
printError('Failed to establish a debug connection with $id: $error'); printError('Failed to establish a debug connection with $id: $error');
return LaunchResult.failed();
} finally { } finally {
await observatoryDiscovery?.cancel(); await observatoryDiscovery?.cancel();
} }
return LaunchResult.failed();
} finally { } finally {
installStatus.stop(); installStatus.stop();
} }
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:multicast_dns/multicast_dns.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart';
import 'device.dart';
import 'globals.dart';
/// A wrapper around [MDnsClient] to find a Dart observatory instance.
class MDnsObservatoryDiscovery {
/// Creates a new [MDnsObservatoryDiscovery] object.
///
/// The [client] parameter will be defaulted to a new [MDnsClient] if null.
/// The [applicationId] parameter may be null, and can be used to
/// automatically select which application to use if multiple are advertising
/// Dart observatory ports.
MDnsObservatoryDiscovery({MDnsClient mdnsClient})
: client = mdnsClient ?? MDnsClient();
/// The [MDnsClient] used to do a lookup.
final MDnsClient client;
@visibleForTesting
static const String dartObservatoryName = '_dartobservatory._tcp.local';
static MDnsObservatoryDiscovery get instance => context.get<MDnsObservatoryDiscovery>();
/// Executes an mDNS query for a Dart Observatory.
///
/// The [applicationId] parameter may be used to specify which application
/// to find. For Android, it refers to the package name; on iOS, it refers to
/// the bundle ID.
///
/// If it is not null, this method will find the port and authentication code
/// of the Dart Observatory for that application. If it cannot find a Dart
/// Observatory matching that application identifier, it will call
/// [throwToolExit].
///
/// If it is null and there are multiple ports available, the user will be
/// prompted with a list of available observatory ports and asked to select
/// one.
///
/// If it is null and there is only one available instance of Observatory,
/// it will return that instance's information regardless of what application
/// the Observatory instance is for.
Future<MDnsObservatoryDiscoveryResult> query({String applicationId}) async {
printStatus('Checking for advertised Dart observatories...');
try {
await client.start();
final List<PtrResourceRecord> pointerRecords = await client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final List<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet()
.toList();
String domainName;
if (applicationId != null) {
for (String name in uniqueDomainNames) {
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = name;
break;
}
}
if (domainName == null) {
throwToolExit('Did not find a observatory port advertised for $applicationId.');
}
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple observatory ports available.');
buffer.writeln('Rerun this command with one of the following passed in as the appId:');
buffer.writeln('');
for (final String uniqueDomainName in uniqueDomainNames) {
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
}
throwToolExit(buffer.toString());
} else {
domainName = pointerRecords[0].domainName;
}
printStatus('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate.
final List<SrvResourceRecord> srv = await client
.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName),
)
.toList();
if (srv.isEmpty) {
return null;
}
if (srv.length > 1) {
printError('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
printStatus('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
?.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String authCode = '';
const String authCodePrefix = 'authCode=';
String raw = txt.first.text;
// TXT has a format of [<length byte>, text], so if the length is 2,
// that means that TXT is empty.
if (raw.length > 2) {
// Remove length byte from raw txt.
raw = raw.substring(1);
if (raw.startsWith(authCodePrefix)) {
authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) {
authCode += '/';
}
}
}
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
} finally {
client.stop();
}
}
Future<Uri> getObservatoryUri(String applicationId, Device device, [bool usesIpv6 = false, int observatoryPort]) async {
final MDnsObservatoryDiscoveryResult result = await query(applicationId: applicationId);
Uri observatoryUri;
if (result != null) {
final String host = usesIpv6
? InternetAddress.loopbackIPv6.address
: InternetAddress.loopbackIPv4.address;
observatoryUri = await buildObservatoryUri(device, host, result.port, observatoryPort, result.authCode);
}
return observatoryUri;
}
}
class MDnsObservatoryDiscoveryResult {
MDnsObservatoryDiscoveryResult(this.port, this.authCode);
final int port;
final String authCode;
}
Future<Uri> buildObservatoryUri(Device device,
String host, int devicePort, [int observatoryPort, String authCode]) async {
String path = '/';
if (authCode != null) {
path = authCode;
}
// Not having a trailing slash can cause problems in some situations.
// Ensure that there's one present.
if (!path.endsWith('/')) {
path += '/';
}
final int localPort = observatoryPort
?? await device.portForwarder.forward(devicePort);
return Uri(scheme: 'http', host: host, port: localPort, path: path);
}
...@@ -13,11 +13,12 @@ import 'package:flutter_tools/src/base/terminal.dart'; ...@@ -13,11 +13,12 @@ import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/attach.dart'; import 'package:flutter_tools/src/commands/attach.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:multicast_dns/multicast_dns.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
...@@ -261,7 +262,7 @@ void main() { ...@@ -261,7 +262,7 @@ void main() {
when(portForwarder.unforward(any)) when(portForwarder.unforward(any))
.thenAnswer((_) async => null); .thenAnswer((_) async => null);
when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'))) when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
.thenAnswer((_) async => 0); .thenAnswer((_) async => 0);
when(mockHotRunnerFactory.build( when(mockHotRunnerFactory.build(
any, any,
target: anyNamed('target'), target: anyNamed('target'),
...@@ -466,137 +467,13 @@ void main() { ...@@ -466,137 +467,13 @@ void main() {
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
}); });
}); });
group('mDNS Discovery', () {
final int year3000 = DateTime(3000).millisecondsSinceEpoch;
MDnsClient getMockClient(
List<PtrResourceRecord> ptrRecords,
Map<String, List<SrvResourceRecord>> srvResponse,
) {
final MDnsClient client = MockMDnsClient();
when(client.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName),
)).thenAnswer((_) => Stream<PtrResourceRecord>.fromIterable(ptrRecords));
for (final MapEntry<String, List<SrvResourceRecord>> entry in srvResponse.entries) {
when(client.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(entry.key),
)).thenAnswer((_) => Stream<SrvResourceRecord>.fromIterable(entry.value));
}
return client;
}
testUsingContext('No ports available', () async {
final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query())?.port;
expect(port, isNull);
});
testUsingContext('One port available, no appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query())?.port;
expect(port, 123);
});
testUsingContext('Multiple ports available, without appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
expect(() => portDiscovery.query(), throwsToolExit());
});
testUsingContext('Multiple ports available, with appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port;
expect(port, 321);
});
testUsingContext('Multiple ports available per process, with appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'),
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'),
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
expect(port, 1234);
});
testUsingContext('Query returns null', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
expect(port, isNull);
});
});
} }
class MockMDnsClient extends Mock implements MDnsClient {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
class MockHotRunner extends Mock implements HotRunner {} class MockHotRunner extends Mock implements HotRunner {}
class MockHotRunnerFactory extends Mock implements HotRunnerFactory {} class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
class MockIOSDevice extends Mock implements IOSDevice {}
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
class StreamLogger extends Logger { class StreamLogger extends Logger {
@override @override
......
...@@ -21,6 +21,7 @@ import 'package:flutter_tools/src/ios/devices.dart'; ...@@ -21,6 +21,7 @@ import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart'; import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -39,6 +40,7 @@ class MockDirectory extends Mock implements Directory {} ...@@ -39,6 +40,7 @@ class MockDirectory extends Mock implements Directory {}
class MockFileSystem extends Mock implements FileSystem {} class MockFileSystem extends Mock implements FileSystem {}
class MockIMobileDevice extends Mock implements IMobileDevice {} class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockIOSDeploy extends Mock implements IOSDeploy {} class MockIOSDeploy extends Mock implements IOSDeploy {}
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
class MockXcode extends Mock implements Xcode {} class MockXcode extends Mock implements Xcode {}
class MockFile extends Mock implements File {} class MockFile extends Mock implements File {}
class MockPortForwarder extends Mock implements DevicePortForwarder {} class MockPortForwarder extends Mock implements DevicePortForwarder {}
...@@ -78,6 +80,7 @@ void main() { ...@@ -78,6 +80,7 @@ void main() {
MockFileSystem mockFileSystem; MockFileSystem mockFileSystem;
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
MockDeviceLogReader mockLogReader; MockDeviceLogReader mockLogReader;
MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery;
MockPortForwarder mockPortForwarder; MockPortForwarder mockPortForwarder;
MockIMobileDevice mockIMobileDevice; MockIMobileDevice mockIMobileDevice;
MockIOSDeploy mockIosDeploy; MockIOSDeploy mockIosDeploy;
...@@ -106,6 +109,7 @@ void main() { ...@@ -106,6 +109,7 @@ void main() {
mockCache = MockCache(); mockCache = MockCache();
when(mockCache.dyLdLibEntry).thenReturn(libraryEntry); when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
mockFileSystem = MockFileSystem(); mockFileSystem = MockFileSystem();
mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery();
mockProcessManager = MockProcessManager(); mockProcessManager = MockProcessManager();
mockLogReader = MockDeviceLogReader(); mockLogReader = MockDeviceLogReader();
mockPortForwarder = MockPortForwarder(); mockPortForwarder = MockPortForwarder();
...@@ -161,16 +165,18 @@ void main() { ...@@ -161,16 +165,18 @@ void main() {
Cache.enableLocking(); Cache.enableLocking();
}); });
testUsingContext(' succeeds in debug mode', () async { testUsingContext(' succeeds in debug mode via mDNS', () async {
final IOSDevice device = IOSDevice('123'); final IOSDevice device = IOSDevice('123');
device.portForwarder = mockPortForwarder; device.portForwarder = mockPortForwarder;
device.setLogReader(mockApp, mockLogReader); device.setLogReader(mockApp, mockLogReader);
final Uri uri = Uri(
// Now that the reader is used, start writing messages to it. scheme: 'http',
Timer.run(() { host: '127.0.0.1',
mockLogReader.addLine('Foo'); port: 1234,
mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort'); path: 'observatory',
}); );
when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
.thenAnswer((Invocation invocation) => Future<Uri>.value(uri));
final LaunchResult launchResult = await device.startApp(mockApp, final LaunchResult launchResult = await device.startApp(mockApp,
prebuiltApplication: true, prebuiltApplication: true,
...@@ -184,29 +190,41 @@ void main() { ...@@ -184,29 +190,41 @@ void main() {
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
Cache: () => mockCache, Cache: () => mockCache,
FileSystem: () => mockFileSystem, FileSystem: () => mockFileSystem,
MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
Platform: () => macPlatform, Platform: () => macPlatform,
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,
}); });
testUsingContext(' succeeds in release mode', () async { testUsingContext(' succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async {
final IOSDevice device = IOSDevice('123'); final IOSDevice device = IOSDevice('123');
device.portForwarder = mockPortForwarder;
device.setLogReader(mockApp, mockLogReader);
// Now that the reader is used, start writing messages to it.
Timer.run(() {
mockLogReader.addLine('Foo');
mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
});
when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
.thenAnswer((Invocation invocation) => Future<Uri>.value(null));
final LaunchResult launchResult = await device.startApp(mockApp, final LaunchResult launchResult = await device.startApp(mockApp,
prebuiltApplication: true, prebuiltApplication: true,
debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)), debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)),
platformArgs: <String, dynamic>{}, platformArgs: <String, dynamic>{},
); );
expect(launchResult.started, isTrue); expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isFalse); expect(launchResult.hasObservatory, isTrue);
expect(await device.stopApp(mockApp), isFalse); expect(await device.stopApp(mockApp), isFalse);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
Cache: () => mockCache, Cache: () => mockCache,
FileSystem: () => mockFileSystem, FileSystem: () => mockFileSystem,
MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
Platform: () => macPlatform, Platform: () => macPlatform,
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,
}); });
testUsingContext(' fails in debug mode when Observatory URI is malformed', () async { testUsingContext(' fails in debug mode when mDNS fails and when Observatory URI is malformed', () async {
final IOSDevice device = IOSDevice('123'); final IOSDevice device = IOSDevice('123');
device.portForwarder = mockPortForwarder; device.portForwarder = mockPortForwarder;
device.setLogReader(mockApp, mockLogReader); device.setLogReader(mockApp, mockLogReader);
...@@ -216,6 +234,8 @@ void main() { ...@@ -216,6 +234,8 @@ void main() {
mockLogReader.addLine('Foo'); mockLogReader.addLine('Foo');
mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort'); mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort');
}); });
when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, any))
.thenAnswer((Invocation invocation) => Future<Uri>.value(null));
final LaunchResult launchResult = await device.startApp(mockApp, final LaunchResult launchResult = await device.startApp(mockApp,
prebuiltApplication: true, prebuiltApplication: true,
...@@ -224,6 +244,25 @@ void main() { ...@@ -224,6 +244,25 @@ void main() {
); );
expect(launchResult.started, isFalse); expect(launchResult.started, isFalse);
expect(launchResult.hasObservatory, isFalse); expect(launchResult.hasObservatory, isFalse);
}, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts,
Cache: () => mockCache,
FileSystem: () => mockFileSystem,
MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
Platform: () => macPlatform,
ProcessManager: () => mockProcessManager,
});
testUsingContext(' succeeds in release mode', () async {
final IOSDevice device = IOSDevice('123');
final LaunchResult launchResult = await device.startApp(mockApp,
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)),
platformArgs: <String, dynamic>{},
);
expect(launchResult.started, isTrue);
expect(launchResult.hasObservatory, isFalse);
expect(await device.stopApp(mockApp), isFalse);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
Cache: () => mockCache, Cache: () => mockCache,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
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';
void main() {
group('mDNS Discovery', () {
final int year3000 = DateTime(3000).millisecondsSinceEpoch;
MDnsClient getMockClient(
List<PtrResourceRecord> ptrRecords,
Map<String, List<SrvResourceRecord>> srvResponse,
) {
final MDnsClient client = MockMDnsClient();
when(client.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName),
)).thenAnswer((_) => Stream<PtrResourceRecord>.fromIterable(ptrRecords));
for (final MapEntry<String, List<SrvResourceRecord>> entry in srvResponse.entries) {
when(client.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(entry.key),
)).thenAnswer((_) => Stream<SrvResourceRecord>.fromIterable(entry.value));
}
return client;
}
testUsingContext('No ports available', () async {
final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query())?.port;
expect(port, isNull);
});
testUsingContext('One port available, no appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query())?.port;
expect(port, 123);
});
testUsingContext('Multiple ports available, without appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
expect(() => portDiscovery.query(), throwsToolExit());
});
testUsingContext('Multiple ports available, with appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port;
expect(port, 321);
});
testUsingContext('Multiple ports available per process, with appId', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', year3000, domainName: 'bar'),
PtrResourceRecord('baz', year3000, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'),
SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'),
SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
expect(port, 1234);
});
testUsingContext('Query returns null', () async {
final MDnsClient client = getMockClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
);
final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
expect(port, isNull);
});
});
}
class MockMDnsClient extends Mock implements MDnsClient {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment