Skip to content

Commit a2fa98e

Browse files
Add polling module discovery for Fuchsia (flutter#24994)
1 parent 32041c0 commit a2fa98e

File tree

3 files changed

+214
-34
lines changed

3 files changed

+214
-34
lines changed

packages/flutter_tools/lib/src/commands/attach.dart

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'dart:async';
77
import '../base/common.dart';
88
import '../base/file_system.dart';
99
import '../base/io.dart';
10-
import '../base/logger.dart';
1110
import '../base/utils.dart';
1211
import '../cache.dart';
1312
import '../commands/daemon.dart';
@@ -138,34 +137,18 @@ class AttachCommand extends FlutterCommand {
138137
if (module == null) {
139138
throwToolExit('\'--module\' is requried for attaching to a Fuchsia device');
140139
}
141-
usesIpv6 = _isIpv6(device.id);
142-
final List<int> ports = await device.servicePorts();
143-
if (ports.isEmpty) {
144-
throwToolExit('No active service ports on ${device.name}');
145-
}
146-
final List<int> localPorts = <int>[];
147-
for (int port in ports) {
148-
localPorts.add(await device.portForwarder.forward(port));
149-
}
150-
final Status status = logger.startProgress(
151-
'Waiting for a connection from Flutter on ${device.name}...',
152-
expectSlowOperation: true,
153-
);
140+
usesIpv6 = device.ipv6;
141+
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
154142
try {
155-
final int localPort = await device.findIsolatePort(module, localPorts);
156-
if (localPort == null) {
157-
throwToolExit('No active Observatory running module \'$module\' on ${device.name}');
158-
}
159-
observatoryUri = usesIpv6
160-
? Uri.parse('http://[$ipv6Loopback]:$localPort/')
161-
: Uri.parse('http://$ipv4Loopback:$localPort/');
162-
status.stop();
143+
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
144+
observatoryUri = await isolateDiscoveryProtocol.uri;
145+
printStatus('Done.');
163146
} catch (_) {
147+
isolateDiscoveryProtocol?.dispose();
164148
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
165149
for (ForwardedPort port in ports) {
166150
await device.portForwarder.unforward(port);
167151
}
168-
status.cancel();
169152
rethrow;
170153
}
171154
} else {
@@ -241,17 +224,6 @@ class AttachCommand extends FlutterCommand {
241224
}
242225

243226
Future<void> _validateArguments() async {}
244-
245-
bool _isIpv6(String address) {
246-
// Workaround for https://github.com/dart-lang/sdk/issues/29456
247-
final String fragment = address.split('%').first;
248-
try {
249-
Uri.parseIPv6Address(fragment);
250-
return true;
251-
} on FormatException {
252-
return false;
253-
}
254-
}
255227
}
256228

257229
class HotRunnerFactory {

packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
99
import '../application_package.dart';
1010
import '../base/common.dart';
1111
import '../base/io.dart';
12+
import '../base/logger.dart';
1213
import '../base/platform.dart';
1314
import '../base/process.dart';
1415
import '../base/process_manager.dart';
@@ -24,6 +25,11 @@ import 'fuchsia_workflow.dart';
2425
final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
2526
final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;
2627

28+
// Enables testing the fuchsia isolate discovery
29+
Future<VMService> _kDefaultFuchsiaIsolateDiscoveryConnector(Uri uri) {
30+
return VMService.connect(uri);
31+
}
32+
2733
/// Read the log for a particular device.
2834
class _FuchsiaLogReader extends DeviceLogReader {
2935
_FuchsiaLogReader(this._device, [this._app]);
@@ -207,6 +213,17 @@ class FuchsiaDevice extends Device {
207213
@override
208214
bool get supportsScreenshot => false;
209215

216+
bool get ipv6 {
217+
// Workaround for https://github.com/dart-lang/sdk/issues/29456
218+
final String fragment = id.split('%').first;
219+
try {
220+
Uri.parseIPv6Address(fragment);
221+
return true;
222+
} on FormatException {
223+
return false;
224+
}
225+
}
226+
210227
/// List the ports currently running a dart observatory.
211228
Future<List<int>> servicePorts() async {
212229
final String findOutput = await shell('find /hub -name vmservice-port');
@@ -278,6 +295,93 @@ class FuchsiaDevice extends Device {
278295
throwToolExit('No ports found running $isolateName');
279296
return null;
280297
}
298+
299+
FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(String isolateName) => FuchsiaIsolateDiscoveryProtocol(this, isolateName);
300+
}
301+
302+
class FuchsiaIsolateDiscoveryProtocol {
303+
FuchsiaIsolateDiscoveryProtocol(this._device, this._isolateName, [
304+
this._vmServiceConnector = _kDefaultFuchsiaIsolateDiscoveryConnector,
305+
this._pollOnce = false,
306+
]);
307+
308+
static const Duration _pollDuration = Duration(seconds: 10);
309+
final Map<int, VMService> _ports = <int, VMService>{};
310+
final FuchsiaDevice _device;
311+
final String _isolateName;
312+
final Completer<Uri> _foundUri = Completer<Uri>();
313+
final Future<VMService> Function(Uri) _vmServiceConnector;
314+
// whether to only poll once.
315+
final bool _pollOnce;
316+
Timer _pollingTimer;
317+
Status _status;
318+
319+
FutureOr<Uri> get uri {
320+
if (_uri != null) {
321+
return _uri;
322+
}
323+
_status ??= logger.startProgress(
324+
'Waiting for a connection from $_isolateName on ${_device.name}...',
325+
expectSlowOperation: true,
326+
);
327+
_pollingTimer ??= Timer(_pollDuration, _findIsolate);
328+
return _foundUri.future.then((Uri uri) {
329+
_uri = uri;
330+
return uri;
331+
});
332+
}
333+
Uri _uri;
334+
335+
void dispose() {
336+
if (!_foundUri.isCompleted) {
337+
_status?.cancel();
338+
_status = null;
339+
_pollingTimer?.cancel();
340+
_pollingTimer = null;
341+
_foundUri.completeError(Exception('Did not complete'));
342+
}
343+
}
344+
345+
Future<void> _findIsolate() async {
346+
final List<int> ports = await _device.servicePorts();
347+
for (int port in ports) {
348+
VMService service;
349+
if (_ports.containsKey(port)) {
350+
service = _ports[port];
351+
} else {
352+
final int localPort = await _device.portForwarder.forward(port);
353+
try {
354+
final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$localPort');
355+
service = await _vmServiceConnector(uri);
356+
_ports[port] = service;
357+
} on SocketException catch (err) {
358+
printTrace('Failed to connect to $localPort: $err');
359+
continue;
360+
}
361+
}
362+
await service.getVM();
363+
await service.refreshViews();
364+
for (FlutterView flutterView in service.vm.views) {
365+
if (flutterView.uiIsolate == null) {
366+
continue;
367+
}
368+
final Uri address = flutterView.owner.vmService.httpAddress;
369+
if (flutterView.uiIsolate.name.contains(_isolateName)) {
370+
_foundUri.complete(_device.ipv6
371+
? Uri.parse('http://[$_ipv6Loopback]:${address.port}/')
372+
: Uri.parse('http://$_ipv4Loopback:${address.port}/'));
373+
_status.stop();
374+
return;
375+
}
376+
}
377+
}
378+
if (_pollOnce) {
379+
_foundUri.completeError(Exception('Max iterations exceeded'));
380+
_status.stop();
381+
return;
382+
}
383+
_pollingTimer = Timer(_pollDuration, _findIsolate);
384+
}
281385
}
282386

283387
class _FuchsiaPortForwarder extends DevicePortForwarder {

packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import 'dart:async';
66
import 'dart:convert';
77

8+
import 'package:flutter_tools/src/base/logger.dart';
9+
import 'package:flutter_tools/src/vmservice.dart';
810
import 'package:mockito/mockito.dart';
911
import 'package:process/process.dart';
1012

@@ -198,6 +200,65 @@ void main() {
198200
});
199201
});
200202
});
203+
204+
group(FuchsiaIsolateDiscoveryProtocol, () {
205+
Future<Uri> findUri(List<MockFlutterView> views, String expectedIsolateName) {
206+
final MockPortForwarder portForwarder = MockPortForwarder();
207+
final MockVMService vmService = MockVMService();
208+
final MockVM vm = MockVM();
209+
vm.vmService = vmService;
210+
vmService.vm = vm;
211+
vm.views = views;
212+
for (MockFlutterView view in views) {
213+
view.owner = vm;
214+
}
215+
final MockFuchsiaDevice fuchsiaDevice = MockFuchsiaDevice('123', portForwarder, false);
216+
final FuchsiaIsolateDiscoveryProtocol discoveryProtocol = FuchsiaIsolateDiscoveryProtocol(
217+
fuchsiaDevice,
218+
expectedIsolateName,
219+
(Uri uri) async => vmService,
220+
true // only poll once.
221+
);
222+
when(fuchsiaDevice.servicePorts()).thenAnswer((Invocation invocation) async => <int>[1]);
223+
when(portForwarder.forward(1)).thenAnswer((Invocation invocation) async => 2);
224+
when(vmService.getVM()).thenAnswer((Invocation invocation) => Future<void>.value(null));
225+
when(vmService.refreshViews()).thenAnswer((Invocation invocation) => Future<void>.value(null));
226+
when(vmService.httpAddress).thenReturn(Uri.parse('example'));
227+
return discoveryProtocol.uri;
228+
}
229+
testUsingContext('can find flutter view with matching isolate name', () async {
230+
const String expectedIsolateName = 'foobar';
231+
final Uri uri = await findUri(<MockFlutterView>[
232+
MockFlutterView(null), // no ui isolate.
233+
MockFlutterView(MockIsolate('wrong name')), // wrong name.
234+
MockFlutterView(MockIsolate(expectedIsolateName)), // matching name.
235+
], expectedIsolateName);
236+
expect(uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
237+
}, overrides: <Type, Generator>{
238+
Logger: () => StdoutLogger(),
239+
});
240+
241+
testUsingContext('can handle flutter view without matching isolate name', () async {
242+
const String expectedIsolateName = 'foobar';
243+
final Future<Uri> uri = findUri(<MockFlutterView>[
244+
MockFlutterView(null), // no ui isolate.
245+
MockFlutterView(MockIsolate('wrong name')), // wrong name.
246+
], expectedIsolateName);
247+
expect(uri, throwsException);
248+
}, overrides: <Type, Generator>{
249+
Logger: () => StdoutLogger(),
250+
});
251+
252+
testUsingContext('can handle non flutter view', () async {
253+
const String expectedIsolateName = 'foobar';
254+
final Future<Uri> uri = findUri(<MockFlutterView>[
255+
MockFlutterView(null), // no ui isolate.
256+
], expectedIsolateName);
257+
expect(uri, throwsException);
258+
}, overrides: <Type, Generator>{
259+
Logger: () => StdoutLogger(),
260+
});
261+
});
201262
}
202263

