[flutter_tools] Generalize waitForExtension (#77220)

This commit is contained in:
Jia Hao 2021-03-11 07:31:25 +08:00 committed by GitHub
parent 385edc33ed
commit 9fdda01252
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 414 additions and 191 deletions

View file

@ -7,7 +7,6 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'base/logger.dart';
import 'resident_runner.dart';
@ -66,12 +65,12 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
// report their URLs yet. Do so now.
_residentRunner.printDebuggerList(includeObservatory: false);
}
await _waitForExtensions(flutterDevices);
final List<FlutterDevice> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
await _maybeCallDevToolsUriServiceExtension(
flutterDevices,
devicesWithExtension,
);
await _callConnectedVmServiceUriExtension(
flutterDevices,
devicesWithExtension,
);
}
@ -107,12 +106,28 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
}
}
Future<void> _waitForExtensions(List<FlutterDevice> flutterDevices) async {
await Future.wait(<Future<void>>[
Future<List<FlutterDevice>> _devicesWithExtensions(List<FlutterDevice> flutterDevices) async {
final List<FlutterDevice> devices = await Future.wait(<Future<FlutterDevice>>[
for (final FlutterDevice device in flutterDevices)
if (device.vmService != null)
waitForExtension(device.vmService.service, 'ext.flutter.connectedVmServiceUri'),
_waitForExtensionsForDevice(device)
]);
return devices.where((FlutterDevice device) => device != null).toList();
}
/// Returns null if the service extension cannot be found on the device.
Future<FlutterDevice> _waitForExtensionsForDevice(FlutterDevice flutterDevice) async {
const String extension = 'ext.flutter.connectedVmServiceUri';
try {
await flutterDevice.vmService?.findExtensionIsolate(extension);
return flutterDevice;
} on VmServiceDisappearedException {
_logger.printTrace(
'The VM Service for ${flutterDevice.device} disappeared while trying to'
' find the $extension service extension. Skipping subsequent DevTools '
'setup for this device.',
);
return null;
}
}
Future<void> _callConnectedVmServiceUriExtension(List<FlutterDevice> flutterDevices) async {
@ -164,10 +179,10 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
@override
Future<void> hotRestart(List<FlutterDevice> flutterDevices) async {
await _waitForExtensions(flutterDevices);
final List<FlutterDevice> devicesWithExtension = await _devicesWithExtensions(flutterDevices);
await Future.wait(<Future<void>>[
_maybeCallDevToolsUriServiceExtension(flutterDevices),
_callConnectedVmServiceUriExtension(flutterDevices),
_maybeCallDevToolsUriServiceExtension(devicesWithExtension),
_callConnectedVmServiceUriExtension(devicesWithExtension),
]);
}
@ -181,37 +196,6 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
}
}
@visibleForTesting
Future<void> waitForExtension(vm_service.VmService vmService, String extension) async {
final Completer<void> completer = Completer<void>();
try {
await vmService.streamListen(vm_service.EventStreams.kExtension);
} on Exception {
// do nothing
}
StreamSubscription<vm_service.Event> extensionStream;
extensionStream = vmService.onExtensionEvent.listen((vm_service.Event event) {
if (event.json['extensionKind'] == 'Flutter.FrameworkInitialization') {
// The 'Flutter.FrameworkInitialization' event is sent on hot restart
// as well, so make sure we don't try to complete this twice.
if (!completer.isCompleted) {
completer.complete();
extensionStream.cancel();
}
}
});
final vm_service.VM vm = await vmService.getVM();
if (vm.isolates.isNotEmpty) {
final vm_service.IsolateRef isolateRef = vm.isolates.first;
final vm_service.Isolate isolate = await vmService.getIsolate(isolateRef.id);
if (isolate.extensionRPCs.contains(extension)) {
return;
}
}
await completer.future;
}
@visibleForTesting
NoOpDevtoolsHandler createNoOpHandler(DevtoolsLauncher launcher, ResidentRunner runner, Logger logger) {
return NoOpDevtoolsHandler();

View file

@ -4,6 +4,8 @@
// @dart = 2.8
import 'dart:async';
import 'package:file/file.dart';
import 'package:meta/meta.dart' show required;
import 'package:vm_service/vm_service.dart' as vm_service;
@ -479,7 +481,7 @@ class FlutterVmService {
@required Uri assetsDirectory,
}) async {
try {
await service.streamListen('Isolate');
await service.streamListen(vm_service.EventStreams.kIsolate);
} on vm_service.RPCError {
// Do nothing, since the tool is already subscribed.
}
@ -784,6 +786,58 @@ class FlutterVmService {
}
}
/// Waits for a signal from the VM service that [extensionName] is registered.
///
/// Looks at the list of loaded extensions for first Flutter view, as well as
/// the stream of added extensions to avoid races.
///
/// Throws a [VmServiceDisappearedException] should the VM Service disappear
/// while making calls to it.
Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
try {
await service.streamListen(vm_service.EventStreams.kIsolate);
} on vm_service.RPCError {
// Do nothing, since the tool is already subscribed.
}
final Completer<vm_service.IsolateRef> extensionAdded = Completer<vm_service.IsolateRef>();
StreamSubscription<vm_service.Event> isolateEvents;
isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
if (event.kind == vm_service.EventKind.kServiceExtensionAdded
&& event.extensionRPC == extensionName) {
isolateEvents.cancel();
extensionAdded.complete(event.isolate);
}
});
try {
final List<FlutterView> flutterViews = await getFlutterViews();
if (flutterViews.isEmpty) {
throw VmServiceDisappearedException();
}
for (final FlutterView flutterView in flutterViews) {
final vm_service.IsolateRef isolateRef = flutterView.uiIsolate;
if (isolateRef == null) {
continue;
}
final vm_service.Isolate isolate = await service.getIsolate(isolateRef.id);
if (isolate.extensionRPCs.contains(extensionName)) {
return isolateRef;
}
}
return await extensionAdded.future;
} finally {
await isolateEvents.cancel();
try {
await service.streamCancel(vm_service.EventStreams.kIsolate);
} on vm_service.RPCError {
// It's ok for cleanup to fail, such as when the service disappears.
}
}
}
/// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
/// been collected.
Future<vm_service.Isolate> getIsolateOrNull(String isolateId) {
@ -845,6 +899,9 @@ class FlutterVmService {
}
}
/// Thrown when the VM Service disappears while calls are being made to it.
class VmServiceDisappearedException implements Exception {}
/// Whether the event attached to an [Isolate.pauseEvent] should be considered
/// a "pause" event.
bool isPauseEvent(String kind) {

View file

@ -5,6 +5,7 @@
// @dart = 2.8
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/devtools_launcher.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
@ -17,7 +18,7 @@ import 'package:test/fake.dart';
import '../src/common.dart';
import '../src/context.dart';
final vm_service.Isolate isolate = vm_service.Isolate(
final vm_service.Isolate isolate = vm_service.Isolate(
id: '1',
pauseEvent: vm_service.Event(
kind: vm_service.EventKind.kResume,
@ -40,60 +41,17 @@ import '../src/context.dart';
startTime: 0,
isSystemIsolate: false,
isolateFlags: <vm_service.IsolateFlag>[],
extensionRPCs: <String>['foo']
);
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
id: '1',
pauseEvent: vm_service.Event(
kind: vm_service.EventKind.kResume,
timestamp: 0
),
breakpoints: <vm_service.Breakpoint>[],
exceptionPauseMode: null,
extensionRPCs: <String>[],
libraries: <vm_service.LibraryRef>[
vm_service.LibraryRef(
id: '1',
uri: 'file:///hello_world/main.dart',
name: '',
),
],
livePorts: 0,
name: 'test',
number: '1',
pauseOnExit: false,
runnable: true,
startTime: 0,
isSystemIsolate: false,
isolateFlags: <vm_service.IsolateFlag>[],
);
final vm_service.VM fakeVM = vm_service.VM(
isolates: <vm_service.IsolateRef>[fakeUnpausedIsolate],
pid: 1,
hostCPU: '',
isolateGroups: <vm_service.IsolateGroupRef>[],
targetCPU: '',
startTime: 0,
name: 'dart',
architectureBits: 64,
operatingSystem: '',
version: '',
systemIsolateGroups: <vm_service.IsolateGroupRef>[],
systemIsolates: <vm_service.IsolateRef>[],
);
final FlutterView fakeFlutterView = FlutterView(
id: 'a',
uiIsolate: fakeUnpausedIsolate,
extensionRPCs: <String>['ext.flutter.connectedVmServiceUri'],
);
final FakeVmServiceRequest listViews = FakeVmServiceRequest(
method: kListViewsMethod,
jsonResponse: <String, Object>{
'views': <Object>[
fakeFlutterView.toJson(),
FlutterView(
id: 'a',
uiIsolate: isolate,
).toJson()
],
},
);
@ -173,10 +131,10 @@ void main() {
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Extension',
'streamId': 'Isolate',
}
),
FakeVmServiceRequest(method: 'getVM', jsonResponse: fakeVM.toJson()),
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson(),
@ -184,13 +142,11 @@ void main() {
'isolateId': '1',
},
),
FakeVmServiceStreamResponse(
streamId: 'Extension',
event: vm_service.Event(
timestamp: 0,
extensionKind: 'Flutter.FrameworkInitialization',
kind: 'test',
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
listViews,
const FakeVmServiceRequest(
@ -218,15 +174,55 @@ void main() {
);
});
testWithoutContext('wait for extension handles an immediate extension', () {
testWithoutContext('serveAndAnnounceDevTools with skips calling service extensions when VM service disappears', () async {
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
FakeResidentRunner(),
BufferLogger.test(),
);
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Extension',
}
'streamId': 'Isolate',
},
),
FakeVmServiceRequest(method: 'getVM', jsonResponse: fakeVM.toJson()),
const FakeVmServiceRequest(
method: kListViewsMethod,
errorCode: RPCErrorCodes.kServiceDisappeared,
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
errorCode: RPCErrorCodes.kServiceDisappeared,
),
], httpAddress: Uri.parse('http://localhost:1234'));
final FakeFlutterDevice device = FakeFlutterDevice()
..vmService = fakeVmServiceHost.vmService;
await handler.serveAndAnnounceDevTools(
flutterDevices: <FlutterDevice>[device],
);
});
testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async {
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
FakeResidentRunner(),
BufferLogger.test(),
);
final FakeVmServiceHost vmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson(),
@ -234,46 +230,61 @@ void main() {
'isolateId': '1',
},
),
]);
waitForExtension(fakeVmServiceHost.vmService.service, 'foo');
});
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
listViews,
const FakeVmServiceRequest(
method: 'ext.flutter.activeDevToolsServerAddress',
args: <String, Object>{
'isolateId': '1',
'value': 'http://localhost:8080',
},
),
listViews,
const FakeVmServiceRequest(
method: 'ext.flutter.connectedVmServiceUri',
args: <String, Object>{
'isolateId': '1',
'value': 'http://localhost:1234',
},
),
], httpAddress: Uri.parse('http://localhost:1234'));
testWithoutContext('wait for extension handles no isolates', () {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
final FakeVmServiceHost vmServiceHostThatDisappears = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Extension',
}
'streamId': 'Isolate',
},
),
FakeVmServiceRequest(method: 'getVM', jsonResponse: vm_service.VM(
isolates: <vm_service.IsolateRef>[],
pid: 1,
hostCPU: '',
isolateGroups: <vm_service.IsolateGroupRef>[],
targetCPU: '',
startTime: 0,
name: 'dart',
architectureBits: 64,
operatingSystem: '',
version: '',
systemIsolateGroups: <vm_service.IsolateGroupRef>[],
systemIsolates: <vm_service.IsolateRef>[],
).toJson()),
FakeVmServiceStreamResponse(
streamId: 'Extension',
event: vm_service.Event(
timestamp: 0,
extensionKind: 'Flutter.FrameworkInitialization',
kind: 'test',
),
const FakeVmServiceRequest(
method: kListViewsMethod,
errorCode: RPCErrorCodes.kServiceDisappeared,
),
]);
waitForExtension(fakeVmServiceHost.vmService.service, 'foo');
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
errorCode: RPCErrorCodes.kServiceDisappeared,
),
], httpAddress: Uri.parse('http://localhost:5678'));
await handler.serveAndAnnounceDevTools(
flutterDevices: <FlutterDevice>[
FakeFlutterDevice()
..vmService = vmServiceHostThatDisappears.vmService,
FakeFlutterDevice()
..vmService = vmServiceHost.vmService,
],
);
});
}
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
@override
DevToolsServerAddress activeDevToolsServer;
@ -296,6 +307,11 @@ class FakeResidentRunner extends Fake implements ResidentRunner {
}
class FakeFlutterDevice extends Fake implements FlutterDevice {
@override
final Device device = FakeDevice();
@override
FlutterVmService vmService;
}
class FakeDevice extends Fake implements Device {}

