Unverified Commit 9202e547 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] move service extensions off of deprecated vm service (#55012)

parent 96a3b2ae
...@@ -34,6 +34,7 @@ import '../project.dart'; ...@@ -34,6 +34,7 @@ import '../project.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
import '../resident_runner.dart'; import '../resident_runner.dart';
import '../run_hot.dart'; import '../run_hot.dart';
import '../vmservice.dart';
import '../web/chrome.dart'; import '../web/chrome.dart';
import '../web/compile.dart'; import '../web/compile.dart';
import '../web/web_device.dart'; import '../web/web_device.dart';
...@@ -195,9 +196,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -195,9 +196,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugDumpApp() async { Future<void> debugDumpApp() async {
try { try {
await _vmService?.callServiceExtension( await _vmService
'ext.flutter.debugDumpApp', ?.flutterDebugDumpApp(
); isolateId: null,
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -206,9 +208,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -206,9 +208,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugDumpRenderTree() async { Future<void> debugDumpRenderTree() async {
try { try {
await _vmService?.callServiceExtension( await _vmService
'ext.flutter.debugDumpRenderTree', ?.flutterDebugDumpRenderTree(
); isolateId: null,
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -217,9 +220,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -217,9 +220,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugDumpLayerTree() async { Future<void> debugDumpLayerTree() async {
try { try {
await _vmService?.callServiceExtension( await _vmService
'ext.flutter.debugDumpLayerTree', ?.flutterDebugDumpLayerTree(
); isolateId: null,
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -228,8 +232,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -228,8 +232,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugDumpSemanticsTreeInTraversalOrder() async { Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
try { try {
await _vmService?.callServiceExtension( await _vmService
'ext.flutter.debugDumpSemanticsTreeInTraversalOrder'); ?.flutterDebugDumpSemanticsTreeInTraversalOrder(
isolateId: null,
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -238,14 +244,16 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -238,14 +244,16 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugTogglePlatform() async { Future<void> debugTogglePlatform() async {
try { try {
final vmservice.Response response = await _vmService final String currentPlatform = await _vmService
?.callServiceExtension('ext.flutter.platformOverride'); ?.flutterPlatformOverride(
final String currentPlatform = response.json['value'] as String; isolateId: null,
);
final String platform = nextPlatform(currentPlatform, featureFlags); final String platform = nextPlatform(currentPlatform, featureFlags);
await _vmService?.callServiceExtension('ext.flutter.platformOverride', await _vmService
args: <String, Object>{ ?.flutterPlatformOverride(
'value': platform, platform: platform,
}); isolateId: null,
);
globals.printStatus('Switched operating system to $platform'); globals.printStatus('Switched operating system to $platform');
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
...@@ -261,8 +269,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -261,8 +269,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
try { try {
await _vmService?.callServiceExtension( await _vmService
'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder'); ?.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
isolateId: null,
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -271,16 +281,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -271,16 +281,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugToggleDebugPaintSizeEnabled() async { Future<void> debugToggleDebugPaintSizeEnabled() async {
try { try {
final vmservice.Response response = await _vmService
await _vmService?.callServiceExtension( ?.flutterToggleDebugPaintSizeEnabled(
'ext.flutter.debugPaint', isolateId: null,
); );
await _vmService?.callServiceExtension(
'ext.flutter.debugPaint',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -289,16 +293,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -289,16 +293,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugToggleDebugCheckElevationsEnabled() async { Future<void> debugToggleDebugCheckElevationsEnabled() async {
try { try {
final vmservice.Response response = await _vmService
await _vmService?.callServiceExtension( ?.flutterToggleDebugCheckElevationsEnabled(
'ext.flutter.debugCheckElevationsEnabled', isolateId: null,
); );
await _vmService?.callServiceExtension(
'ext.flutter.debugCheckElevationsEnabled',
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -307,14 +305,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -307,14 +305,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugTogglePerformanceOverlayOverride() async { Future<void> debugTogglePerformanceOverlayOverride() async {
try { try {
final vmservice.Response response = await _vmService await _vmService
?.callServiceExtension('ext.flutter.showPerformanceOverlay'); ?.flutterTogglePerformanceOverlayOverride(
await _vmService?.callServiceExtension( isolateId: null,
'ext.flutter.showPerformanceOverlay', );
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -323,14 +317,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -323,14 +317,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugToggleWidgetInspector() async { Future<void> debugToggleWidgetInspector() async {
try { try {
final vmservice.Response response = await _vmService await _vmService
?.callServiceExtension('ext.flutter.debugToggleWidgetInspector'); ?.flutterToggleWidgetInspector(
await _vmService?.callServiceExtension( isolateId: null,
'ext.flutter.debugToggleWidgetInspector', );
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -339,14 +329,10 @@ abstract class ResidentWebRunner extends ResidentRunner { ...@@ -339,14 +329,10 @@ abstract class ResidentWebRunner extends ResidentRunner {
@override @override
Future<void> debugToggleProfileWidgetBuilds() async { Future<void> debugToggleProfileWidgetBuilds() async {
try { try {
final vmservice.Response response = await _vmService await _vmService
?.callServiceExtension('ext.flutter.profileWidgetBuilds'); ?.flutterToggleProfileWidgetBuilds(
await _vmService?.callServiceExtension( isolateId: null,
'ext.flutter.profileWidgetBuilds', );
args: <dynamic, dynamic>{
'enabled': !(response.json['enabled'] == 'true')
},
);
} on vmservice.RPCError { } on vmservice.RPCError {
return; return;
} }
...@@ -674,6 +660,14 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -674,6 +660,14 @@ class _ResidentWebRunner extends ResidentWebRunner {
_connectionResult = await webDevFS.connect(useDebugExtension); _connectionResult = await webDevFS.connect(useDebugExtension);
unawaited(_connectionResult.debugConnection.onDone.whenComplete(_cleanupAndExit)); unawaited(_connectionResult.debugConnection.onDone.whenComplete(_cleanupAndExit));
_stdOutSub = _vmService.onStdoutEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes));
globals.printStatus(message, newline: false);
});
_stdErrSub = _vmService.onStderrEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes));
globals.printStatus(message, newline: false);
});
try { try {
await _vmService.streamListen(vmservice.EventStreams.kStdout); await _vmService.streamListen(vmservice.EventStreams.kStdout);
} on vmservice.RPCError { } on vmservice.RPCError {
...@@ -692,14 +686,6 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -692,14 +686,6 @@ class _ResidentWebRunner extends ResidentWebRunner {
// It is safe to ignore this error because we expect an error to be // It is safe to ignore this error because we expect an error to be
// thrown if we're not already subscribed. // thrown if we're not already subscribed.
} }
_stdOutSub = _vmService.onStdoutEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes));
globals.printStatus(message, newline: false);
});
_stdErrSub = _vmService.onStderrEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes));
globals.printStatus(message, newline: false);
});
unawaited(_vmService.registerService('reloadSources', 'FlutterTools')); unawaited(_vmService.registerService('reloadSources', 'FlutterTools'));
_vmService.registerServiceCallback('reloadSources', (Map<String, Object> params) async { _vmService.registerServiceCallback('reloadSources', (Map<String, Object> params) async {
final bool pause = params['pause'] as bool ?? false; final bool pause = params['pause'] as bool ?? false;
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'android/android_device_discovery.dart'; import 'android/android_device_discovery.dart';
import 'android/android_workflow.dart'; import 'android/android_workflow.dart';
...@@ -25,7 +26,6 @@ import 'linux/linux_device.dart'; ...@@ -25,7 +26,6 @@ import 'linux/linux_device.dart';
import 'macos/macos_device.dart'; import 'macos/macos_device.dart';
import 'project.dart'; import 'project.dart';
import 'tester/flutter_tester.dart'; import 'tester/flutter_tester.dart';
import 'vmservice.dart';
import 'web/web_device.dart'; import 'web/web_device.dart';
import 'windows/windows_device.dart'; import 'windows/windows_device.dart';
...@@ -755,7 +755,7 @@ abstract class DeviceLogReader { ...@@ -755,7 +755,7 @@ abstract class DeviceLogReader {
/// Some logs can be obtained from a VM service stream. /// Some logs can be obtained from a VM service stream.
/// Set this after the VM services are connected. /// Set this after the VM services are connected.
VMService connectedVMService; vm_service.VmService connectedVMService;
@override @override
String toString() => name; String toString() => name;
...@@ -785,7 +785,7 @@ class NoOpDeviceLogReader implements DeviceLogReader { ...@@ -785,7 +785,7 @@ class NoOpDeviceLogReader implements DeviceLogReader {
int appPid; int appPid;
@override @override
VMService connectedVMService; vm_service.VmService connectedVMService;
@override @override
Stream<String> get logLines => const Stream<String>.empty(); Stream<String> get logLines => const Stream<String>.empty();
......
...@@ -24,7 +24,6 @@ import '../macos/xcode.dart'; ...@@ -24,7 +24,6 @@ import '../macos/xcode.dart';
import '../mdns_discovery.dart'; import '../mdns_discovery.dart';
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import '../vmservice.dart';
import 'fallback_discovery.dart'; import 'fallback_discovery.dart';
import 'ios_deploy.dart'; import 'ios_deploy.dart';
import 'ios_workflow.dart'; import 'ios_workflow.dart';
...@@ -561,18 +560,18 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -561,18 +560,18 @@ class IOSDeviceLogReader extends DeviceLogReader {
Stream<String> get logLines => _linesController.stream; Stream<String> get logLines => _linesController.stream;
@override @override
VMService get connectedVMService => _connectedVMService; vm_service.VmService get connectedVMService => _connectedVMService;
VMService _connectedVMService; vm_service.VmService _connectedVMService;
@override @override
set connectedVMService(VMService connectedVmService) { set connectedVMService(vm_service.VmService connectedVmService) {
_listenToUnifiedLoggingEvents(connectedVmService); _listenToUnifiedLoggingEvents(connectedVmService);
_connectedVMService = connectedVmService; _connectedVMService = connectedVmService;
} }
static const int _minimumUniversalLoggingSdkVersion = 13; static const int _minimumUniversalLoggingSdkVersion = 13;
Future<void> _listenToUnifiedLoggingEvents(VMService connectedVmService) async { Future<void> _listenToUnifiedLoggingEvents(vm_service.VmService connectedVmService) async {
if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) {
return; return;
} }
......
...@@ -152,7 +152,7 @@ class FlutterDevice { ...@@ -152,7 +152,7 @@ class FlutterDevice {
final ResidentCompiler generator; final ResidentCompiler generator;
final BuildInfo buildInfo; final BuildInfo buildInfo;
Stream<Uri> observatoryUris; Stream<Uri> observatoryUris;
VMService vmService; vm_service.VmService vmService;
DevFS devFS; DevFS devFS;
ApplicationPackage package; ApplicationPackage package;
List<String> fileSystemRoots; List<String> fileSystemRoots;
...@@ -226,24 +226,28 @@ class FlutterDevice { ...@@ -226,24 +226,28 @@ class FlutterDevice {
return completer.future; return completer.future;
} }
// TODO(jonahwilliams): remove once all callsites are updated.
VMService get flutterDeprecatedVmService => vmService as VMService;
Future<void> refreshViews() async { Future<void> refreshViews() async {
if (vmService == null) { if (vmService == null) {
return; return;
} }
await vmService.vm.refreshViews(waitForViews: true); await flutterDeprecatedVmService.vm.refreshViews(waitForViews: true);
} }
List<FlutterView> get views { List<FlutterView> get views {
if (vmService == null || vmService.isClosed) { if (vmService == null || flutterDeprecatedVmService.isClosed) {
return <FlutterView>[]; return <FlutterView>[];
} }
return (viewFilter != null return (viewFilter != null
? vmService.vm.allViewsWithName(viewFilter) ? flutterDeprecatedVmService.vm.allViewsWithName(viewFilter)
: vmService.vm.views).toList(); : flutterDeprecatedVmService.vm.views).toList();
} }
Future<void> getVMs() => vmService.getVMOld(); Future<void> getVMs() => flutterDeprecatedVmService.getVMOld();
Future<void> exitApps() async { Future<void> exitApps() async {
if (!device.supportsFlutterExit) { if (!device.supportsFlutterExit) {
...@@ -270,7 +274,9 @@ class FlutterDevice { ...@@ -270,7 +274,9 @@ class FlutterDevice {
for (final FlutterView view in flutterViews) { for (final FlutterView view in flutterViews) {
if (view != null && view.uiIsolate != null) { if (view != null && view.uiIsolate != null) {
assert(!view.uiIsolate.pauseEvent.isPauseEvent); assert(!view.uiIsolate.pauseEvent.isPauseEvent);
futures.add(view.uiIsolate.flutterExit()); futures.add(vmService.flutterExit(
isolateId: view.uiIsolate.id,
));
} }
} }
// The flutterExit message only returns if it fails, so just wait a few // The flutterExit message only returns if it fails, so just wait a few
...@@ -286,7 +292,7 @@ class FlutterDevice { ...@@ -286,7 +292,7 @@ class FlutterDevice {
}) { }) {
// One devFS per device. Shared by all running instances. // One devFS per device. Shared by all running instances.
devFS = DevFS( devFS = DevFS(
vmService, flutterDeprecatedVmService,
fsName, fsName,
rootDirectory, rootDirectory,
osUtils: globals.os, osUtils: globals.os,
...@@ -301,7 +307,7 @@ class FlutterDevice { ...@@ -301,7 +307,7 @@ class FlutterDevice {
final String deviceEntryUri = devFS.baseUri final String deviceEntryUri = devFS.baseUri
.resolveUri(globals.fs.path.toUri(entryPath)).toString(); .resolveUri(globals.fs.path.toUri(entryPath)).toString();
return <Future<vm_service.ReloadReport>>[ return <Future<vm_service.ReloadReport>>[
for (final Isolate isolate in vmService.vm.isolates) for (final Isolate isolate in flutterDeprecatedVmService.vm.isolates)
vmService.reloadSources( vmService.reloadSources(
isolate.id, isolate.id,
pause: pause, pause: pause,
...@@ -325,68 +331,91 @@ class FlutterDevice { ...@@ -325,68 +331,91 @@ class FlutterDevice {
Future<void> debugDumpApp() async { Future<void> debugDumpApp() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterDebugDumpApp(); await vmService.flutterDebugDumpApp(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> debugDumpRenderTree() async { Future<void> debugDumpRenderTree() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterDebugDumpRenderTree(); await vmService.flutterDebugDumpRenderTree(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> debugDumpLayerTree() async { Future<void> debugDumpLayerTree() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterDebugDumpLayerTree(); await vmService.flutterDebugDumpLayerTree(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> debugDumpSemanticsTreeInTraversalOrder() async { Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder(); await vmService.flutterDebugDumpSemanticsTreeInTraversalOrder(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(); await vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> toggleDebugPaintSizeEnabled() async { Future<void> toggleDebugPaintSizeEnabled() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterToggleDebugPaintSizeEnabled(); await vmService.flutterToggleDebugPaintSizeEnabled(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> toggleDebugCheckElevationsEnabled() async { Future<void> toggleDebugCheckElevationsEnabled() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterToggleDebugCheckElevationsEnabled(); await vmService.flutterToggleDebugCheckElevationsEnabled(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> debugTogglePerformanceOverlayOverride() async { Future<void> debugTogglePerformanceOverlayOverride() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterTogglePerformanceOverlayOverride(); await vmService.flutterTogglePerformanceOverlayOverride(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> toggleWidgetInspector() async { Future<void> toggleWidgetInspector() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterToggleWidgetInspector(); await vmService.flutterToggleWidgetInspector(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<void> toggleProfileWidgetBuilds() async { Future<void> toggleProfileWidgetBuilds() async {
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterToggleProfileWidgetBuilds(); await vmService.flutterToggleProfileWidgetBuilds(
isolateId: view.uiIsolate.id,
);
} }
} }
Future<String> togglePlatform({ String from }) async { Future<String> togglePlatform({ String from }) async {
final String to = nextPlatform(from, featureFlags); final String to = nextPlatform(from, featureFlags);
for (final FlutterView view in views) { for (final FlutterView view in views) {
await view.uiIsolate.flutterPlatformOverride(to); await vmService.flutterPlatformOverride(
platform: to,
isolateId: view.uiIsolate.id,
);
} }
return to; return to;
} }
...@@ -416,7 +445,9 @@ class FlutterDevice { ...@@ -416,7 +445,9 @@ class FlutterDevice {
} }
Future<void> initLogReader() async { Future<void> initLogReader() async {
(await device.getLogReader(app: package)).appPid = vmService.vm.pid; final vm_service.VM vm = await vmService.getVM();
final DeviceLogReader logReader = await device.getLogReader(app: package);
logReader.appPid = vm.pid;
} }
Future<int> runHot({ Future<int> runHot({
...@@ -721,8 +752,16 @@ abstract class ResidentRunner { ...@@ -721,8 +752,16 @@ abstract class ResidentRunner {
String method, { String method, {
Map<String, dynamic> params, Map<String, dynamic> params,
}) { }) {
return flutterDevices.first.views.first.uiIsolate return flutterDevices
.invokeFlutterExtensionRpcRaw(method, params: params); .first
.vmService
.invokeFlutterExtensionRpcRaw(
method,
args: params,
isolateId: flutterDevices
.first.views
.first.uiIsolate.id
);
} }
/// Whether this runner can hot reload. /// Whether this runner can hot reload.
...@@ -812,7 +851,7 @@ abstract class ResidentRunner { ...@@ -812,7 +851,7 @@ abstract class ResidentRunner {
void writeVmserviceFile() { void writeVmserviceFile() {
if (debuggingOptions.vmserviceOutFile != null) { if (debuggingOptions.vmserviceOutFile != null) {
try { try {
final String address = flutterDevices.first.vmService.wsAddress.toString(); final String address = flutterDevices.first.flutterDeprecatedVmService.wsAddress.toString();
final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile); final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile);
vmserviceOutFile.createSync(recursive: true); vmserviceOutFile.createSync(recursive: true);
vmserviceOutFile.writeAsStringSync(address); vmserviceOutFile.writeAsStringSync(address);
...@@ -944,7 +983,10 @@ abstract class ResidentRunner { ...@@ -944,7 +983,10 @@ abstract class ResidentRunner {
await device.refreshViews(); await device.refreshViews();
try { try {
for (final FlutterView view in device.views) { for (final FlutterView view in device.views) {
await view.uiIsolate.flutterDebugAllowBanner(false); await device.vmService.flutterDebugAllowBanner(
false,
isolateId: view.uiIsolate.id,
);
} }
} on Exception catch (error) { } on Exception catch (error) {
status.cancel(); status.cancel();
...@@ -958,7 +1000,10 @@ abstract class ResidentRunner { ...@@ -958,7 +1000,10 @@ abstract class ResidentRunner {
if (supportsServiceProtocol && isRunningDebug) { if (supportsServiceProtocol && isRunningDebug) {
try { try {
for (final FlutterView view in device.views) { for (final FlutterView view in device.views) {
await view.uiIsolate.flutterDebugAllowBanner(true); await device.vmService.flutterDebugAllowBanner(
true,
isolateId: view.uiIsolate.id,
);
} }
} on Exception catch (error) { } on Exception catch (error) {
status.cancel(); status.cancel();
...@@ -980,7 +1025,12 @@ abstract class ResidentRunner { ...@@ -980,7 +1025,12 @@ abstract class ResidentRunner {
Future<void> debugTogglePlatform() async { Future<void> debugTogglePlatform() async {
await refreshViews(); await refreshViews();
final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride(); final String isolateId = flutterDevices
.first.views.first.uiIsolate.id;
final String from = await flutterDevices
.first.vmService.flutterPlatformOverride(
isolateId: isolateId,
);
String to; String to;
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
to = await device.togglePlatform(from: from); to = await device.togglePlatform(from: from);
...@@ -1039,7 +1089,7 @@ abstract class ResidentRunner { ...@@ -1039,7 +1089,7 @@ abstract class ResidentRunner {
// This hooks up callbacks for when the connection stops in the future. // This hooks up callbacks for when the connection stops in the future.
// We don't want to wait for them. We don't handle errors in those callbacks' // We don't want to wait for them. We don't handle errors in those callbacks'
// futures either because they just print to logger and is not critical. // futures either because they just print to logger and is not critical.
unawaited(device.vmService.done.then<void>( unawaited(device.vmService.onDone.then<void>(
_serviceProtocolDone, _serviceProtocolDone,
onError: _serviceProtocolError, onError: _serviceProtocolError,
).whenComplete(_serviceDisconnected)); ).whenComplete(_serviceDisconnected));
...@@ -1056,7 +1106,7 @@ abstract class ResidentRunner { ...@@ -1056,7 +1106,7 @@ abstract class ResidentRunner {
<String, dynamic>{ <String, dynamic>{
'reuseWindows': true, 'reuseWindows': true,
}, },
flutterDevices.first.vmService.httpAddress, flutterDevices.first.flutterDeprecatedVmService.httpAddress,
'http://${_devtoolsServer.address.host}:${_devtoolsServer.port}', 'http://${_devtoolsServer.address.host}:${_devtoolsServer.port}',
false, // headless mode, false, // headless mode,
false, // machine mode false, // machine mode
......
...@@ -83,8 +83,8 @@ class ColdRunner extends ResidentRunner { ...@@ -83,8 +83,8 @@ class ColdRunner extends ResidentRunner {
if (flutterDevices.first.observatoryUris != null) { if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection. // For now, only support one debugger connection.
connectionInfoCompleter?.complete(DebugConnectionInfo( connectionInfoCompleter?.complete(DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress, httpUri: flutterDevices.first.flutterDeprecatedVmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress, wsUri: flutterDevices.first.flutterDeprecatedVmService.wsAddress,
)); ));
} }
...@@ -105,7 +105,7 @@ class ColdRunner extends ResidentRunner { ...@@ -105,7 +105,7 @@ class ColdRunner extends ResidentRunner {
if (device.vmService != null) { if (device.vmService != null) {
globals.printStatus('Tracing startup on ${device.device.name}.'); globals.printStatus('Tracing startup on ${device.device.name}.');
await downloadStartupTrace( await downloadStartupTrace(
device.vmService, device.flutterDeprecatedVmService,
awaitFirstFrame: awaitFirstFrameWhenTracing, awaitFirstFrame: awaitFirstFrameWhenTracing,
); );
} }
...@@ -197,7 +197,7 @@ class ColdRunner extends ResidentRunner { ...@@ -197,7 +197,7 @@ class ColdRunner extends ResidentRunner {
// Caution: This log line is parsed by device lab tests. // Caution: This log line is parsed by device lab tests.
globals.printStatus( globals.printStatus(
'An Observatory debugger and profiler on $dname is available at: ' 'An Observatory debugger and profiler on $dname is available at: '
'${device.vmService.httpAddress}', '${device.flutterDeprecatedVmService.httpAddress}',
); );
} }
} }
......
...@@ -205,7 +205,10 @@ class HotRunner extends ResidentRunner { ...@@ -205,7 +205,10 @@ class HotRunner extends ResidentRunner {
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
for (final FlutterView view in device.views) { for (final FlutterView view in device.views) {
await view.uiIsolate.flutterFastReassemble(classId); await device.vmService.flutterFastReassemble(
classId,
isolateId: view.uiIsolate.id,
);
} }
} }
...@@ -260,8 +263,8 @@ class HotRunner extends ResidentRunner { ...@@ -260,8 +263,8 @@ class HotRunner extends ResidentRunner {
// Only handle one debugger connection. // Only handle one debugger connection.
connectionInfoCompleter.complete( connectionInfoCompleter.complete(
DebugConnectionInfo( DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress, httpUri: flutterDevices.first.flutterDeprecatedVmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress, wsUri: flutterDevices.first.flutterDeprecatedVmService.wsAddress,
baseUri: baseUris.first.toString(), baseUri: baseUris.first.toString(),
), ),
); );
...@@ -570,7 +573,7 @@ class HotRunner extends ResidentRunner { ...@@ -570,7 +573,7 @@ class HotRunner extends ResidentRunner {
// The engine handles killing and recreating isolates that it has spawned // The engine handles killing and recreating isolates that it has spawned
// ("uiIsolates"). The isolates that were spawned from these uiIsolates // ("uiIsolates"). The isolates that were spawned from these uiIsolates
// will not be restared, and so they must be manually killed. // will not be restared, and so they must be manually killed.
for (final Isolate isolate in device?.vmService?.vm?.isolates ?? <Isolate>[]) { for (final Isolate isolate in device?.flutterDeprecatedVmService?.vm?.isolates ?? <Isolate>[]) {
if (!uiIsolates.contains(isolate)) { if (!uiIsolates.contains(isolate)) {
operations.add(isolate.invokeRpcRaw('kill', params: <String, dynamic>{ operations.add(isolate.invokeRpcRaw('kill', params: <String, dynamic>{
'isolateId': isolate.id, 'isolateId': isolate.id,
...@@ -938,10 +941,16 @@ class HotRunner extends ResidentRunner { ...@@ -938,10 +941,16 @@ class HotRunner extends ResidentRunner {
} }
await Future.wait(allDevices); await Future.wait(allDevices);
// Check if any isolates are paused. globals.printTrace('Evicting dirty assets');
await _evictDirtyAssets();
// Check if any isolates are paused and reassemble those
// that aren't.
final List<FlutterView> reassembleViews = <FlutterView>[]; final List<FlutterView> reassembleViews = <FlutterView>[];
final List<Future<void>> reassembleFutures = <Future<void>>[];
String serviceEventKind; String serviceEventKind;
int pausedIsolatesFound = 0; int pausedIsolatesFound = 0;
bool failedReassemble = false;
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
for (final FlutterView view in device.views) { for (final FlutterView view in device.views) {
// Check if the isolate is paused, and if so, don't reassemble. Ignore the // Check if the isolate is paused, and if so, don't reassemble. Ignore the
...@@ -956,6 +965,12 @@ class HotRunner extends ResidentRunner { ...@@ -956,6 +965,12 @@ class HotRunner extends ResidentRunner {
} }
} else { } else {
reassembleViews.add(view); reassembleViews.add(view);
reassembleFutures.add(device.vmService.flutterReassemble(
isolateId: view.uiIsolate.id,
).catchError((dynamic error) {
failedReassemble = true;
globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
}, test: (dynamic error) => error is Exception));
} }
} }
} }
...@@ -968,24 +983,10 @@ class HotRunner extends ResidentRunner { ...@@ -968,24 +983,10 @@ class HotRunner extends ResidentRunner {
return OperationResult(OperationResult.ok.code, reloadMessage); return OperationResult(OperationResult.ok.code, reloadMessage);
} }
} }
globals.printTrace('Evicting dirty assets');
await _evictDirtyAssets();
assert(reassembleViews.isNotEmpty); assert(reassembleViews.isNotEmpty);
globals.printTrace('Reassembling application'); globals.printTrace('Reassembling application');
bool failedReassemble = false; final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures);
final List<Future<void>> futures = <Future<void>>[
for (final FlutterView view in reassembleViews)
() async {
try {
await view.uiIsolate.flutterReassemble();
} on Exception catch (error) {
failedReassemble = true;
globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
return;
}
}(),
];
final Future<void> reassembleFuture = Future.wait<void>(futures);
await reassembleFuture.timeout( await reassembleFuture.timeout(
const Duration(seconds: 2), const Duration(seconds: 2),
onTimeout: () async { onTimeout: () async {
...@@ -1128,7 +1129,7 @@ class HotRunner extends ResidentRunner { ...@@ -1128,7 +1129,7 @@ class HotRunner extends ResidentRunner {
// Caution: This log line is parsed by device lab tests. // Caution: This log line is parsed by device lab tests.
globals.printStatus( globals.printStatus(
'An Observatory debugger and profiler on $dname is available at: ' 'An Observatory debugger and profiler on $dname is available at: '
'${device.vmService.httpAddress}', '${device.flutterDeprecatedVmService.httpAddress}',
); );
} }
} }
...@@ -1144,7 +1145,13 @@ class HotRunner extends ResidentRunner { ...@@ -1144,7 +1145,13 @@ class HotRunner extends ResidentRunner {
continue; continue;
} }
for (final String assetPath in device.devFS.assetPathsToEvict) { for (final String assetPath in device.devFS.assetPathsToEvict) {
futures.add(device.views.first.uiIsolate.flutterEvictAsset(assetPath)); futures.add(
device.views.first.uiIsolate.vmService
.flutterEvictAsset(
assetPath,
isolateId: device.views.first.uiIsolate.id,
)
);
} }
device.devFS.assetPathsToEvict.clear(); device.devFS.assetPathsToEvict.clear();
} }
......
...@@ -54,7 +54,10 @@ class Tracing { ...@@ -54,7 +54,10 @@ class Tracing {
}); });
bool done = false; bool done = false;
for (final FlutterView view in vmService.vm.views) { for (final FlutterView view in vmService.vm.views) {
if (await view.uiIsolate.flutterAlreadyPaintedFirstUsefulFrame()) { if (await view.uiIsolate.vmService
.flutterAlreadyPaintedFirstUsefulFrame(
isolateId: view.uiIsolate.id,
)) {
done = true; done = true;
break; break;
} }
......
...@@ -5,12 +5,17 @@ ...@@ -5,12 +5,17 @@
import 'dart:async'; import 'dart:async';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/testing/async.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/net.dart';
import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/attach.dart'; import 'package:flutter_tools/src/commands/attach.dart';
...@@ -22,10 +27,6 @@ import 'package:flutter_tools/src/project.dart'; ...@@ -22,10 +27,6 @@ import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:quiver/testing/async.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart'; import '../../src/common.dart';
...@@ -142,7 +143,7 @@ void main() { ...@@ -142,7 +143,7 @@ void main() {
final Process dartProcess = MockProcess(); final Process dartProcess = MockProcess();
final StreamController<List<int>> compilerStdoutController = StreamController<List<int>>(); final StreamController<List<int>> compilerStdoutController = StreamController<List<int>>();
when(dartProcess.stdout).thenAnswer((_) => compilerStdoutController.stream); when(dartProcess.stdout).thenAnswer((_) => compilerStdoutController.stream);
when(dartProcess.stderr) when(dartProcess.stderr)
.thenAnswer((_) => Stream<List<int>>.fromFuture(Future<List<int>>.value(const <int>[]))); .thenAnswer((_) => Stream<List<int>>.fromFuture(Future<List<int>>.value(const <int>[])));
...@@ -787,6 +788,23 @@ VMServiceConnector getFakeVmServiceFactory({ ...@@ -787,6 +788,23 @@ VMServiceConnector getFakeVmServiceFactory({
when(vmService.done).thenAnswer((_) { when(vmService.done).thenAnswer((_) {
return Future<void>.value(null); return Future<void>.value(null);
}); });
when(vmService.onDone).thenAnswer((_) {
return Future<void>.value(null);
});
when(vmService.getVM()).thenAnswer((_) async {
return vm_service.VM(
pid: 1,
architectureBits: 64,
hostCPU: '',
name: '',
isolates: <vm_service.IsolateRef>[],
isolateGroups: <vm_service.IsolateGroupRef>[],
startTime: 0,
targetCPU: '',
operatingSystem: '',
version: '',
);
});
when(vm.refreshViews(waitForViews: anyNamed('waitForViews'))) when(vm.refreshViews(waitForViews: anyNamed('waitForViews')))
.thenAnswer((_) => Future<void>.value(null)); .thenAnswer((_) => Future<void>.value(null));
......
...@@ -6,7 +6,6 @@ import 'dart:async'; ...@@ -6,7 +6,6 @@ import 'dart:async';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
...@@ -285,10 +284,10 @@ void main() { ...@@ -285,10 +284,10 @@ void main() {
testWithoutContext('runInView forwards arguments correctly', () async { testWithoutContext('runInView forwards arguments correctly', () async {
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[ requests: <VmServiceExpectation>[
const FakeVmServiceRequest(method: 'streamListen', id: '1', params: <String, Object>{ const FakeVmServiceRequest(method: 'streamListen', id: '1', args: <String, Object>{
'streamId': 'Isolate' 'streamId': 'Isolate'
}), }),
const FakeVmServiceRequest(method: kRunInViewMethod, id: '2', params: <String, Object>{ const FakeVmServiceRequest(method: kRunInViewMethod, id: '2', args: <String, Object>{
'viewId': '1234', 'viewId': '1234',
'mainScript': 'main.dart', 'mainScript': 'main.dart',
'assetDirectory': 'flutter_assets/', 'assetDirectory': 'flutter_assets/',
...@@ -312,95 +311,6 @@ void main() { ...@@ -312,95 +311,6 @@ void main() {
}); });
} }
class FakeVmServiceHost {
FakeVmServiceHost({
@required List<VmServiceExpectation> requests,
}) : _requests = requests {
_vmService = vm_service.VmService(
_input.stream,
_output.add,
);
_applyStreamListen();
_output.stream.listen((String data) {
final Map<String, Object> request = json.decode(data) as Map<String, Object>;
if (_requests.isEmpty) {
throw Exception('Unexpected request: $request');
}
final FakeVmServiceRequest fakeRequest = _requests.removeAt(0) as FakeVmServiceRequest;
expect(fakeRequest, isA<FakeVmServiceRequest>()
.having((FakeVmServiceRequest request) => request.method, 'method', request['method'])
.having((FakeVmServiceRequest request) => request.id, 'id', request['id'])
.having((FakeVmServiceRequest request) => request.params, 'params', request['params'])
);
_input.add(json.encode(<String, Object>{
'jsonrpc': '2.0',
'id': fakeRequest.id,
'result': fakeRequest.jsonResponse ?? <String, Object>{'type': 'Success'},
}));
_applyStreamListen();
});
}
final List<VmServiceExpectation> _requests;
final StreamController<String> _input = StreamController<String>();
final StreamController<String> _output = StreamController<String>();
vm_service.VmService get vmService => _vmService;
vm_service.VmService _vmService;
bool get hasRemainingExpectations => _requests.isNotEmpty;
// remove FakeStreamResponse objects from _requests until it is empty
// or until we hit a FakeRequest
void _applyStreamListen() {
while (_requests.isNotEmpty && !_requests.first.isRequest) {
final FakeVmServiceStreamResponse response = _requests.removeAt(0) as FakeVmServiceStreamResponse;
_input.add(json.encode(<String, Object>{
'jsonrpc': '2.0',
'method': 'streamNotify',
'params': <String, Object>{
'streamId': response.streamId,
'event': response.event.toJson(),
},
}));
}
}
}
abstract class VmServiceExpectation {
bool get isRequest;
}
class FakeVmServiceRequest implements VmServiceExpectation {
const FakeVmServiceRequest({
@required this.method,
@required this.id,
@required this.params,
this.jsonResponse,
});
final String method;
final String id;
final Map<String, Object> params;
final Map<String, Object> jsonResponse;
@override
bool get isRequest => true;
}
class FakeVmServiceStreamResponse implements VmServiceExpectation {
const FakeVmServiceStreamResponse({
@required this.event,
@required this.streamId,
});
final vm_service.Event event;
final String streamId;
@override
bool get isRequest => false;
}
class MockDevice extends Mock implements Device {} class MockDevice extends Mock implements Device {}
class MockVMService extends Mock implements vm_service.VmService {} class MockVMService extends Mock implements vm_service.VmService {}
class MockFlutterVersion extends Mock implements FlutterVersion { class MockFlutterVersion extends Mock implements FlutterVersion {
......
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
import 'dart:async'; import 'dart:async';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/commands/create.dart'; import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
...@@ -217,3 +219,109 @@ class NoContext implements AppContext { ...@@ -217,3 +219,109 @@ class NoContext implements AppContext {
return body(); return body();
} }
} }
/// A fake implementation of a vm_service that mocks the JSON-RPC request
/// and response structure.
class FakeVmServiceHost {
FakeVmServiceHost({
@required List<VmServiceExpectation> requests,
}) : _requests = requests {
_vmService = vm_service.VmService(
_input.stream,
_output.add,
);
_applyStreamListen();
_output.stream.listen((String data) {
final Map<String, Object> request = json.decode(data) as Map<String, Object>;
if (_requests.isEmpty) {
throw Exception('Unexpected request: $request');
}
final FakeVmServiceRequest fakeRequest = _requests.removeAt(0) as FakeVmServiceRequest;
expect(request, isA<Map<String, Object>>()
.having((Map<String, Object> request) => request['method'], 'method', fakeRequest.method)
.having((Map<String, Object> request) => request['id'], 'id', fakeRequest.id)
.having((Map<String, Object> request) => request['params'], 'args', fakeRequest.args)
);
if (fakeRequest.errorCode == null) {
_input.add(json.encode(<String, Object>{
'jsonrpc': '2.0',
'id': fakeRequest.id,
'result': fakeRequest.jsonResponse ?? <String, Object>{'type': 'Success'},
}));
} else {
_input.add(json.encode(<String, Object>{
'jsonrpc': '2.0',
'id': fakeRequest.id,
'error': <String, Object>{
'code': fakeRequest.errorCode,
}
}));
}
_applyStreamListen();
});
}
final List<VmServiceExpectation> _requests;
final StreamController<String> _input = StreamController<String>();
final StreamController<String> _output = StreamController<String>();
vm_service.VmService get vmService => _vmService;
vm_service.VmService _vmService;
bool get hasRemainingExpectations => _requests.isNotEmpty;
// remove FakeStreamResponse objects from _requests until it is empty
// or until we hit a FakeRequest
void _applyStreamListen() {
while (_requests.isNotEmpty && !_requests.first.isRequest) {
final FakeVmServiceStreamResponse response = _requests.removeAt(0) as FakeVmServiceStreamResponse;
_input.add(json.encode(<String, Object>{
'jsonrpc': '2.0',
'method': 'streamNotify',
'params': <String, Object>{
'streamId': response.streamId,
'event': response.event.toJson(),
},
}));
}
}
}
abstract class VmServiceExpectation {
bool get isRequest;
}
class FakeVmServiceRequest implements VmServiceExpectation {
const FakeVmServiceRequest({
@required this.method,
@required this.id,
@required this.args,
this.jsonResponse,
this.errorCode,
});
final String method;
final String id;
/// If non-null, the error code for a [vm_service.RPCError] in place of a
/// standard response.
final int errorCode;
final Map<String, Object> args;
final Map<String, Object> jsonResponse;
@override
bool get isRequest => true;
}
class FakeVmServiceStreamResponse implements VmServiceExpectation {
const FakeVmServiceStreamResponse({
@required this.event,
@required this.streamId,
});
final vm_service.Event event;
final String streamId;
@override
bool get isRequest => false;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment