Observe logging from VM service on iOS 13 (#43915)
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 8c2bf96..adb04a3 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart
@@ -23,6 +23,7 @@ import 'macos/macos_device.dart'; import 'project.dart'; import 'tester/flutter_tester.dart'; +import 'vmservice.dart'; import 'web/web_device.dart'; import 'windows/windows_device.dart'; @@ -618,6 +619,10 @@ /// A broadcast stream where each element in the string is a line of log output. Stream<String> get logLines; + /// Some logs can be obtained from a VM service stream. + /// Set this after the VM services are connected. + List<VMService> connectedVMServices; + @override String toString() => name; @@ -646,6 +651,9 @@ int appPid; @override + List<VMService> connectedVMServices; + + @override Stream<String> get logLines => const Stream<String>.empty(); @override
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index c8ee917..3fff840 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -8,6 +8,7 @@ import '../application_package.dart'; import '../artifacts.dart'; +import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -22,6 +23,7 @@ import '../project.dart'; import '../protocol_discovery.dart'; import '../reporting/reporting.dart'; +import '../vmservice.dart'; import 'code_signing.dart'; import 'ios_workflow.dart'; import 'mac.dart'; @@ -141,6 +143,12 @@ final String _sdkVersion; + /// May be 0 if version cannot be parsed. + int get majorSdkVersion { + final String majorVersionString = _sdkVersion?.split('.')?.first?.trim(); + return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0; + } + @override bool get supportsHotReload => true; @@ -529,7 +537,7 @@ class IOSDeviceLogReader extends DeviceLogReader { IOSDeviceLogReader(this.device, ApplicationPackage app) { _linesController = StreamController<String>.broadcast( - onListen: _start, + onListen: _listenToSysLog, onCancel: dispose, ); @@ -543,6 +551,7 @@ // and "Flutter". The regex tries to strike a balance between not producing // false positives and not producing false negatives. _anyLineRegex = RegExp(r'\w+(\([^)]*\))?\[\d+\] <[A-Za-z]+>: '); + _loggingSubscriptions = <StreamSubscription<ServiceEvent>>[]; } final IOSDevice device; @@ -553,6 +562,7 @@ RegExp _anyLineRegex; StreamController<String> _linesController; + List<StreamSubscription<ServiceEvent>> _loggingSubscriptions; @override Stream<String> get logLines => _linesController.stream; @@ -560,10 +570,44 @@ @override String get name => device.name; - void _start() { + @override + List<VMService> get connectedVMServices => _connectedVMServices; + List<VMService> _connectedVMServices; + + @override + set connectedVMServices(List<VMService> connectedVMServices) { + _listenToUnifiedLoggingEvents(connectedVMServices); + _connectedVMServices = connectedVMServices; + } + + static const int _minimumUniversalLoggingSdkVersion = 13; + + Future<void> _listenToUnifiedLoggingEvents(List<VMService> vmServices) async { + if (device.majorSdkVersion < _minimumUniversalLoggingSdkVersion) { + return; + } + for (VMService vmService in vmServices) { + // The VM service will not publish logging events unless the debug stream is being listened to. + // onDebugEvent listens to this stream as a side effect. + unawaited(vmService.onDebugEvent); + _loggingSubscriptions.add((await vmService.onStdoutEvent).listen((ServiceEvent event) { + final String logMessage = event.message; + if (logMessage.isNotEmpty) { + _linesController.add(logMessage); + } + })); + } + _connectedVMServices = connectedVMServices; + } + + void _listenToSysLog () { + // syslog is not written on iOS 13+. + if (device.majorSdkVersion >= _minimumUniversalLoggingSdkVersion) { + return; + } iMobileDevice.startLogger(device.id).then<void>((Process process) { - process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newLineHandler()); - process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newLineHandler()); + process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler()); + process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_newSyslogLineHandler()); process.exitCode.whenComplete(() { if (_linesController.hasListener) { _linesController.close(); @@ -584,7 +628,7 @@ // any specific prefix. To properly capture those, we enter "printing" mode // after matching a log line from the runner. When in printing mode, we print // all lines until we find the start of another log message (from any app). - Function _newLineHandler() { + Function _newSyslogLineHandler() { bool printing = false; return (String line) { @@ -611,6 +655,9 @@ @override void dispose() { + for (StreamSubscription<ServiceEvent> loggingSubscription in _loggingSubscriptions) { + loggingSubscription.cancel(); + } _idevicesyslogProcess?.kill(); } }
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index c48d2a6..0e465b7 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -163,6 +163,7 @@ printTrace('Successfully connected to service protocol: ${observatoryUris[i]}'); } vmServices = localVmServices; + device.getLogReader(app: package).connectedVMServices = vmServices; } Future<void> refreshViews() async {
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index a637edc..c324953 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -18,7 +18,7 @@ import 'base/file_system.dart'; import 'base/io.dart' as io; import 'base/utils.dart'; -import 'convert.dart' show base64; +import 'convert.dart' show base64, utf8; import 'globals.dart'; import 'version.dart'; import 'vmservice_record_replay.dart'; @@ -99,6 +99,10 @@ return IOWebSocketChannel(socket).cast<String>(); } +/// Override `VMServiceConnector` in [context] to return a different VMService +/// from [VMService.connect] (used by tests). +typedef VMServiceConnector = Future<VMService> Function(Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, io.CompressionOptions compression }); + /// A connection to the Dart VM Service. // TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031 class VMService { @@ -302,6 +306,17 @@ /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217 static Future<VMService> connect( Uri httpUri, { + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + io.CompressionOptions compression = io.CompressionOptions.compressionDefault, + }) async { + final VMServiceConnector connector = context.get<VMServiceConnector>() ?? VMService._connect; + return connector(httpUri, reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, compression: compression); + } + + static Future<VMService> _connect( + Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, @@ -344,6 +359,8 @@ // 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. /// Returns a stream of VM service events. @@ -643,6 +660,8 @@ Map<String, dynamic> get extensionData => _extensionData; List<Map<String, dynamic>> _timelineEvents; List<Map<String, dynamic>> get timelineEvents => _timelineEvents; + String _message; + String get message => _message; // The possible 'kind' values. static const String kVMUpdate = 'VMUpdate'; @@ -690,6 +709,11 @@ // on a Stream. final List<dynamic> dynamicList = map['timelineEvents']; _timelineEvents = dynamicList?.cast<Map<String, dynamic>>(); + + final String base64Bytes = map['bytes']; + if (base64Bytes != null) { + _message = utf8.decode(base64.decode(base64Bytes)).trim(); + } } bool get isPauseEvent {
diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index ed515ad..1bea916 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -68,6 +68,16 @@ Platform: () => macPlatform, }); + testUsingContext('parses major version', () { + expect(IOSDevice('device-123', sdkVersion: '1.0.0').majorSdkVersion, 1); + expect(IOSDevice('device-123', sdkVersion: '13.1.1').majorSdkVersion, 13); + expect(IOSDevice('device-123', sdkVersion: '10').majorSdkVersion, 10); + expect(IOSDevice('device-123', sdkVersion: '0').majorSdkVersion, 0); + expect(IOSDevice('device-123', sdkVersion: 'bogus').majorSdkVersion, 0); + }, overrides: <Type, Generator>{ + Platform: () => macPlatform, + }); + for (Platform platform in unsupportedPlatforms) { testUsingContext('throws UnsupportedError exception if instantiated on ${platform.operatingSystem}', () { expect(
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 2ed7552..ceeb55d 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/compile.dart'; @@ -626,6 +627,23 @@ }, overrides: <Type, Generator>{ FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); + + test('connect sets up log reader', () => testbed.run(() async { + final MockDevice mockDevice = MockDevice(); + final MockDeviceLogReader mockLogReader = MockDeviceLogReader(); + when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(mockLogReader); + + final TestFlutterDevice flutterDevice = TestFlutterDevice( + mockDevice, + <FlutterView>[], + observatoryUris: <Uri>[ testUri ] + ); + + await flutterDevice.connect(); + verify(mockLogReader.connectedVMServices = <VMService>[ mockVMService ]); + }, overrides: <Type, Generator>{ + VMServiceConnector: () => (Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, io.CompressionOptions compression }) async => mockVMService, + })); } class MockFlutterDevice extends Mock implements FlutterDevice {} @@ -634,15 +652,22 @@ class MockDevFS extends Mock implements DevFS {} class MockIsolate extends Mock implements Isolate {} class MockDevice extends Mock implements Device {} +class MockDeviceLogReader extends Mock implements DeviceLogReader {} class MockUsage extends Mock implements Usage {} class MockProcessManager extends Mock implements ProcessManager {} class MockServiceEvent extends Mock implements ServiceEvent {} class TestFlutterDevice extends FlutterDevice { - TestFlutterDevice(Device device, this.views) - : super(device, buildMode: BuildMode.debug, trackWidgetCreation: false); + TestFlutterDevice(Device device, this.views, { List<Uri> observatoryUris }) + : super(device, buildMode: BuildMode.debug, trackWidgetCreation: false) { + _observatoryUris = observatoryUris; + } @override final List<FlutterView> views; + + @override + List<Uri> get observatoryUris => _observatoryUris; + List<Uri> _observatoryUris; } class ThrowingForwardingFileSystem extends ForwardingFileSystem {