203264
class MockProcessManager extends Mock implements ProcessManager {}
@@ -207,3 +268,46 @@ class MockProcessResult extends Mock implements ProcessResult {}
207268
class MockFile extends Mock implements File {}
208269

209270
class MockProcess extends Mock implements Process {}
271+
272+
class MockFuchsiaDevice extends Mock implements FuchsiaDevice {
273+
MockFuchsiaDevice(this.id, this.portForwarder, this.ipv6);
274+
275+
@override
276+
final bool ipv6;
277+
@override
278+
final String id;
279+
@override
280+
final DevicePortForwarder portForwarder;
281+
}
282+
283+
class MockPortForwarder extends Mock implements DevicePortForwarder {}
284+
285+
class MockVMService extends Mock implements VMService {
286+
@override
287+
VM vm;
288+
}
289+
290+
class MockVM extends Mock implements VM {
291+
@override
292+
VMService vmService;
293+
294+
@override
295+
List<FlutterView> views;
296+
}
297+
298+
class MockFlutterView extends Mock implements FlutterView {
299+
MockFlutterView(this.uiIsolate);
300+
301+
@override
302+
final Isolate uiIsolate;
303+
304+
@override
305+
ServiceObjectOwner owner;
306+
}
307+
308+
class MockIsolate extends Mock implements Isolate {
309+
MockIsolate(this.name);
310+
311+
@override
312+
final String name;
313+
}

0 commit comments

Comments
 (0)