Add polling module discovery for Fuchsia (#24994)
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index 63316af..bf42f80 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -7,7 +7,6 @@
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 @@
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 @@
}
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 {
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index ed48b22..c932610 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -9,6 +9,7 @@
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 @@
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 @@
@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 @@
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 {
diff --git a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
index 5152192..87ab363 100644
--- a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
+++ b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
@@ -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 @@
});
});
});
+
+ 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 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;
+}