View file

@ -45,52 +45,47 @@ final Map<String, Object> vm = <String, dynamic>{
],
};
final vm_service.Isolate isolate = vm_service.Isolate.parse(
<String, dynamic>{
'type': 'Isolate',
'fixedId': true,
'id': 'isolates/242098474',
'name': 'main.dart:main()',
'number': 242098474,
'_originNumber': 242098474,
'startTime': 1540488745340,
'_heaps': <String, dynamic>{
'new': <String, dynamic>{
'used': 0,
'capacity': 0,
'external': 0,
'collections': 0,
'time': 0.0,
'avgCollectionPeriodMillis': 0.0,
},
'old': <String, dynamic>{
'used': 0,
'capacity': 0,
'external': 0,
'collections': 0,
'time': 0.0,
'avgCollectionPeriodMillis': 0.0,
},
},
}
const String kExtensionName = 'ext.flutter.test.interestingExtension';
final vm_service.Isolate isolate = vm_service.Isolate(
id: '1',
pauseEvent: vm_service.Event(
kind: vm_service.EventKind.kResume,
timestamp: 0
),
breakpoints: <vm_service.Breakpoint>[],
exceptionPauseMode: null,
libraries: <vm_service.LibraryRef>[
vm_service.LibraryRef(
id: '1',
uri: 'file:///hello_world/main.dart',
name: '',
),
],
livePorts: 0,
name: 'test',
number: '1',
pauseOnExit: false,
runnable: true,
startTime: 0,
isSystemIsolate: false,
isolateFlags: <vm_service.IsolateFlag>[],
extensionRPCs: <String>[kExtensionName],
);
final Map<String, Object> listViews = <String, dynamic>{
'type': 'FlutterViewList',
'views': <dynamic>[
<String, dynamic>{
'type': 'FlutterView',
'id': '_flutterView/0x4a4c1f8',
'isolate': <String, dynamic>{
'type': '@Isolate',
'fixedId': true,
'id': 'isolates/242098474',
'name': 'main.dart:main()',
'number': 242098474,
},
},
]
};
final FlutterView fakeFlutterView = FlutterView(
id: 'a',
uiIsolate: isolate,
);
final FakeVmServiceRequest listViewsRequest = FakeVmServiceRequest(
method: kListViewsMethod,
jsonResponse: <String, Object>{
'views': <Object>[
fakeFlutterView.toJson(),
],
},
);
typedef ServiceCallback = Future<Map<String, dynamic>> Function(Map<String, Object>);
@ -408,17 +403,7 @@ void main() {
'views': <Object>[],
},
),
const FakeVmServiceRequest(
method: kListViewsMethod,
jsonResponse: <String, Object>{
'views': <Object>[
<String, Object>{
'id': 'a',
'isolate': <String, Object>{},
},
],
},
),
listViewsRequest,
]
);
@ -452,6 +437,187 @@ void main() {
expect(fakeVmServiceHost.hasRemainingExpectations, false);
});
group('findExtensionIsolate', () {
testWithoutContext('returns an isolate with the registered extensionRPC', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
listViewsRequest,
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson(),
args: <String, Object>{
'isolateId': '1',
},
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
]);
final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName);
expect(isolateRef.id, '1');
});
testWithoutContext('returns the isolate with the registered extensionRPC when there are multiple FlutterViews', () async {
const String otherExtensionName = 'ext.flutter.test.otherExtension';
// Copy the other isolate and change a few fields.
final vm_service.Isolate isolate2 = vm_service.Isolate.parse(
isolate.toJson()
..['id'] = '2'
..['extensionRPCs'] = <String>[otherExtensionName],
);
final FlutterView fakeFlutterView2 = FlutterView(
id: '2',
uiIsolate: isolate2,
);
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
FakeVmServiceRequest(
method: kListViewsMethod,
jsonResponse: <String, Object>{
'views': <Object>[
fakeFlutterView.toJson(),
fakeFlutterView2.toJson(),
],
},
),
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson(),
args: <String, Object>{
'isolateId': '1',
},
),
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate2.toJson(),
args: <String, Object>{
'isolateId': '2',
},
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
]);
final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(otherExtensionName);
expect(isolateRef.id, '2');
});
testWithoutContext('when the isolate stream is already subscribed, returns an isolate with the registered extensionRPC', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
// Stream already subscribed - https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#streamlisten
errorCode: 103,
),
listViewsRequest,
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson()..['extensionRPCs'] = <String>[kExtensionName],
args: <String, Object>{
'isolateId': '1',
},
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
]);
final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName);
expect(isolateRef.id, '1');
});
testWithoutContext('returns an isolate with a extensionRPC that is registered later', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
listViewsRequest,
FakeVmServiceRequest(
method: 'getIsolate',
jsonResponse: isolate.toJson(),
args: <String, Object>{
'isolateId': '1',
},
),
FakeVmServiceStreamResponse(
streamId: 'Isolate',
event: vm_service.Event(
kind: vm_service.EventKind.kServiceExtensionAdded,
extensionRPC: kExtensionName,
timestamp: 1,
),
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
),
]);
final vm_service.IsolateRef isolateRef = await fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName);
expect(isolateRef.id, '1');
});
testWithoutContext('throws when the service disappears', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
'streamId': 'Isolate',
},
),
const FakeVmServiceRequest(
method: kListViewsMethod,
errorCode: RPCErrorCodes.kServiceDisappeared,
),
const FakeVmServiceRequest(
method: 'streamCancel',
args: <String, Object>{
'streamId': 'Isolate',
},
errorCode: RPCErrorCodes.kServiceDisappeared,
),
]);
expect(
() => fakeVmServiceHost.vmService.findExtensionIsolate(kExtensionName),
throwsA(isA<VmServiceDisappearedException>()),
);
});
});
testWithoutContext('Can process log events from the vm service', () {
final vm_service.Event event = vm_service.Event(
bytes: base64.encode(utf8.encode('Hello There\n')),