Unverified Commit 48518888 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] Migrate to package:vm_service 4: trigonometric boogaloo (#54132)

parent fc06d3fe
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vmservice;
import 'asset.dart'; import 'asset.dart';
import 'base/context.dart'; import 'base/context.dart';
...@@ -439,7 +439,7 @@ class DevFS { ...@@ -439,7 +439,7 @@ class DevFS {
globals.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)'); globals.printTrace('DevFS: Creating new filesystem on the device ($_baseUri)');
try { try {
_baseUri = await _operations.create(fsName); _baseUri = await _operations.create(fsName);
} on rpc.RpcException catch (rpcException) { } on vmservice.RPCError catch (rpcException) {
// 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h // 1001 is kFileSystemAlreadyExists in //dart/runtime/vm/json_stream.h
if (rpcException.code != 1001) { if (rpcException.code != 1001) {
rethrow; rethrow;
......
...@@ -620,7 +620,7 @@ class FuchsiaDevice extends Device { ...@@ -620,7 +620,7 @@ class FuchsiaDevice extends Device {
// loopback (::1). // loopback (::1).
final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$port'); final Uri uri = Uri.parse('http://[$_ipv6Loopback]:$port');
final VMService vmService = await VMService.connect(uri); final VMService vmService = await VMService.connect(uri);
await vmService.getVM(); await vmService.getVMOld();
await vmService.refreshViews(); await vmService.refreshViews();
for (final FlutterView flutterView in vmService.vm.views) { for (final FlutterView flutterView in vmService.vm.views) {
if (flutterView.uiIsolate == null) { if (flutterView.uiIsolate == null) {
...@@ -717,7 +717,7 @@ class FuchsiaIsolateDiscoveryProtocol { ...@@ -717,7 +717,7 @@ class FuchsiaIsolateDiscoveryProtocol {
continue; continue;
} }
} }
await service.getVM(); await service.getVMOld();
await service.refreshViews(); await service.refreshViews();
for (final FlutterView flutterView in service.vm.views) { for (final FlutterView flutterView in service.vm.views) {
if (flutterView.uiIsolate == null) { if (flutterView.uiIsolate == null) {
......
...@@ -7,11 +7,11 @@ import 'dart:math' as math; ...@@ -7,11 +7,11 @@ import 'dart:math' as math;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
...@@ -514,7 +514,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -514,7 +514,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
// and "Flutter". The regex tries to strike a balance between not producing // and "Flutter". The regex tries to strike a balance between not producing
// false positives and not producing false negatives. // false positives and not producing false negatives.
_anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: '); _anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: ');
_loggingSubscriptions = <StreamSubscription<ServiceEvent>>[]; _loggingSubscriptions = <StreamSubscription<void>>[];
} }
/// Create a new [IOSDeviceLogReader]. /// Create a new [IOSDeviceLogReader].
...@@ -554,7 +554,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -554,7 +554,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
RegExp _anyLineRegex; RegExp _anyLineRegex;
StreamController<String> _linesController; StreamController<String> _linesController;
List<StreamSubscription<ServiceEvent>> _loggingSubscriptions; List<StreamSubscription<void>> _loggingSubscriptions;
@override @override
Stream<String> get logLines => _linesController.stream; Stream<String> get logLines => _linesController.stream;
...@@ -575,18 +575,20 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -575,18 +575,20 @@ class IOSDeviceLogReader extends DeviceLogReader {
if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) {
return; return;
} }
// The VM service will not publish logging events unless the debug stream is being listened to. try {
// onDebugEvent listens to this stream as a side effect. await connectedVmService.streamListen('Stdout');
unawaited(connectedVmService.onDebugEvent); } on vm_service.RPCError {
_loggingSubscriptions.add((await connectedVmService.onStdoutEvent).listen((ServiceEvent event) { // Do nothing, since the tool is already subscribed.
final String logMessage = event.message; }
if (logMessage.isNotEmpty) { _loggingSubscriptions.add(connectedVmService.onStdoutEvent.listen((vm_service.Event event) {
_linesController.add(logMessage); final String message = utf8.decode(base64.decode(event.bytes));
if (message.isNotEmpty) {
_linesController.add(message);
} }
})); }));
} }
void _listenToSysLog () { void _listenToSysLog() {
// syslog is not written on iOS 13+. // syslog is not written on iOS 13+.
if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) {
return; return;
...@@ -641,7 +643,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -641,7 +643,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
@override @override
void dispose() { void dispose() {
for (final StreamSubscription<ServiceEvent> loggingSubscription in _loggingSubscriptions) { for (final StreamSubscription<void> loggingSubscription in _loggingSubscriptions) {
loggingSubscription.cancel(); loggingSubscription.cancel();
} }
_idevicesyslogProcess?.kill(); _idevicesyslogProcess?.kill();
......
...@@ -234,7 +234,7 @@ class FlutterDevice { ...@@ -234,7 +234,7 @@ class FlutterDevice {
: vmService.vm.views).toList(); : vmService.vm.views).toList();
} }
Future<void> getVMs() => vmService.getVM(); Future<void> getVMs() => vmService.getVMOld();
Future<void> exitApps() async { Future<void> exitApps() async {
if (!device.supportsFlutterExit) { if (!device.supportsFlutterExit) {
......
...@@ -3,10 +3,8 @@ ...@@ -3,10 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pool/pool.dart'; import 'package:pool/pool.dart';
import 'base/async_guard.dart'; import 'base/async_guard.dart';
...@@ -110,9 +108,10 @@ class HotRunner extends ResidentRunner { ...@@ -110,9 +108,10 @@ class HotRunner extends ResidentRunner {
// TODO(cbernaschina): check that isolateId is the id of the UI isolate. // TODO(cbernaschina): check that isolateId is the id of the UI isolate.
final OperationResult result = await restart(pause: pause); final OperationResult result = await restart(pause: pause);
if (!result.isOk) { if (!result.isOk) {
throw rpc.RpcException( throw vm_service.RPCError(
rpc_error_code.INTERNAL_ERROR,
'Unable to reload sources', 'Unable to reload sources',
RPCErrorCodes.kInternalError,
'',
); );
} }
} }
...@@ -121,9 +120,10 @@ class HotRunner extends ResidentRunner { ...@@ -121,9 +120,10 @@ class HotRunner extends ResidentRunner {
final OperationResult result = final OperationResult result =
await restart(fullRestart: true, pause: pause); await restart(fullRestart: true, pause: pause);
if (!result.isOk) { if (!result.isOk) {
throw rpc.RpcException( throw vm_service.RPCError(
rpc_error_code.INTERNAL_ERROR,
'Unable to restart', 'Unable to restart',
RPCErrorCodes.kInternalError,
'',
); );
} }
} }
...@@ -564,16 +564,15 @@ class HotRunner extends ResidentRunner { ...@@ -564,16 +564,15 @@ class HotRunner extends ResidentRunner {
if (benchmarkMode) { if (benchmarkMode) {
final List<Future<void>> isolateNotifications = <Future<void>>[]; final List<Future<void>> isolateNotifications = <Future<void>>[];
for (final FlutterDevice device in flutterDevices) { for (final FlutterDevice device in flutterDevices) {
try {
await device.vmService.streamListen('Isolate');
} on vm_service.RPCError {
// Do nothing, we're already subcribed.
}
for (final FlutterView view in device.views) { for (final FlutterView view in device.views) {
isolateNotifications.add( isolateNotifications.add(
view.owner.vm.vmService.onIsolateEvent view.owner.vm.vmService.onIsolateEvent.firstWhere((vm_service.Event event) {
.then((Stream<ServiceEvent> serviceEvents) async { return event.kind == vm_service.EventKind.kServiceExtensionAdded;
await for (final ServiceEvent serviceEvent in serviceEvents) {
if (serviceEvent.owner.name.contains('_spawn')
&& serviceEvent.kind == ServiceEvent.kIsolateExit) {
return;
}
}
}), }),
); );
} }
...@@ -720,9 +719,12 @@ class HotRunner extends ResidentRunner { ...@@ -720,9 +719,12 @@ class HotRunner extends ResidentRunner {
if (!result.isOk) { if (!result.isOk) {
restartEvent = 'restart-failed'; restartEvent = 'restart-failed';
} }
} on rpc.RpcException { } on vm_service.SentinelException catch (err, st) {
restartEvent = 'exception';
return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
} on vm_service.RPCError catch (err, st) {
restartEvent = 'exception'; restartEvent = 'exception';
return OperationResult(1, 'hot restart failed to complete', fatal: true); return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
} finally { } finally {
HotEvent(restartEvent, HotEvent(restartEvent,
targetPlatform: targetPlatform, targetPlatform: targetPlatform,
...@@ -764,7 +766,7 @@ class HotRunner extends ResidentRunner { ...@@ -764,7 +766,7 @@ class HotRunner extends ResidentRunner {
); );
}, },
); );
} on rpc.RpcException { } on vm_service.RPCError {
HotEvent('exception', HotEvent('exception',
targetPlatform: targetPlatform, targetPlatform: targetPlatform,
sdkName: sdkName, sdkName: sdkName,
......
...@@ -189,7 +189,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libra ...@@ -189,7 +189,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libra
Future<VMService> Function(Uri) connector = _defaultConnect, Future<VMService> Function(Uri) connector = _defaultConnect,
}) async { }) async {
final VMService vmService = await connector(serviceUri); final VMService vmService = await connector(serviceUri);
await vmService.getVM(); await vmService.getVMOld();
final Map<String, dynamic> result = await _getAllCoverage( final Map<String, dynamic> result = await _getAllCoverage(
vmService, libraryPredicate); vmService, libraryPredicate);
await vmService.close(); await vmService.close();
...@@ -197,7 +197,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libra ...@@ -197,7 +197,7 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libra
} }
Future<Map<String, dynamic>> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async { Future<Map<String, dynamic>> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async {
await service.getVM(); await service.getVMOld();
final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[]; final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
for (final Isolate isolateRef in service.vm.isolates) { for (final Isolate isolateRef in service.vm.isolates) {
await isolateRef.load(); await isolateRef.load();
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'package:vm_service/vm_service.dart' as vm_service;
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart'; import 'base/logger.dart';
...@@ -45,7 +46,8 @@ class Tracing { ...@@ -45,7 +46,8 @@ class Tracing {
); );
try { try {
final Completer<void> whenFirstFrameRendered = Completer<void>(); final Completer<void> whenFirstFrameRendered = Completer<void>();
(await vmService.onExtensionEvent).listen((ServiceEvent event) { await vmService.streamListen('Extension');
vmService.onExtensionEvent.listen((vm_service.Event event) {
if (event.extensionKind == 'Flutter.FirstFrame') { if (event.extensionKind == 'Flutter.FirstFrame') {
whenFirstFrameRendered.complete(); whenFirstFrameRendered.complete();
} }
......
...@@ -5,18 +5,14 @@ ...@@ -5,18 +5,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:meta/meta.dart' show required; import 'package:meta/meta.dart' show required;
import 'package:stream_channel/stream_channel.dart'; import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/io.dart' as io; import 'base/io.dart' as io;
import 'base/utils.dart'; import 'base/utils.dart';
import 'convert.dart' show base64, utf8; import 'convert.dart' show base64, json, utf8;
import 'device.dart'; import 'device.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'version.dart'; import 'version.dart';
...@@ -25,10 +21,24 @@ import 'version.dart'; ...@@ -25,10 +21,24 @@ import 'version.dart';
/// for [WebSocket]s (used by tests). /// for [WebSocket]s (used by tests).
typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression}); typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression});
/// A function that opens a two-way communication channel to the specified [uri]. WebSocketConnector _openChannel = _defaultOpenChannel;
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri, {io.CompressionOptions compression});
_OpenChannel _openChannel = _defaultOpenChannel; /// The error codes for the JSON-RPC standard.
///
/// See also: https://www.jsonrpc.org/specification#error_object
abstract class RPCErrorCodes {
/// The method does not exist or is not available.
static const int kMethodNotFound = -32601;
/// Invalid method parameter(s), such as a mismatched type.
static const int kInvalidParams = -32602;
/// Internal JSON-RPC error.
static const int kInternalError = -32603;
/// Application specific error codes.s
static const int kServerError = -32000;
}
/// A function that reacts to the invocation of the 'reloadSources' service. /// A function that reacts to the invocation of the 'reloadSources' service.
/// ///
...@@ -64,7 +74,9 @@ typedef ReloadMethod = Future<void> Function({ ...@@ -64,7 +74,9 @@ typedef ReloadMethod = Future<void> Function({
String libraryId, String libraryId,
}); });
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async { Future<io.WebSocket> _defaultOpenChannel(String url, {
io.CompressionOptions compression = io.CompressionOptions.compressionDefault
}) async {
Duration delay = const Duration(milliseconds: 100); Duration delay = const Duration(milliseconds: 100);
int attempts = 0; int attempts = 0;
io.WebSocket socket; io.WebSocket socket;
...@@ -90,14 +102,14 @@ Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOption ...@@ -90,14 +102,14 @@ Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOption
while (socket == null) { while (socket == null) {
attempts += 1; attempts += 1;
try { try {
socket = await constructor(uri.toString(), compression: compression); socket = await constructor(url, compression: compression);
} on io.WebSocketException catch (e) { } on io.WebSocketException catch (e) {
await handleError(e); await handleError(e);
} on io.SocketException catch (e) { } on io.SocketException catch (e) {
await handleError(e); await handleError(e);
} }
} }
return IOWebSocketChannel(socket).cast<String>(); return socket;
} }
/// Override `VMServiceConnector` in [context] to return a different VMService /// Override `VMServiceConnector` in [context] to return a different VMService
...@@ -112,10 +124,10 @@ typedef VMServiceConnector = Future<VMService> Function(Uri httpUri, { ...@@ -112,10 +124,10 @@ typedef VMServiceConnector = Future<VMService> Function(Uri httpUri, {
}); });
/// A connection to the Dart VM Service. /// A connection to the Dart VM Service.
// TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031 ///
class VMService { /// This also implements the package:vm_service API to enable a gradual migration.
class VMService implements vm_service.VmService {
VMService( VMService(
this._peer,
this.httpAddress, this.httpAddress,
this.wsAddress, this.wsAddress,
ReloadSources reloadSources, ReloadSources reloadSources,
...@@ -123,38 +135,49 @@ class VMService { ...@@ -123,38 +135,49 @@ class VMService {
CompileExpression compileExpression, CompileExpression compileExpression,
Device device, Device device,
ReloadMethod reloadMethod, ReloadMethod reloadMethod,
this._delegateService,
this.streamClosedCompleter,
Stream<dynamic> secondary,
) { ) {
_vm = VM._empty(this); _vm = VM._empty(this);
_peer.listen().catchError(_connectionError.completeError);
_peer.registerMethod('streamNotify', (rpc.Parameters event) { // TODO(jonahwilliams): this is temporary to support the current vm_service
_handleStreamNotify(event.asMap.cast<String, dynamic>()); // semantics of update-in-place.
secondary.listen((dynamic rawData) {
final String message = rawData as String;
final dynamic map = json.decode(message);
if (map != null && map['method'] == 'streamNotify') {
_handleStreamNotify(map['params'] as Map<String, dynamic>);
}
}); });
if (reloadSources != null) { if (reloadSources != null) {
_peer.registerMethod('reloadSources', (rpc.Parameters params) async { _delegateService.registerServiceCallback('reloadSources', (Map<String, dynamic> params) async {
final String isolateId = params['isolateId'].value as String; final String isolateId = params['isolateId'].value as String;
final bool force = params.asMap['force'] as bool ?? false; final bool force = params['force'] as bool ?? false;
final bool pause = params.asMap['pause'] as bool ?? false; final bool pause = params['pause'] as bool ?? false;
if (isolateId.isEmpty) { if (isolateId.isEmpty) {
throw rpc.RpcException.invalidParams("Invalid 'isolateId': $isolateId"); throw vm_service.RPCError(
"Invalid 'isolateId': $isolateId",
RPCErrorCodes.kInvalidParams,
'',
);
} }
try { try {
await reloadSources(isolateId, force: force, pause: pause); await reloadSources(isolateId, force: force, pause: pause);
return <String, String>{'type': 'Success'}; return <String, String>{'type': 'Success'};
} on rpc.RpcException { } on vm_service.RPCError {
rethrow; rethrow;
} on Exception catch (e, st) { } on Exception catch (e, st) {
throw rpc.RpcException(rpc_error_code.SERVER_ERROR, throw vm_service.RPCError(
'Error during Sources Reload: $e\n$st'); 'Error during Sources Reload: $e\n$st',
RPCErrorCodes.kServerError,
'',
);
} }
}); });
_delegateService.registerService('reloadSources', 'Flutter Tools');
_peer.sendNotification('registerService', <String, String>{
'service': 'reloadSources',
'alias': 'Flutter Tools',
});
} }
...@@ -168,15 +191,23 @@ class VMService { ...@@ -168,15 +191,23 @@ class VMService {
// if the build method of a StatelessWidget is updated, this is the name of class. // if the build method of a StatelessWidget is updated, this is the name of class.
// If the build method of a StatefulWidget is updated, then this is the name // If the build method of a StatefulWidget is updated, then this is the name
// of the Widget class that created the State object. // of the Widget class that created the State object.
_peer.registerMethod('reloadMethod', (rpc.Parameters params) async { _delegateService.registerServiceCallback('reloadMethod', (Map<String, dynamic> params) async {
final String libraryId = params['library'].value as String; final String libraryId = params['library'] as String;
final String classId = params['class'].value as String; final String classId = params['class'] as String;
if (libraryId.isEmpty) { if (libraryId.isEmpty) {
throw rpc.RpcException.invalidParams("Invalid 'libraryId': $libraryId"); throw vm_service.RPCError(
"Invalid 'libraryId': $libraryId",
RPCErrorCodes.kInvalidParams,
'',
);
} }
if (classId.isEmpty) { if (classId.isEmpty) {
throw rpc.RpcException.invalidParams("Invalid 'classId': $classId"); throw vm_service.RPCError(
"Invalid 'classId': $classId",
RPCErrorCodes.kInvalidParams,
'',
);
} }
globals.printTrace('reloadMethod not yet supported, falling back to hot reload'); globals.printTrace('reloadMethod not yet supported, falling back to hot reload');
...@@ -187,111 +218,95 @@ class VMService { ...@@ -187,111 +218,95 @@ class VMService {
classId: classId, classId: classId,
); );
return <String, String>{'type': 'Success'}; return <String, String>{'type': 'Success'};
} on rpc.RpcException { } on vm_service.RPCError {
rethrow; rethrow;
} on Exception catch (e, st) { } on Exception catch (e, st) {
throw rpc.RpcException(rpc_error_code.SERVER_ERROR, throw vm_service.RPCError('Error during Sources Reload: $e\n$st', -32000, '');
'Error during Sources Reload: $e\n$st');
} }
}); });
_peer.sendNotification('registerService', <String, String>{ _delegateService.registerService('reloadMethod', 'Flutter Tools');
'service': 'reloadMethod',
'alias': 'Flutter Tools',
});
} }
if (restart != null) { if (restart != null) {
_peer.registerMethod('hotRestart', (rpc.Parameters params) async { _delegateService.registerServiceCallback('hotRestart', (Map<String, dynamic> params) async {
final bool pause = params.asMap['pause'] as bool ?? false; final bool pause = params['pause'] as bool ?? false;
if (pause is! bool) {
throw rpc.RpcException.invalidParams("Invalid 'pause': $pause");
}
try { try {
await restart(pause: pause); await restart(pause: pause);
return <String, String>{'type': 'Success'}; return <String, String>{'type': 'Success'};
} on rpc.RpcException { } on vm_service.RPCError {
rethrow; rethrow;
} on Exception catch (e, st) { } on Exception catch (e, st) {
throw rpc.RpcException(rpc_error_code.SERVER_ERROR, throw vm_service.RPCError(
'Error during Hot Restart: $e\n$st'); 'Error during Hot Restart: $e\n$st',
RPCErrorCodes.kServerError,
'',
);
} }
}); });
_delegateService.registerService('hotRestart', 'Flutter Tools');
_peer.sendNotification('registerService', <String, String>{
'service': 'hotRestart',
'alias': 'Flutter Tools',
});
} }
_peer.registerMethod('flutterVersion', (rpc.Parameters params) async { _delegateService.registerServiceCallback('flutterVersion', (Map<String, dynamic> params) async {
final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion(); final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion();
final Map<String, Object> versionJson = version.toJson(); final Map<String, Object> versionJson = version.toJson();
versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort;
versionJson['engineRevisionShort'] = version.engineRevisionShort; versionJson['engineRevisionShort'] = version.engineRevisionShort;
return versionJson; return versionJson;
}); });
_delegateService.registerService('flutterVersion', 'Flutter Tools');
_peer.sendNotification('registerService', <String, String>{
'service': 'flutterVersion',
'alias': 'Flutter Tools',
});
if (compileExpression != null) { if (compileExpression != null) {
_peer.registerMethod('compileExpression', (rpc.Parameters params) async { _delegateService.registerServiceCallback('compileExpression', (Map<String, dynamic> params) async {
final String isolateId = params['isolateId'].asString; final String isolateId = params['isolateId'] as String;
if (isolateId is! String || isolateId.isEmpty) { if (isolateId is! String || isolateId.isEmpty) {
throw rpc.RpcException.invalidParams( throw throw vm_service.RPCError(
"Invalid 'isolateId': $isolateId"); "Invalid 'isolateId': $isolateId",
RPCErrorCodes.kInvalidParams,
'',
);
} }
final String expression = params['expression'].asString; final String expression = params['expression'] as String;
if (expression is! String || expression.isEmpty) { if (expression is! String || expression.isEmpty) {
throw rpc.RpcException.invalidParams( throw throw vm_service.RPCError(
"Invalid 'expression': $expression"); "Invalid 'expression': $expression",
RPCErrorCodes.kInvalidParams,
'',
);
} }
final List<String> definitions = final List<String> definitions = List<String>.from(params['definitions'] as List<dynamic>);
List<String>.from(params['definitions'].asList); final List<String> typeDefinitions = List<String>.from(params['typeDefinitions'] as List<dynamic>);
final List<String> typeDefinitions = final String libraryUri = params['libraryUri'] as String;
List<String>.from(params['typeDefinitions'].asList); final String klass = params['klass'] as String;
final String libraryUri = params['libraryUri'].asString; final bool isStatic = params['isStatic'] as bool ?? false;
final String klass = params['klass'].exists ? params['klass'].asString : null;
final bool isStatic = params['isStatic'].asBoolOr(false);
try { try {
final String kernelBytesBase64 = await compileExpression(isolateId, final String kernelBytesBase64 = await compileExpression(isolateId,
expression, definitions, typeDefinitions, libraryUri, klass, expression, definitions, typeDefinitions, libraryUri, klass,
isStatic); isStatic);
return <String, dynamic>{'type': 'Success', return <String, dynamic>{
'result': <String, dynamic> {'kernelBytes': kernelBytesBase64}}; 'type': 'Success',
} on rpc.RpcException { 'result': <String, dynamic>{
'result': <String, dynamic>{'kernelBytes': kernelBytesBase64},
},
};
} on vm_service.RPCError {
rethrow; rethrow;
} on Exception catch (e, st) { } on Exception catch (e, st) {
throw rpc.RpcException(rpc_error_code.SERVER_ERROR, throw vm_service.RPCError(
'Error during expression compilation: $e\n$st'); 'Error during expression compilation: $e\n$st',
RPCErrorCodes.kServerError,
'',
);
} }
}); });
_delegateService.registerService('compileExpression', 'Flutter Tools');
_peer.sendNotification('registerService', <String, String>{
'service': 'compileExpression',
'alias': 'Flutter Tools',
});
} }
if (device != null) { if (device != null) {
_peer.registerMethod('flutterMemoryInfo', (rpc.Parameters params) async { _delegateService.registerServiceCallback('flutterMemoryInfo', (Map<String, dynamic> params) async {
final MemoryInfo result = await device.queryMemoryInfo(); final MemoryInfo result = await device.queryMemoryInfo();
return result.toJson(); return result.toJson();
}); });
_peer.sendNotification('registerService', <String, String>{ _delegateService.registerService('flutterMemoryInfo', 'Flutter Tools');
'service': 'flutterMemoryInfo',
'alias': 'Flutter Tools',
});
}
} }
static void _unhandledError(dynamic error, dynamic stack) {
globals.logger.printTrace('Error in internal implementation of JSON RPC.\n$error\n$stack');
assert(false);
} }
/// Connect to a Dart VM Service at [httpUri]. /// Connect to a Dart VM Service at [httpUri].
...@@ -332,10 +347,38 @@ class VMService { ...@@ -332,10 +347,38 @@ class VMService {
Device device, Device device,
}) async { }) async {
final Uri wsUri = httpUri.replace(scheme: 'ws', path: globals.fs.path.join(httpUri.path, 'ws')); final Uri wsUri = httpUri.replace(scheme: 'ws', path: globals.fs.path.join(httpUri.path, 'ws'));
final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression); final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression);
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError); final StreamController<dynamic> primary = StreamController<dynamic>();
final StreamController<dynamic> secondary = StreamController<dynamic>();
channel.listen((dynamic data) {
primary.add(data);
secondary.add(data);
}, onDone: () {
primary.close();
secondary.close();
}, onError: (dynamic error, StackTrace stackTrace) {
primary.addError(error, stackTrace);
secondary.addError(error, stackTrace);
});
// Create an instance of the package:vm_service API in addition to the flutter
// tool's to allow gradual migration.
final Completer<void> streamClosedCompleter = Completer<void>();
final vm_service.VmService delegateService = vm_service.VmService(
primary.stream,
channel.add,
log: null,
disposeHandler: () async {
if (!streamClosedCompleter.isCompleted) {
streamClosedCompleter.complete();
}
await channel.close();
},
);
final VMService service = VMService( final VMService service = VMService(
peer,
httpUri, httpUri,
wsUri, wsUri,
reloadSources, reloadSources,
...@@ -343,17 +386,21 @@ class VMService { ...@@ -343,17 +386,21 @@ class VMService {
compileExpression, compileExpression,
device, device,
reloadMethod, reloadMethod,
delegateService,
streamClosedCompleter,
secondary.stream,
); );
// This call is to ensure we are able to establish a connection instead of // This call is to ensure we are able to establish a connection instead of
// keeping on trucking and failing farther down the process. // keeping on trucking and failing farther down the process.
await service._sendRequest('getVersion', const <String, dynamic>{}); await delegateService.getVersion();
return service; return service;
} }
final vm_service.VmService _delegateService;
final Uri httpAddress; final Uri httpAddress;
final Uri wsAddress; final Uri wsAddress;
final rpc.Peer _peer; final Completer<void> streamClosedCompleter;
final Completer<Map<String, dynamic>> _connectionError = Completer<Map<String, dynamic>>();
VM _vm; VM _vm;
/// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects. /// The singleton [VM] object. Owns [Isolate] and [FlutterView] objects.
...@@ -362,46 +409,36 @@ class VMService { ...@@ -362,46 +409,36 @@ class VMService {
final Map<String, StreamController<ServiceEvent>> _eventControllers = final Map<String, StreamController<ServiceEvent>> _eventControllers =
<String, StreamController<ServiceEvent>>{}; <String, StreamController<ServiceEvent>>{};
final Set<String> _listeningFor = <String>{};
/// Whether our connection to the VM service has been closed; /// Whether our connection to the VM service has been closed;
bool get isClosed => _peer.isClosed; bool get isClosed => streamClosedCompleter.isCompleted;
Future<void> get done async { Future<void> get done async {
await _peer.done; return streamClosedCompleter.future;
} }
// Events @override
Future<Stream<ServiceEvent>> get onDebugEvent => onEvent('Debug'); Stream<vm_service.Event> get onDebugEvent => onEvent('Debug');
Future<Stream<ServiceEvent>> get onExtensionEvent => onEvent('Extension');
// IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, ServiceExtensionAdded
Future<Stream<ServiceEvent>> get onIsolateEvent => onEvent('Isolate');
Future<Stream<ServiceEvent>> get onTimelineEvent => onEvent('Timeline');
Future<Stream<ServiceEvent>> get onStdoutEvent => onEvent('Stdout'); // WriteEvent
// TODO(johnmccutchan): Add FlutterView events. @override
Stream<vm_service.Event> get onExtensionEvent => onEvent('Extension');
/// Returns a stream of VM service events. @override
/// Stream<vm_service.Event> get onIsolateEvent => onEvent('Isolate');
/// This purposely returns a `Future<Stream<T>>` rather than a `Stream<T>`
/// because it first registers with the VM to receive events on the stream, @override
/// and only once the VM has acknowledged that the stream has started will Stream<vm_service.Event> get onTimelineEvent => onEvent('Timeline');
/// we return the associated stream. Any attempt to streamline this API into
/// returning `Stream<T>` should take that into account to avoid race @override
/// conditions. Stream<vm_service.Event> get onStdoutEvent => onEvent('Stdout');
Future<Stream<ServiceEvent>> onEvent(String streamId) async {
await _streamListen(streamId); @override
return _getEventController(streamId).stream; Future<vm_service.Success> streamListen(String streamId) {
} return _delegateService.streamListen(streamId);
}
Future<Map<String, dynamic>> _sendRequest(
String method, @override
Map<String, dynamic> params, Stream<vm_service.Event> onEvent(String streamId) {
) { return _delegateService.onEvent(streamId);
return Future.any<Map<String, dynamic>>(<Future<Map<String, dynamic>>>[
_peer.sendRequest(method, params).then<Map<String, dynamic>>(castStringKeyedMap),
_connectionError.future,
]);
} }
StreamController<ServiceEvent> _getEventController(String eventName) { StreamController<ServiceEvent> _getEventController(String eventName) {
...@@ -441,19 +478,20 @@ class VMService { ...@@ -441,19 +478,20 @@ class VMService {
_getEventController(streamId).add(event); _getEventController(streamId).add(event);
} }
Future<void> _streamListen(String streamId) async {
if (!_listeningFor.contains(streamId)) {
_listeningFor.add(streamId);
await _sendRequest('streamListen', <String, dynamic>{'streamId': streamId});
}
}
/// Reloads the VM. /// Reloads the VM.
Future<void> getVM() async => await vm.reload(); Future<void> getVMOld() async => await vm.reload();
Future<void> refreshViews({ bool waitForViews = false }) => vm.refreshViews(waitForViews: waitForViews); Future<void> refreshViews({ bool waitForViews = false }) => vm.refreshViews(waitForViews: waitForViews);
Future<void> close() async => await _peer.close(); Future<void> close() async {
_delegateService?.dispose();
}
// To enable a gradual migration to package:vm_service
@override
dynamic noSuchMethod(Invocation invocation) {
throw UnsupportedError('${invocation.memberName} is not currently supported');
}
} }
/// An error that is thrown when constructing/updating a service object. /// An error that is thrown when constructing/updating a service object.
...@@ -936,36 +974,15 @@ class VM extends ServiceObjectOwner { ...@@ -936,36 +974,15 @@ class VM extends ServiceObjectOwner {
return Future<Isolate>.value(_isolateCache[isolateId]); return Future<Isolate>.value(_isolateCache[isolateId]);
} }
static String _truncate(String message, int width, String ellipsis) {
assert(ellipsis.length < width);
if (message.length <= width) {
return message;
}
return message.substring(0, width - ellipsis.length) + ellipsis;
}
/// Invoke the RPC and return the raw response. /// Invoke the RPC and return the raw response.
Future<Map<String, dynamic>> invokeRpcRaw( Future<Map<String, dynamic>> invokeRpcRaw(
String method, { String method, {
Map<String, dynamic> params = const <String, dynamic>{}, Map<String, dynamic> params = const <String, dynamic>{},
bool truncateLogs = true, bool truncateLogs = true,
}) async { }) async {
globals.printTrace('Sending to VM service: $method($params)'); final vm_service.Response response = await _vmService
assert(params != null); ._delegateService.callServiceExtension(method, args: params);
try { return response.json;
final Map<String, dynamic> result = await _vmService._sendRequest(method, params);
final String resultString =
truncateLogs ? _truncate(result.toString(), 250, '...') : result.toString();
globals.printTrace('Result: $resultString');
return result;
} on WebSocketChannelException catch (error) {
throwToolExit('Error connecting to observatory: $error');
return null;
} on rpc.RpcException catch (error) {
globals.printError('Error ${error.code} received from application: ${error.message}');
globals.printTrace('${error.data}');
rethrow;
}
} }
/// Invoke the RPC and return a [ServiceObject] response. /// Invoke the RPC and return a [ServiceObject] response.
...@@ -1264,13 +1281,17 @@ class Isolate extends ServiceObjectOwner { ...@@ -1264,13 +1281,17 @@ class Isolate extends ServiceObjectOwner {
} }
final Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments); final Map<String, dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments);
return response; return response;
} on rpc.RpcException catch (e) { } on vm_service.RPCError catch (e) {
return Future<Map<String, dynamic>>.value(<String, dynamic>{ return Future<Map<String, dynamic>>.value(<String, dynamic>{
'code': e.code, 'code': e.code,
'message': e.message, 'message': e.message,
'data': e.data, 'data': e.data,
}); });
} on vm_service.SentinelException catch (e) {
throwToolExit('Unexpected Sentinel while reloading sources: $e');
} }
assert(false);
return null;
} }
Future<Map<String, dynamic>> getObject(Map<String, dynamic> objectRef) { Future<Map<String, dynamic>> getObject(Map<String, dynamic> objectRef) {
...@@ -1293,9 +1314,9 @@ class Isolate extends ServiceObjectOwner { ...@@ -1293,9 +1314,9 @@ class Isolate extends ServiceObjectOwner {
}) async { }) async {
try { try {
return await invokeRpcRaw(method, params: params); return await invokeRpcRaw(method, params: params);
} on rpc.RpcException catch (e) { } on vm_service.RPCError catch (err) {
// If an application is not using the framework // If an application is not using the framework
if (e.code == rpc_error_code.METHOD_NOT_FOUND) { if (err.code == RPCErrorCodes.kMethodNotFound) {
return null; return null;
} }
rethrow; rethrow;
...@@ -1491,10 +1512,13 @@ class FlutterView extends ServiceObject { ...@@ -1491,10 +1512,13 @@ class FlutterView extends ServiceObject {
final String viewId = id; final String viewId = id;
// When this completer completes the isolate is running. // When this completer completes the isolate is running.
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
final StreamSubscription<ServiceEvent> subscription = try {
(await owner.vm.vmService.onIsolateEvent).listen((ServiceEvent event) { await owner.vm.vmService.streamListen('Isolate');
// TODO(johnmccutchan): Listen to the debug stream and catch initial } on vm_service.RPCError {
// launch errors. // Do nothing, since the tool is already subscribed.
}
final StreamSubscription<vm_service.Event> subscription =
owner.vm.vmService.onIsolateEvent.listen((vm_service.Event event) {
if (event.kind == ServiceEvent.kIsolateRunnable) { if (event.kind == ServiceEvent.kIsolateRunnable) {
globals.printTrace('Isolate is runnable.'); globals.printTrace('Isolate is runnable.');
if (!completer.isCompleted) { if (!completer.isCompleted) {
......
...@@ -19,7 +19,6 @@ dependencies: ...@@ -19,7 +19,6 @@ dependencies:
flutter_template_images: 1.0.1 flutter_template_images: 1.0.1
http: 0.12.0+4 http: 0.12.0+4
intl: 0.16.1 intl: 0.16.1
json_rpc_2: 2.1.0
meta: 1.1.8 meta: 1.1.8
multicast_dns: 0.2.2 multicast_dns: 0.2.2
mustache_template: 1.0.0+1 mustache_template: 1.0.0+1
...@@ -28,10 +27,8 @@ dependencies: ...@@ -28,10 +27,8 @@ dependencies:
process: 3.0.12 process: 3.0.12
quiver: 2.1.3 quiver: 2.1.3
stack_trace: 1.9.3 stack_trace: 1.9.3
stream_channel: 2.0.0
usage: 3.4.1 usage: 3.4.1
webdriver: 2.1.2 webdriver: 2.1.2
web_socket_channel: 1.1.0
webkit_inspection_protocol: 0.5.0+1 webkit_inspection_protocol: 0.5.0+1
xml: 3.7.0 xml: 3.7.0
yaml: 2.2.0 yaml: 2.2.0
...@@ -88,6 +85,7 @@ dependencies: ...@@ -88,6 +85,7 @@ dependencies:
source_maps: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_maps: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_span: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
sse: 3.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sse: 3.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stream_transform: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_transform: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -95,6 +93,7 @@ dependencies: ...@@ -95,6 +93,7 @@ dependencies:
typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
uuid: 2.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" uuid: 2.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
watcher: 0.9.7+14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" watcher: 0.9.7+14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies: dev_dependencies:
collection: 1.14.12 collection: 1.14.12
...@@ -112,4 +111,4 @@ dartdoc: ...@@ -112,4 +111,4 @@ dartdoc:
# Exclude this package from the hosted API docs. # Exclude this package from the hosted API docs.
nodoc: true nodoc: true
# PUBSPEC CHECKSUM: 8ac6 # PUBSPEC CHECKSUM: 7689
...@@ -15,8 +15,8 @@ import 'package:flutter_tools/src/base/os.dart'; ...@@ -15,8 +15,8 @@ import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -403,7 +403,7 @@ class MockVM implements VM { ...@@ -403,7 +403,7 @@ class MockVM implements VM {
Future<Map<String, dynamic>> createDevFS(String fsName) async { Future<Map<String, dynamic>> createDevFS(String fsName) async {
_service.messages.add('create $fsName'); _service.messages.add('create $fsName');
if (_devFSExists) { if (_devFSExists) {
throw rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists'); throw vm_service.RPCError('File system already exists', kFileSystemAlreadyExists, '');
} }
_devFSExists = true; _devFSExists = true;
return <String, dynamic>{'uri': '$_baseUri'}; return <String, dynamic>{'uri': '$_baseUri'};
......
...@@ -596,7 +596,7 @@ void main() { ...@@ -596,7 +596,7 @@ void main() {
.thenAnswer((Invocation invocation) async => <int>[1]); .thenAnswer((Invocation invocation) async => <int>[1]);
when(portForwarder.forward(1)) when(portForwarder.forward(1))
.thenAnswer((Invocation invocation) async => 2); .thenAnswer((Invocation invocation) async => 2);
when(vmService.getVM()) when(vmService.getVMOld())
.thenAnswer((Invocation invocation) => Future<void>.value(null)); .thenAnswer((Invocation invocation) => Future<void>.value(null));
when(vmService.refreshViews()) when(vmService.refreshViews())
.thenAnswer((Invocation invocation) => Future<void>.value(null)); .thenAnswer((Invocation invocation) => Future<void>.value(null));
......
...@@ -2,13 +2,18 @@ ...@@ -2,13 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
...@@ -137,6 +142,39 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -137,6 +142,39 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
' with a non-Flutter log message following it.', ' with a non-Flutter log message following it.',
]); ]);
}); });
testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async {
final MockVmService vmService = MockVmService();
final DeviceLogReader logReader = IOSDeviceLogReader.test(
useSyslog: false,
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
);
final StreamController<Event> controller = StreamController<Event>();
final Completer<Success> stdoutCompleter = Completer<Success>();
when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) {
return stdoutCompleter.future;
});
when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) {
return controller.stream;
});
logReader.connectedVMService = vmService;
stdoutCompleter.complete(Success());
controller.add(Event(
kind: 'Stdout',
timestamp: 0,
bytes: base64.encode(utf8.encode(' This is a message ')),
));
// Wait for stream listeners to fire.
await expectLater(logReader.logLines, emits(' This is a message '));
});
} }
class MockArtifacts extends Mock implements Artifacts {} class MockArtifacts extends Mock implements Artifacts {}
class MockVmService extends Mock implements VMService, VmService {}
...@@ -24,8 +24,8 @@ import 'package:flutter_tools/src/resident_runner.dart'; ...@@ -24,8 +24,8 @@ import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_cold.dart'; import 'package:flutter_tools/src/run_cold.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:json_rpc_2/json_rpc_2.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
...@@ -224,7 +224,7 @@ void main() { ...@@ -224,7 +224,7 @@ void main() {
pathToReload: anyNamed('pathToReload'), pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'), invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'), dillOutputPath: anyNamed('dillOutputPath'),
)).thenThrow(RpcException(666, 'something bad happened')); )).thenThrow(vm_service.RPCError('something bad happened', 666, ''));
final OperationResult result = await residentRunner.restart(fullRestart: false); final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, true); expect(result.fatal, true);
...@@ -329,7 +329,7 @@ void main() { ...@@ -329,7 +329,7 @@ void main() {
pathToReload: anyNamed('pathToReload'), pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'), invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'), dillOutputPath: anyNamed('dillOutputPath'),
)).thenThrow(RpcException(666, 'something bad happened')); )).thenThrow(vm_service.RPCError('something bad happened', 666, ''));
final OperationResult result = await residentRunner.restart(fullRestart: true); final OperationResult result = await residentRunner.restart(fullRestart: true);
expect(result.fatal, true); expect(result.fatal, true);
......
...@@ -3,95 +3,18 @@ ...@@ -3,95 +3,18 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter_tools/src/base/io.dart'; import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:mockito/mockito.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vmservice.dart'; import 'package:flutter_tools/src/vmservice.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:quiver/testing/async.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/mocks.dart';
class MockPeer implements rpc.Peer {
Function _versionFn = (dynamic _) => null;
@override
rpc.ErrorCallback get onUnhandledError => null;
@override
Future<dynamic> get done async {
throw 'unexpected call to done';
}
@override
bool get isClosed => _isClosed;
@override
Future<dynamic> close() async {
_isClosed = true;
}
@override
Future<dynamic> listen() async {
// this does get called
}
@override
void registerFallback(dynamic callback(rpc.Parameters parameters)) {
throw 'unexpected call to registerFallback';
}
@override
void registerMethod(String name, Function callback) {
registeredMethods.add(name);
if (name == 'flutterVersion') {
_versionFn = callback;
}
}
@override
void sendNotification(String method, [ dynamic parameters ]) {
// this does get called
sentNotifications.putIfAbsent(method, () => <dynamic>[]).add(parameters);
}
Map<String, List<dynamic>> sentNotifications = <String, List<dynamic>>{};
List<String> registeredMethods = <String>[];
bool isolatesEnabled = false;
bool _isClosed = false;
Future<void> _getVMLatch;
Completer<void> _currentGetVMLatchCompleter;
void tripGetVMLatch() {
final Completer<void> lastCompleter = _currentGetVMLatchCompleter;
_currentGetVMLatchCompleter = Completer<void>();
_getVMLatch = _currentGetVMLatchCompleter.future;
lastCompleter?.complete();
}
int returnedFromSendRequest = 0; final Map<String, Object> vm = <String, dynamic>{
@override
Future<dynamic> sendRequest(String method, [ dynamic parameters ]) async {
if (method == 'getVM') {
await _getVMLatch;
}
await Future<void>.delayed(Duration.zero);
returnedFromSendRequest += 1;
if (method == 'getVM') {
return <String, dynamic>{
'type': 'VM', 'type': 'VM',
'name': 'vm', 'name': 'vm',
'architectureBits': 64, 'architectureBits': 64,
...@@ -105,7 +28,7 @@ class MockPeer implements rpc.Peer { ...@@ -105,7 +28,7 @@ class MockPeer implements rpc.Peer {
'_embedder': 'Flutter', '_embedder': 'Flutter',
'_maxRSS': 312614912, '_maxRSS': 312614912,
'_currentRSS': 33091584, '_currentRSS': 33091584,
'isolates': isolatesEnabled ? <dynamic>[ 'isolates': <dynamic>[
<String, dynamic>{ <String, dynamic>{
'type': '@Isolate', 'type': '@Isolate',
'fixedId': true, 'fixedId': true,
...@@ -113,11 +36,11 @@ class MockPeer implements rpc.Peer { ...@@ -113,11 +36,11 @@ class MockPeer implements rpc.Peer {
'name': 'main.dart:main()', 'name': 'main.dart:main()',
'number': 242098474, 'number': 242098474,
}, },
] : <dynamic>[], ],
}; };
}
if (method == 'getIsolate') { final vm_service.Isolate isolate = vm_service.Isolate.parse(
return <String, dynamic>{ <String, dynamic>{
'type': 'Isolate', 'type': 'Isolate',
'fixedId': true, 'fixedId': true,
'id': 'isolates/242098474', 'id': 'isolates/242098474',
...@@ -143,12 +66,12 @@ class MockPeer implements rpc.Peer { ...@@ -143,12 +66,12 @@ class MockPeer implements rpc.Peer {
'avgCollectionPeriodMillis': 0.0, 'avgCollectionPeriodMillis': 0.0,
}, },
}, },
};
} }
if (method == '_flutter.listViews') { );
return <String, dynamic>{
final Map<String, Object> listViews = <String, dynamic>{
'type': 'FlutterViewList', 'type': 'FlutterViewList',
'views': isolatesEnabled ? <dynamic>[ 'views': <dynamic>[
<String, dynamic>{ <String, dynamic>{
'type': 'FlutterView', 'type': 'FlutterView',
'id': '_flutterView/0x4a4c1f8', 'id': '_flutterView/0x4a4c1f8',
...@@ -160,205 +83,138 @@ class MockPeer implements rpc.Peer { ...@@ -160,205 +83,138 @@ class MockPeer implements rpc.Peer {
'number': 242098474, 'number': 242098474,
}, },
}, },
] : <dynamic>[], ]
}; };
}
if (method == 'flutterVersion') {
return _versionFn(parameters);
}
return null;
}
@override typedef ServiceCallback = Future<Map<String, dynamic>> Function(Map<String, Object>);
dynamic withBatch(dynamic callback()) {
throw 'unexpected call to withBatch';
}
}
void main() { void main() {
MockStdio mockStdio; testUsingContext('VMService can refreshViews', () async {
final MockFlutterVersion mockVersion = MockFlutterVersion(); final MockVMService mockVmService = MockVMService();
group('VMService', () { final VMService vmService = VMService(
null,
setUp(() { null,
mockStdio = MockStdio(); null,
null,
null,
null,
null,
mockVmService,
Completer<void>(),
const Stream<dynamic>.empty(),
);
verify(mockVmService.registerService('flutterVersion', 'Flutter Tools')).called(1);
when(mockVmService.callServiceExtension('getVM',
args: anyNamed('args'), // Empty
isolateId: null
)).thenAnswer((Invocation invocation) async {
return vm_service.Response.parse(vm);
}); });
await vmService.getVMOld();
testUsingContext('fails connection eagerly in the connect() method', () async {
FakeAsync().run((FakeAsync time) {
bool failed = false;
final Future<VMService> future = VMService.connect(Uri.parse('http://host.invalid:9999/'));
future.whenComplete(() {
failed = true;
});
time.elapse(const Duration(seconds: 5));
expect(failed, isFalse);
expect(mockStdio.writtenToStdout.join(''), '');
expect(mockStdio.writtenToStderr.join(''), '');
time.elapse(const Duration(seconds: 5));
expect(failed, isFalse);
expect(mockStdio.writtenToStdout.join(''), 'This is taking longer than expected...\n');
expect(mockStdio.writtenToStderr.join(''), '');
});
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(
outputPreferences: OutputPreferences.test(),
stdio: mockStdio,
terminal: AnsiTerminal(
stdio: mockStdio,
platform: const LocalPlatform(),
),
timeoutConfiguration: const TimeoutConfiguration(),
),
WebSocketConnector: () => (String url, {CompressionOptions compression}) async => throw const SocketException('test'),
});
testUsingContext('closing VMService closes Peer', () async { when(mockVmService.callServiceExtension('_flutter.listViews',
final MockPeer mockPeer = MockPeer(); args: anyNamed('args'),
final VMService vmService = VMService(mockPeer, null, null, null, null, null, MockDevice(), null); isolateId: anyNamed('isolateId')
expect(mockPeer.isClosed, equals(false)); )).thenAnswer((Invocation invocation) async {
await vmService.close(); return vm_service.Response.parse(listViews);
expect(mockPeer.isClosed, equals(true));
}); });
await vmService.refreshViews(waitForViews: true);
testUsingContext('refreshViews', () { expect(vmService.vm.name, 'vm');
FakeAsync().run((FakeAsync time) { expect(vmService.vm.views.single.id, '_flutterView/0x4a4c1f8');
bool done = false;
final MockPeer mockPeer = MockPeer();
expect(mockPeer.returnedFromSendRequest, 0);
final VMService vmService = VMService(mockPeer, null, null, null, null, null, null, null);
expect(mockPeer.sentNotifications, contains('registerService'));
final List<String> registeredServices =
mockPeer.sentNotifications['registerService']
.map((dynamic service) => (service as Map<String, String>)['service'])
.toList();
expect(registeredServices, contains('flutterVersion'));
vmService.getVM().then((void value) { done = true; });
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 0);
time.elapse(Duration.zero);
expect(done, isTrue);
expect(mockPeer.returnedFromSendRequest, 1);
done = false;
mockPeer.tripGetVMLatch(); // this blocks the upcoming getVM call
final Future<void> ready = vmService.refreshViews(waitForViews: true);
ready.then((void value) { done = true; });
expect(mockPeer.returnedFromSendRequest, 1);
time.elapse(Duration.zero); // this unblocks the listViews call which returns nothing
expect(mockPeer.returnedFromSendRequest, 2);
time.elapse(const Duration(milliseconds: 50)); // the last listViews had no views, so it waits 50ms, then calls getVM
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 2);
mockPeer.tripGetVMLatch(); // this unblocks the getVM call
expect(mockPeer.returnedFromSendRequest, 2);
time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
expect(mockPeer.returnedFromSendRequest, 4);
time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 4);
mockPeer.tripGetVMLatch(); // this unblocks the getVM call
expect(mockPeer.returnedFromSendRequest, 4);
time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
expect(mockPeer.returnedFromSendRequest, 6);
time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 6);
mockPeer.tripGetVMLatch(); // this unblocks the getVM call
expect(mockPeer.returnedFromSendRequest, 6);
time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
expect(mockPeer.returnedFromSendRequest, 8);
time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 8);
mockPeer.tripGetVMLatch(); // this unblocks the getVM call
expect(mockPeer.returnedFromSendRequest, 8);
time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
expect(mockPeer.returnedFromSendRequest, 10);
const String message = 'Flutter is taking longer than expected to report its views. Still trying...\n';
expect(mockStdio.writtenToStdout.join(''), message);
expect(mockStdio.writtenToStderr.join(''), '');
time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 10);
mockPeer.isolatesEnabled = true;
mockPeer.tripGetVMLatch(); // this unblocks the getVM call
expect(mockPeer.returnedFromSendRequest, 10);
time.elapse(Duration.zero); // now it returns an isolate and the listViews call returns views
expect(mockPeer.returnedFromSendRequest, 13);
expect(done, isTrue);
expect(mockStdio.writtenToStdout.join(''), message);
expect(mockStdio.writtenToStderr.join(''), '');
});
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Logger: () => StdoutLogger( Logger: () => BufferLogger.test()
outputPreferences: globals.outputPreferences,
terminal: AnsiTerminal(
stdio: mockStdio,
platform: const LocalPlatform(),
),
stdio: mockStdio,
timeoutConfiguration: const TimeoutConfiguration(),
),
}); });
testUsingContext('registers hot UI method', () { testUsingContext('VmService registers reloadSources', () {
FakeAsync().run((FakeAsync time) { Future<void> reloadSources(String isolateId, { bool pause, bool force}) async {}
final MockPeer mockPeer = MockPeer(); final MockVMService mockVMService = MockVMService();
Future<void> reloadMethod({ String classId, String libraryId }) async {} VMService(
VMService(mockPeer, null, null, null, null, null, null, reloadMethod); null,
null,
expect(mockPeer.registeredMethods, contains('reloadMethod')); reloadSources,
null,
null,
null,
null,
mockVMService,
Completer<void>(),
const Stream<dynamic>.empty(),
);
verify(mockVMService.registerService('reloadSources', 'Flutter Tools')).called(1);
}, overrides: <Type, Generator>{
Logger: () => BufferLogger.test()
}); });
testUsingContext('VmService registers reloadMethod', () {
Future<void> reloadMethod({ String classId, String libraryId,}) async {}
final MockVMService mockVMService = MockVMService();
VMService(
null,
null,
null,
null,
null,
null,
reloadMethod,
mockVMService,
Completer<void>(),
const Stream<dynamic>.empty(),
);
verify(mockVMService.registerService('reloadMethod', 'Flutter Tools')).called(1);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Logger: () => StdoutLogger( Logger: () => BufferLogger.test()
outputPreferences: globals.outputPreferences,
terminal: AnsiTerminal(
stdio: mockStdio,
platform: const LocalPlatform(),
),
stdio: mockStdio,
timeoutConfiguration: const TimeoutConfiguration(),
),
}); });
testUsingContext('registers flutterMemoryInfo service', () { testUsingContext('VmService registers flutterMemoryInfo service', () {
FakeAsync().run((FakeAsync time) {
final MockDevice mockDevice = MockDevice(); final MockDevice mockDevice = MockDevice();
final MockPeer mockPeer = MockPeer(); final MockVMService mockVMService = MockVMService();
Future<void> reloadSources(String isolateId, { bool pause, bool force}) async {} VMService(
VMService(mockPeer, null, null, reloadSources, null, null, mockDevice, null); null,
null,
expect(mockPeer.registeredMethods, contains('flutterMemoryInfo')); null,
}); null,
null,
mockDevice,
null,
mockVMService,
Completer<void>(),
const Stream<dynamic>.empty(),
);
verify(mockVMService.registerService('flutterMemoryInfo', 'Flutter Tools')).called(1);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Logger: () => StdoutLogger( Logger: () => BufferLogger.test()
outputPreferences: globals.outputPreferences,
terminal: AnsiTerminal(
stdio: mockStdio,
platform: const LocalPlatform(),
),
stdio: mockStdio,
timeoutConfiguration: const TimeoutConfiguration(),
),
}); });
testUsingContext('returns correct FlutterVersion', () { testUsingContext('VMService returns correct FlutterVersion', () async {
FakeAsync().run((FakeAsync time) async { final MockVMService mockVMService = MockVMService();
final MockPeer mockPeer = MockPeer(); VMService(
VMService(mockPeer, null, null, null, null, null, MockDevice(), null); null,
null,
expect(mockPeer.registeredMethods, contains('flutterVersion')); null,
expect(await mockPeer.sendRequest('flutterVersion'), equals(mockVersion.toJson())); null,
}); null,
null,
null,
mockVMService,
Completer<void>(),
const Stream<dynamic>.empty(),
);
verify(mockVMService.registerService('flutterVersion', 'Flutter Tools')).called(1);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FlutterVersion: () => mockVersion, FlutterVersion: () => MockFlutterVersion(),
});
}); });
} }
class MockDevice extends Mock implements Device {} class MockDevice extends Mock implements Device {}
class MockVMService extends Mock implements vm_service.VmService {}
class MockFlutterVersion extends Mock implements FlutterVersion { class MockFlutterVersion extends Mock implements FlutterVersion {
@override @override
Map<String, Object> toJson() => const <String, Object>{'Mock': 'Version'}; Map<String, Object> toJson() => const <String, Object>{'Mock': 'Version'};
......
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