Add polling module discovery for Fuchsia (#24994)

This commit is contained in:
Jonah Williams 2019-01-09 09:28:31 -08:00 committed by GitHub
parent 32041c0c9c
commit a2fa98ebaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 34 deletions

View file

@ -7,7 +7,6 @@ import 'dart:async';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/utils.dart';
import '../cache.dart';
import '../commands/daemon.dart';
@ -138,34 +137,18 @@ class AttachCommand extends FlutterCommand {
if (module == null) {
throwToolExit('\'--module\' is requried for attaching to a Fuchsia device');
}
usesIpv6 = _isIpv6(device.id);
final List<int> ports = await device.servicePorts();
if (ports.isEmpty) {
throwToolExit('No active service ports on ${device.name}');
}
final List<int> localPorts = <int>[];
for (int port in ports) {
localPorts.add(await device.portForwarder.forward(port));
}
final Status status = logger.startProgress(
'Waiting for a connection from Flutter on ${device.name}...',
expectSlowOperation: true,
);
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
final int localPort = await device.findIsolatePort(module, localPorts);
if (localPort == null) {
throwToolExit('No active Observatory running module \'$module\' on ${device.name}');
}
observatoryUri = usesIpv6
? Uri.parse('http://[$ipv6Loopback]:$localPort/')
: Uri.parse('http://$ipv4Loopback:$localPort/');
status.stop();
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = await isolateDiscoveryProtocol.uri;
printStatus('Done.');
} catch (_) {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
status.cancel();
rethrow;
}
} else {
@ -241,17 +224,6 @@ class AttachCommand extends FlutterCommand {
}
Future<void> _validateArguments() async {}
bool _isIpv6(String address) {
// Workaround for https://github.com/dart-lang/sdk/issues/29456
final String fragment = address.split('%').first;
try {
Uri.parseIPv6Address(fragment);
return true;
} on FormatException {
return false;
}
}
}
class HotRunnerFactory {

View file

@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/process_manager.dart';
@ -24,6 +25,11 @@ import 'fuchsia_workflow.dart';
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;
// Enables testing the fuchsia isolate discovery
Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
return VMService.connect(uri);
}
/// Read the log for a particular device.
class _FuchsiaLogReader extends DeviceLogReader {
_FuchsiaLogReader(this._device, [this._app]);
@ -207,6 +213,17 @@ class FuchsiaDevice extends Device {
@override
bool get supportsScreenshot => false;
bool get ipv6 {
// Workaround for https://github.com/dart-lang/sdk/issues/29456
final String fragment = id.split('%').first;
try {
Uri.parseIPv6Address(fragment);
return true;
} on FormatException {
return false;
}
}
/// List the ports currently running a dart observatory.
Future<List<int>> servicePorts() async {
final String findOutput = await shell('find /hub -name vmservice-port');
@ -278,6 +295,93 @@ class FuchsiaDevice extends Device {
throwToolExit('No ports found running $isolateName');
return null;
}
FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(String isolateName) => FuchsiaIsolateDiscoveryProtocol(this, isolateName);
}
class FuchsiaIsolateDiscoveryProtocol {
FuchsiaIsolateDiscoveryProtocol(this._device, this._isolateName, [
this._vmServiceConnector = _kDefaultFuchsiaIsolateDiscoveryConnector,
this._pollOnce = false,
]);
static const Duration _pollDuration = Duration(seconds: 10);
final Map<int, VMService> _ports = <int, VMService>{};
final FuchsiaDevice _device;
final String _isolateName;
final Completer<Uri> _foundUri = Completer<Uri>();
final Future<VMService> Function(Uri) _vmServiceConnector;
// whether to only poll once.
final bool _pollOnce;
Timer _pollingTimer;
Status _status;
FutureOr<Uri> get uri {
if (_uri != null) {
return _uri;
}
_status ??= logger.startProgress(
'Waiting for a connection from $_isolateName on ${_device.name}...',
expectSlowOperation: true,
);
_pollingTimer ??= Timer(_pollDuration, _findIsolate);
return _foundUri.future.then((Uri uri) {
_uri = uri;
return uri;
});
}
Uri _uri;
void dispose() {
if (!_foundUri.isCompleted) {
_status?.cancel();
_status = null;
_pollingTimer?.cancel();
_pollingTimer = null;
_foundUri.completeError(Exception('Did not complete'));
}
}
Future<void> _findIsolate() async {
final List<int> ports = await _device.servicePorts();
for (int port in ports) {
VMService service;
if (_ports.containsKey(port)) {
service = _ports[port];
} else {
final int localPort = await _device.portForwarder.forward(port);
try {
final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$localPort');
service = await _vmServiceConnector(uri);
_ports[port] = service;
} on SocketException catch (err) {
printTrace('Failed to connect to $localPort: $err');
continue;
}
}
await service.getVM();
await service.refreshViews();
for (FlutterView flutterView in service.vm.views) {
if (flutterView.uiIsolate == null) {
continue;
}
final Uri address = flutterView.owner.vmService.httpAddress;
if (flutterView.uiIsolate.name.contains(_isolateName)) {
_foundUri.complete(_device.ipv6
? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
: Uri.parse('http://$_ipv4Loopback:${address.port}/'));
_status.stop();
return;
}
}
}
if (_pollOnce) {
_foundUri.completeError(Exception('Max iterations exceeded'));
_status.stop();
return;
}
_pollingTimer = Timer(_pollDuration, _findIsolate);
}
}
class _FuchsiaPortForwarder extends DevicePortForwarder {

View file

@ -5,6 +5,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
@ -198,6 +200,65 @@ void main() {
});
});
});
group(FuchsiaIsolateDiscoveryProtocol, () {
Future<Uri> findUri(List<MockFlutterView> views, String expectedIsolateName) {
final MockPortForwarder portForwarder = MockPortForwarder();
final MockVMService vmService = MockVMService();
final MockVM vm = MockVM();
vm.vmService = vmService;
vmService.vm = vm;
vm.views = views;
for (MockFlutterView view in views) {
view.owner = vm;
}
final MockFuchsiaDevice fuchsiaDevice = MockFuchsiaDevice('123', portForwarder, false);
final FuchsiaIsolateDiscoveryProtocol discoveryProtocol = FuchsiaIsolateDiscoveryProtocol(
fuchsiaDevice,
expectedIsolateName,
(Uri uri) async => vmService,
true // only poll once.
);
when(fuchsiaDevice.servicePorts()).thenAnswer((Invocation invocation) async => <int>[1]);
when(portForwarder.forward(1)).thenAnswer((Invocation invocation) async => 2);
when(vmService.getVM()).thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.refreshViews()).thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.httpAddress).thenReturn(Uri.parse('example'));
return discoveryProtocol.uri;
}
testUsingContext('can find flutter view with matching isolate name', () async {
const String expectedIsolateName = 'foobar';
final Uri uri = await findUri(<MockFlutterView>[
MockFlutterView(null), // no ui isolate.
MockFlutterView(MockIsolate('wrong name')), // wrong name.
MockFlutterView(MockIsolate(expectedIsolateName)), // matching name.
], expectedIsolateName);
expect(uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
});
testUsingContext('can handle flutter view without matching isolate name', () async {
const String expectedIsolateName = 'foobar';
final Future<Uri> uri = findUri(<MockFlutterView>[
MockFlutterView(null), // no ui isolate.
MockFlutterView(MockIsolate('wrong name')), // wrong name.
], expectedIsolateName);
expect(uri, throwsException);
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
});
testUsingContext('can handle non flutter view', () async {
const String expectedIsolateName = 'foobar';
final Future<Uri> uri = findUri(<MockFlutterView>[
MockFlutterView(null), // no ui isolate.
], expectedIsolateName);
expect(uri, throwsException);
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
});
});
}
class MockProcessManager extends Mock implements ProcessManager {}
@ -207,3 +268,46 @@ class MockProcessResult extends Mock implements ProcessResult {}
class MockFile extends Mock implements File {}
class MockProcess extends Mock implements Process {}
class MockFuchsiaDevice extends Mock implements FuchsiaDevice {
MockFuchsiaDevice(this.id, this.portForwarder, this.ipv6);
@override
final bool ipv6;
@override
final String id;
@override
final DevicePortForwarder portForwarder;
}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
class MockVMService extends Mock implements VMService {
@override
VM vm;
}
class MockVM extends Mock implements VM {
@override
VMService vmService;
@override
List<FlutterView> views;
}
class MockFlutterView extends Mock implements FlutterView {
MockFlutterView(this.uiIsolate);
@override
final Isolate uiIsolate;
@override
ServiceObjectOwner owner;
}
class MockIsolate extends Mock implements Isolate {
MockIsolate(this.name);
@override
final String name;
}