mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Add polling module discovery for Fuchsia (#24994)
This commit is contained in:
parent
32041c0c9c
commit
a2fa98ebaf
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue