Send text error in JSON and print in tools (#59018)
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 03177fa..fe655c6 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -929,8 +929,17 @@ ); errorJson['errorsSinceReload'] = _errorsSinceReload; - _errorsSinceReload += 1; + if (_errorsSinceReload == 0) { + errorJson['renderedErrorText'] = TextTreeRenderer( + wrapWidth: FlutterError.wrapWidth, + wrapWidthProperties: FlutterError.wrapWidth, + maxDescendentsTruncatableNode: 5, + ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight(); + } else { + errorJson['renderedErrorText'] = 'Another exception was thrown: ${details.summary}'; + } + _errorsSinceReload += 1; postEvent('Flutter.Error', errorJson); }
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index 8cdd02a..7c467e2 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -2254,6 +2254,10 @@ // Validate that we received an error count. expect(error['errorsSinceReload'], 0); + expect( + error['renderedErrorText'], + startsWith( + '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞════════════')); // Send a second error. FlutterError.reportError(FlutterErrorDetailsForRendering( @@ -2267,6 +2271,7 @@ expect(flutterErrorEvents, hasLength(2)); error = flutterErrorEvents.last; expect(error['errorsSinceReload'], 1); + expect(error['renderedErrorText'], startsWith('Another exception was thrown:')); // Reloads the app. final FlutterExceptionHandler oldHandler = FlutterError.onError;
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index e2ad8f7..a28fb74 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -112,6 +112,7 @@ ConnectionResult _connectionResult; StreamSubscription<vmservice.Event> _stdOutSub; StreamSubscription<vmservice.Event> _stdErrSub; + StreamSubscription<vmservice.Event> _extensionEventSub; bool _exited = false; WipConnection _wipConnection; ChromiumLauncher _chromiumLauncher; @@ -150,6 +151,7 @@ } await _stdOutSub?.cancel(); await _stdErrSub?.cancel(); + await _extensionEventSub?.cancel(); await device.device.stopApp(null); try { _generatedEntrypointDirectory?.deleteSync(recursive: true); @@ -675,6 +677,8 @@ final String message = utf8.decode(base64.decode(log.bytes)); globals.printStatus(message, newline: false); }); + _extensionEventSub = + _vmService.onExtensionEvent.listen(printStructuredErrorLog); try { await _vmService.streamListen(vmservice.EventStreams.kStdout); } on vmservice.RPCError { @@ -693,6 +697,12 @@ // It is safe to ignore this error because we expect an error to be // thrown if we're not already subscribed. } + try { + await _vmService.streamListen(vmservice.EventStreams.kExtension); + } on vmservice.RPCError { + // It is safe to ignore this error because we expect an error to be + // thrown if we're not already subscribed. + } unawaited(_vmService.registerService('reloadSources', 'FlutterTools')); _vmService.registerServiceCallback('reloadSources', (Map<String, Object> params) async { final bool pause = params['pause'] as bool ?? false;
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index e16a3bc..573467b 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -188,6 +188,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, }) { final Completer<void> completer = Completer<void>(); StreamSubscription<void> subscription; @@ -207,6 +208,7 @@ compileExpression: compileExpression, reloadMethod: reloadMethod, getSkSLMethod: getSkSLMethod, + printStructuredErrorLogMethod: printStructuredErrorLogMethod, device: device, ); } on Exception catch (exception) { @@ -1061,11 +1063,23 @@ final String copyPath = getDefaultCachedKernelPath( trackWidgetCreation: trackWidgetCreation, ); - globals.fs.file(copyPath).parent.createSync(recursive: true); + globals.fs + .file(copyPath) + .parent + .createSync(recursive: true); outputDill.copySync(copyPath); } } + void printStructuredErrorLog(vm_service.Event event) { + if (event.extensionKind == 'Flutter.Error') { + final Map<dynamic, dynamic> json = event.extensionData?.data; + if (json != null && json.containsKey('renderedErrorText')) { + globals.printStatus('\n${json['renderedErrorText']}'); + } + } + } + /// If the [reloadSources] parameter is not null the 'reloadSources' service /// will be registered. // @@ -1090,7 +1104,8 @@ restart: restart, compileExpression: compileExpression, reloadMethod: reloadMethod, - getSkSLMethod: getSkSLMethod + getSkSLMethod: getSkSLMethod, + printStructuredErrorLogMethod: printStructuredErrorLog, ); // This will wait for at least one flutter view before returning. final Status status = globals.logger.startProgress(
diff --git a/packages/flutter_tools/lib/src/tracing.dart b/packages/flutter_tools/lib/src/tracing.dart index 1a32c45..7458eb0 100644 --- a/packages/flutter_tools/lib/src/tracing.dart +++ b/packages/flutter_tools/lib/src/tracing.dart
@@ -46,7 +46,12 @@ ); try { final Completer<void> whenFirstFrameRendered = Completer<void>(); - await vmService.streamListen('Extension'); + try { + await vmService.streamListen(vm_service.EventStreams.kExtension); + } on vm_service.RPCError { + // It is safe to ignore this error because we expect an error to be + // thrown if we're already subscribed. + } vmService.onExtensionEvent.listen((vm_service.Event event) { if (event.extensionKind == 'Flutter.FirstFrame') { whenFirstFrameRendered.complete();
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 9fcf284..ad7218b 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -31,6 +31,8 @@ /// for [WebSocket]s (used by tests). typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression}); +typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event); + WebSocketConnector _openChannel = _defaultOpenChannel; /// The error codes for the JSON-RPC standard. @@ -149,6 +151,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, io.CompressionOptions compression, Device device, }); @@ -175,6 +178,7 @@ Device device, ReloadMethod reloadMethod, GetSkSLMethod skSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, vm_service.VmService vmService ) { if (reloadSources != null) { @@ -293,6 +297,15 @@ }); vmService.registerService('flutterGetSkSL', 'Flutter Tools'); } + if (printStructuredErrorLogMethod != null) { + try { + vmService.streamListen(vm_service.EventStreams.kExtension); + } on vm_service.RPCError { + // It is safe to ignore this error because we expect an error to be + // thrown if we're already subscribed. + } + vmService.onExtensionEvent.listen(printStructuredErrorLogMethod); + } return vmService; } @@ -311,6 +324,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, io.CompressionOptions compression = io.CompressionOptions.compressionDefault, Device device, }) async { @@ -323,6 +337,7 @@ device: device, reloadMethod: reloadMethod, getSkSLMethod: getSkSLMethod, + printStructuredErrorLogMethod: printStructuredErrorLogMethod, ); } @@ -333,6 +348,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, io.CompressionOptions compression = io.CompressionOptions.compressionDefault, Device device, }) async { @@ -354,6 +370,7 @@ device, reloadMethod, getSkSLMethod, + printStructuredErrorLogMethod, delegateService, ); _httpAddressExpando[service] = httpUri;
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index e4c8b94..c1c2722 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -705,6 +705,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, CompressionOptions compression, Device device, }) async {
diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart index 83b3d74..d9f88a9 100644 --- a/packages/flutter_tools/test/general.shard/cold_test.dart +++ b/packages/flutter_tools/test/general.shard/cold_test.dart
@@ -135,6 +135,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, }) async { throw exception; }
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart index 48de79b..1f78639 100644 --- a/packages/flutter_tools/test/general.shard/hot_test.dart +++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -510,6 +510,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, }) async { throw exception; }
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 2d4222e..7737f6c 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -1357,6 +1357,7 @@ CompileExpression compileExpression, ReloadMethod reloadMethod, GetSkSLMethod getSkSLMethod, + PrintStructuredErrorLogMethod printStructuredErrorLogMethod, io.CompressionOptions compression, Device device, }) async => mockVMService,
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 23285db..b93c348 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:dwds/dwds.dart'; import 'package:flutter_tools/src/base/common.dart'; @@ -57,6 +58,12 @@ } ), FakeVmServiceRequest( + method: 'streamListen', + args: <String, Object>{ + 'streamId': 'Extension', + }, + ), + FakeVmServiceRequest( method: 'registerService', args: <String, Object>{ 'service': 'reloadSources', @@ -340,6 +347,68 @@ expect(testLogger.statusText, contains('SO IS THIS')); })); + test('Listens to extension events with structured errors', () => testbed.run(() async { + final Map<String, String> extensionData = <String, String>{ + 'test': 'data', + 'renderedErrorText': 'error text', + }; + final Map<String, String> emptyExtensionData = <String, String>{ + 'test': 'data', + 'renderedErrorText': '', + }; + final Map<String, String> nonStructuredErrorData = <String, String>{ + 'other': 'other stuff', + }; + fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ + ...kAttachExpectations, + FakeVmServiceStreamResponse( + streamId: 'Extension', + event: vm_service.Event( + timestamp: 0, + extensionKind: 'Flutter.Error', + extensionData: vm_service.ExtensionData.parse(extensionData), + kind: vm_service.EventStreams.kExtension, + ), + ), + // Empty error text should not break anything. + FakeVmServiceStreamResponse( + streamId: 'Extension', + event: vm_service.Event( + timestamp: 0, + extensionKind: 'Flutter.Error', + extensionData: vm_service.ExtensionData.parse(emptyExtensionData), + kind: vm_service.EventStreams.kExtension, + ), + ), + // This is not Flutter.Error kind data, so it should not be logged. + FakeVmServiceStreamResponse( + streamId: 'Extension', + event: vm_service.Event( + timestamp: 0, + extensionKind: 'Other', + extensionData: vm_service.ExtensionData.parse(nonStructuredErrorData), + kind: vm_service.EventStreams.kExtension, + ), + ), + ]); + + _setupMocks(); + final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); + unawaited(residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + )); + await connectionInfoCompleter.future; + + // Need these to run events, otherwise expect statements below run before + // structured errors are processed. + await null; + await null; + await null; + + expect(testLogger.statusText, contains('\nerror text')); + expect(testLogger.statusText, isNot(contains('other stuff'))); + })); + test('Does not run main with --start-paused', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); residentWebRunner = DwdsWebRunnerFactory().createWebRunner( @@ -1073,19 +1142,7 @@ test('Sends launched app.webLaunchUrl event for Chrome device', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ ...kAttachLogExpectations, - const FakeVmServiceRequest( - method: 'streamListen', - args: <String, Object>{ - 'streamId': 'Isolate' - } - ), - const FakeVmServiceRequest( - method: 'registerService', - args: <String, Object>{ - 'service': 'reloadSources', - 'alias': 'FlutterTools', - } - ) + ...kAttachIsolateExpectations, ]); _setupMocks(); final ChromiumLauncher chromiumLauncher = MockChromeLauncher();
diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart index 859e8b0..78bfed1 100644 --- a/packages/flutter_tools/test/general.shard/vmservice_test.dart +++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart
@@ -104,6 +104,7 @@ null, null, null, + null, mockVMService, ); @@ -123,6 +124,7 @@ null, reloadMethod, null, + null, mockVMService, ); @@ -142,6 +144,7 @@ mockDevice, null, null, + null, mockVMService, ); @@ -159,6 +162,7 @@ null, null, () async => 'hello', + null, mockVMService, ); @@ -167,6 +171,26 @@ Logger: () => BufferLogger.test() }); + testUsingContext('VmService registers flutterPrintStructuredErrorLogMethod', () async { + final MockVMService mockVMService = MockVMService(); + when(mockVMService.onExtensionEvent).thenAnswer((Invocation invocation) { + return const Stream<vm_service.Event>.empty(); + }); + setUpVmService( + null, + null, + null, + null, + null, + null, + (vm_service.Event event) async => 'hello', + mockVMService, + ); + verify(mockVMService.streamListen(vm_service.EventStreams.kExtension)).called(1); + }, overrides: <Type, Generator>{ + Logger: () => BufferLogger.test() + }); + testUsingContext('VMService returns correct FlutterVersion', () async { final MockVMService mockVMService = MockVMService(); setUpVmService( @@ -176,6 +200,7 @@ null, null, null, + null, mockVMService, );