429 lines
18 KiB
Dart
429 lines
18 KiB
Dart
// Copyright 2016 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'basic_types.dart';
|
|
import 'print.dart';
|
|
|
|
/// Signature for [FlutterError.onError] handler.
|
|
typedef void FlutterExceptionHandler(FlutterErrorDetails details);
|
|
|
|
/// Signature for [FlutterErrorDetails.informationCollector] callback
|
|
/// and other callbacks that collect information into a string buffer.
|
|
typedef void InformationCollector(StringBuffer information);
|
|
|
|
/// Class for information provided to [FlutterExceptionHandler] callbacks.
|
|
///
|
|
/// See [FlutterError.onError].
|
|
class FlutterErrorDetails {
|
|
/// Creates a [FlutterErrorDetails] object with the given arguments setting
|
|
/// the object's properties.
|
|
///
|
|
/// The framework calls this constructor when catching an exception that will
|
|
/// subsequently be reported using [FlutterError.onError].
|
|
///
|
|
/// The [exception] must not be null; other arguments can be left to
|
|
/// their default values. (`throw null` results in a
|
|
/// [NullThrownError] exception.)
|
|
const FlutterErrorDetails({
|
|
this.exception,
|
|
this.stack,
|
|
this.library = 'Flutter framework',
|
|
this.context,
|
|
this.stackFilter,
|
|
this.informationCollector,
|
|
this.silent = false
|
|
});
|
|
|
|
/// The exception. Often this will be an [AssertionError], maybe specifically
|
|
/// a [FlutterError]. However, this could be any value at all.
|
|
final dynamic exception;
|
|
|
|
/// The stack trace from where the [exception] was thrown (as opposed to where
|
|
/// it was caught).
|
|
///
|
|
/// StackTrace objects are opaque except for their [toString] function.
|
|
///
|
|
/// If this field is not null, then the [stackFilter] callback, if any, will
|
|
/// be called with the result of calling [toString] on this object and
|
|
/// splitting that result on line breaks. If there's no [stackFilter]
|
|
/// callback, then [FlutterError.defaultStackFilter] is used instead. That
|
|
/// function expects the stack to be in the format used by
|
|
/// [StackTrace.toString].
|
|
final StackTrace stack;
|
|
|
|
/// A human-readable brief name describing the library that caught the error
|
|
/// message. This is used by the default error handler in the header dumped to
|
|
/// the console.
|
|
final String library;
|
|
|
|
/// A human-readable description of where the error was caught (as opposed to
|
|
/// where it was thrown).
|
|
final String context;
|
|
|
|
/// A callback which filters the [stack] trace. Receives an iterable of
|
|
/// strings representing the frames encoded in the way that
|
|
/// [StackTrace.toString()] provides. Should return an iterable of lines to
|
|
/// output for the stack.
|
|
///
|
|
/// If this is not provided, then [FlutterError.dumpErrorToConsole] will use
|
|
/// [FlutterError.defaultStackFilter] instead.
|
|
///
|
|
/// If the [FlutterError.defaultStackFilter] behavior is desired, then the
|
|
/// callback should manually call that function. That function expects the
|
|
/// incoming list to be in the [StackTrace.toString()] format. The output of
|
|
/// that function, however, does not always follow this format.
|
|
///
|
|
/// This won't be called if [stack] is null.
|
|
final IterableFilter<String> stackFilter;
|
|
|
|
/// A callback which, when called with a [StringBuffer] will write to that buffer
|
|
/// information that could help with debugging the problem.
|
|
///
|
|
/// Information collector callbacks can be expensive, so the generated information
|
|
/// should be cached, rather than the callback being called multiple times.
|
|
///
|
|
/// The text written to the information argument may contain newlines but should
|
|
/// not end with a newline.
|
|
final InformationCollector informationCollector;
|
|
|
|
/// Whether this error should be ignored by the default error reporting
|
|
/// behavior in release mode.
|
|
///
|
|
/// If this is false, the default, then the default error handler will always
|
|
/// dump this error to the console.
|
|
///
|
|
/// If this is true, then the default error handler would only dump this error
|
|
/// to the console in checked mode. In release mode, the error is ignored.
|
|
///
|
|
/// This is used by certain exception handlers that catch errors that could be
|
|
/// triggered by environmental conditions (as opposed to logic errors). For
|
|
/// example, the HTTP library sets this flag so as to not report every 404
|
|
/// error to the console on end-user devices, while still allowing a custom
|
|
/// error handler to see the errors even in release builds.
|
|
final bool silent;
|
|
|
|
/// Converts the [exception] to a string.
|
|
///
|
|
/// This applies some additional logic to make [AssertionError] exceptions
|
|
/// prettier, to handle exceptions that stringify to empty strings, to handle
|
|
/// objects that don't inherit from [Exception] or [Error], and so forth.
|
|
String exceptionAsString() {
|
|
String longMessage;
|
|
if (exception is AssertionError) {
|
|
// Regular _AssertionErrors thrown by assert() put the message last, after
|
|
// some code snippets. This leads to ugly messages. To avoid this, we move
|
|
// the assertion message up to before the code snippets, separated by a
|
|
// newline, if we recognise that format is being used.
|
|
final String message = exception.message;
|
|
final String fullMessage = exception.toString();
|
|
if (message is String && message != fullMessage) {
|
|
if (fullMessage.length > message.length) {
|
|
final int position = fullMessage.lastIndexOf(message);
|
|
if (position == fullMessage.length - message.length &&
|
|
position > 2 &&
|
|
fullMessage.substring(position - 2, position) == ': ') {
|
|
longMessage = '${message.trimRight()}\n${fullMessage.substring(0, position - 2)}';
|
|
}
|
|
}
|
|
}
|
|
longMessage ??= fullMessage;
|
|
} else if (exception is String) {
|
|
longMessage = exception;
|
|
} else if (exception is Error || exception is Exception) {
|
|
longMessage = exception.toString();
|
|
} else {
|
|
longMessage = ' ${exception.toString()}';
|
|
}
|
|
longMessage = longMessage.trimRight();
|
|
if (longMessage.isEmpty)
|
|
longMessage = ' <no message available>';
|
|
return longMessage;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
final StringBuffer buffer = new StringBuffer();
|
|
if ((library != null && library != '') || (context != null && context != '')) {
|
|
if (library != null && library != '') {
|
|
buffer.write('Error caught by $library');
|
|
if (context != null && context != '')
|
|
buffer.write(', ');
|
|
} else {
|
|
buffer.writeln('Exception ');
|
|
}
|
|
if (context != null && context != '')
|
|
buffer.write('thrown $context');
|
|
buffer.writeln('.');
|
|
} else {
|
|
buffer.write('An error was caught.');
|
|
}
|
|
buffer.writeln(exceptionAsString());
|
|
if (informationCollector != null)
|
|
informationCollector(buffer);
|
|
if (stack != null) {
|
|
Iterable<String> stackLines = stack.toString().trimRight().split('\n');
|
|
if (stackFilter != null) {
|
|
stackLines = stackFilter(stackLines);
|
|
} else {
|
|
stackLines = FlutterError.defaultStackFilter(stackLines);
|
|
}
|
|
buffer.writeAll(stackLines, '\n');
|
|
}
|
|
return buffer.toString().trimRight();
|
|
}
|
|
}
|
|
|
|
/// Error class used to report Flutter-specific assertion failures and
|
|
/// contract violations.
|
|
class FlutterError extends AssertionError {
|
|
/// Creates a [FlutterError].
|
|
///
|
|
/// See [message] for details on the format that the message should
|
|
/// take.
|
|
///
|
|
/// Include as much detail as possible in the full error message,
|
|
/// including specifics about the state of the app that might be
|
|
/// relevant to debugging the error.
|
|
FlutterError(String message) : super(message);
|
|
|
|
/// The message associated with this error.
|
|
///
|
|
/// The message may have newlines in it. The first line should be a terse
|
|
/// description of the error, e.g. "Incorrect GlobalKey usage" or "setState()
|
|
/// or markNeedsBuild() called during build". Subsequent lines should contain
|
|
/// substantial additional information, ideally sufficient to develop a
|
|
/// correct solution to the problem.
|
|
///
|
|
/// In some cases, when a FlutterError is reported to the user, only the first
|
|
/// line is included. For example, Flutter will typically only fully report
|
|
/// the first exception at runtime, displaying only the first line of
|
|
/// subsequent errors.
|
|
///
|
|
/// All sentences in the error should be correctly punctuated (i.e.,
|
|
/// do end the error message with a period).
|
|
@override
|
|
String get message => super.message;
|
|
|
|
@override
|
|
String toString() => message;
|
|
|
|
/// Called whenever the Flutter framework catches an error.
|
|
///
|
|
/// The default behavior is to call [dumpErrorToConsole].
|
|
///
|
|
/// You can set this to your own function to override this default behavior.
|
|
/// For example, you could report all errors to your server.
|
|
///
|
|
/// If the error handler throws an exception, it will not be caught by the
|
|
/// Flutter framework.
|
|
///
|
|
/// Set this to null to silently catch and ignore errors. This is not
|
|
/// recommended.
|
|
static FlutterExceptionHandler onError = dumpErrorToConsole;
|
|
|
|
static int _errorCount = 0;
|
|
|
|
/// Resets the count of errors used by [dumpErrorToConsole] to decide whether
|
|
/// to show a complete error message or an abbreviated one.
|
|
///
|
|
/// After this is called, the next error message will be shown in full.
|
|
static void resetErrorCount() {
|
|
_errorCount = 0;
|
|
}
|
|
|
|
/// The width to which [dumpErrorToConsole] will wrap lines.
|
|
///
|
|
/// This can be used to ensure strings will not exceed the length at which
|
|
/// they will wrap, e.g. when placing ASCII art diagrams in messages.
|
|
static const int wrapWidth = 100;
|
|
|
|
/// Prints the given exception details to the console.
|
|
///
|
|
/// The first time this is called, it dumps a very verbose message to the
|
|
/// console using [debugPrint].
|
|
///
|
|
/// Subsequent calls only dump the first line of the exception, unless
|
|
/// `forceReport` is set to true (in which case it dumps the verbose message).
|
|
///
|
|
/// Call [resetErrorCount] to cause this method to go back to acting as if it
|
|
/// had not been called before (so the next message is verbose again).
|
|
///
|
|
/// The default behavior for the [onError] handler is to call this function.
|
|
static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport = false }) {
|
|
assert(details != null);
|
|
assert(details.exception != null);
|
|
bool reportError = details.silent != true; // could be null
|
|
assert(() {
|
|
// In checked mode, we ignore the "silent" flag.
|
|
reportError = true;
|
|
return true;
|
|
}());
|
|
if (!reportError && !forceReport)
|
|
return;
|
|
if (_errorCount == 0 || forceReport) {
|
|
final String header = '\u2550\u2550\u2561 EXCEPTION CAUGHT BY ${details.library} \u255E'.toUpperCase();
|
|
final String footer = '\u2550' * wrapWidth;
|
|
debugPrint('$header${"\u2550" * (footer.length - header.length)}');
|
|
final String verb = 'thrown${ details.context != null ? " ${details.context}" : ""}';
|
|
if (details.exception is NullThrownError) {
|
|
debugPrint('The null value was $verb.', wrapWidth: wrapWidth);
|
|
} else if (details.exception is num) {
|
|
debugPrint('The number ${details.exception} was $verb.', wrapWidth: wrapWidth);
|
|
} else {
|
|
String errorName;
|
|
if (details.exception is AssertionError) {
|
|
errorName = 'assertion';
|
|
} else if (details.exception is String) {
|
|
errorName = 'message';
|
|
} else if (details.exception is Error || details.exception is Exception) {
|
|
errorName = '${details.exception.runtimeType}';
|
|
} else {
|
|
errorName = '${details.exception.runtimeType} object';
|
|
}
|
|
// Many exception classes put their type at the head of their message.
|
|
// This is redundant with the way we display exceptions, so attempt to
|
|
// strip out that header when we see it.
|
|
final String prefix = '${details.exception.runtimeType}: ';
|
|
String message = details.exceptionAsString();
|
|
if (message.startsWith(prefix))
|
|
message = message.substring(prefix.length);
|
|
debugPrint('The following $errorName was $verb:\n$message', wrapWidth: wrapWidth);
|
|
}
|
|
Iterable<String> stackLines = (details.stack != null) ? details.stack.toString().trimRight().split('\n') : null;
|
|
if ((details.exception is AssertionError) && (details.exception is! FlutterError)) {
|
|
bool ourFault = true;
|
|
if (stackLines != null) {
|
|
final List<String> stackList = stackLines.take(2).toList();
|
|
if (stackList.length >= 2) {
|
|
// TODO(ianh): This has bitrotted and is no longer matching. https://github.com/flutter/flutter/issues/4021
|
|
final RegExp throwPattern = new RegExp(r'^#0 +_AssertionError._throwNew \(dart:.+\)$');
|
|
final RegExp assertPattern = new RegExp(r'^#1 +[^(]+ \((.+?):([0-9]+)(?::[0-9]+)?\)$');
|
|
if (throwPattern.hasMatch(stackList[0])) {
|
|
final Match assertMatch = assertPattern.firstMatch(stackList[1]);
|
|
if (assertMatch != null) {
|
|
assert(assertMatch.groupCount == 2);
|
|
final RegExp ourLibraryPattern = new RegExp(r'^package:flutter/');
|
|
ourFault = ourLibraryPattern.hasMatch(assertMatch.group(1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ourFault) {
|
|
debugPrint('\nEither the assertion indicates an error in the framework itself, or we should '
|
|
'provide substantially more information in this error message to help you determine '
|
|
'and fix the underlying cause.', wrapWidth: wrapWidth);
|
|
debugPrint('In either case, please report this assertion by filing a bug on GitHub:', wrapWidth: wrapWidth);
|
|
debugPrint(' https://github.com/flutter/flutter/issues/new');
|
|
}
|
|
}
|
|
if (details.stack != null) {
|
|
debugPrint('\nWhen the exception was thrown, this was the stack:', wrapWidth: wrapWidth);
|
|
if (details.stackFilter != null) {
|
|
stackLines = details.stackFilter(stackLines);
|
|
} else {
|
|
stackLines = defaultStackFilter(stackLines);
|
|
}
|
|
for (String line in stackLines)
|
|
debugPrint(line, wrapWidth: wrapWidth);
|
|
}
|
|
if (details.informationCollector != null) {
|
|
final StringBuffer information = new StringBuffer();
|
|
details.informationCollector(information);
|
|
debugPrint('\n${information.toString().trimRight()}', wrapWidth: wrapWidth);
|
|
}
|
|
debugPrint(footer);
|
|
} else {
|
|
debugPrint('Another exception was thrown: ${details.exceptionAsString().split("\n")[0].trimLeft()}');
|
|
}
|
|
_errorCount += 1;
|
|
}
|
|
|
|
/// Converts a stack to a string that is more readable by omitting stack
|
|
/// frames that correspond to Dart internals.
|
|
///
|
|
/// This is the default filter used by [dumpErrorToConsole] if the
|
|
/// [FlutterErrorDetails] object has no [FlutterErrorDetails.stackFilter]
|
|
/// callback.
|
|
///
|
|
/// This function expects its input to be in the format used by
|
|
/// [StackTrace.toString()]. The output of this function is similar to that
|
|
/// format but the frame numbers will not be consecutive (frames are elided)
|
|
/// and the final line may be prose rather than a stack frame.
|
|
static Iterable<String> defaultStackFilter(Iterable<String> frames) {
|
|
const List<String> filteredPackages = const <String>[
|
|
'dart:async-patch',
|
|
'dart:async',
|
|
'package:stack_trace',
|
|
];
|
|
const List<String> filteredClasses = const <String>[
|
|
'_AssertionError',
|
|
'_FakeAsync',
|
|
'_FrameCallbackEntry',
|
|
];
|
|
final RegExp stackParser = new RegExp(r'^#[0-9]+ +([^.]+).* \(([^/\\]*)[/\\].+:[0-9]+(?::[0-9]+)?\)$');
|
|
final RegExp packageParser = new RegExp(r'^([^:]+):(.+)$');
|
|
final List<String> result = <String>[];
|
|
final List<String> skipped = <String>[];
|
|
for (String line in frames) {
|
|
final Match match = stackParser.firstMatch(line);
|
|
if (match != null) {
|
|
assert(match.groupCount == 2);
|
|
if (filteredPackages.contains(match.group(2))) {
|
|
final Match packageMatch = packageParser.firstMatch(match.group(2));
|
|
if (packageMatch != null && packageMatch.group(1) == 'package') {
|
|
skipped.add('package ${packageMatch.group(2)}'); // avoid "package package:foo"
|
|
} else {
|
|
skipped.add('package ${match.group(2)}');
|
|
}
|
|
continue;
|
|
}
|
|
if (filteredClasses.contains(match.group(1))) {
|
|
skipped.add('class ${match.group(1)}');
|
|
continue;
|
|
}
|
|
}
|
|
result.add(line);
|
|
}
|
|
if (skipped.length == 1) {
|
|
result.add('(elided one frame from ${skipped.single})');
|
|
} else if (skipped.length > 1) {
|
|
final List<String> where = new Set<String>.from(skipped).toList()..sort();
|
|
if (where.length > 1)
|
|
where[where.length - 1] = 'and ${where.last}';
|
|
if (where.length > 2) {
|
|
result.add('(elided ${skipped.length} frames from ${where.join(", ")})');
|
|
} else {
|
|
result.add('(elided ${skipped.length} frames from ${where.join(" ")})');
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Calls [onError] with the given details, unless it is null.
|
|
static void reportError(FlutterErrorDetails details) {
|
|
assert(details != null);
|
|
assert(details.exception != null);
|
|
if (onError != null)
|
|
onError(details);
|
|
}
|
|
}
|
|
|
|
/// Dump the current stack to the console using [debugPrint] and
|
|
/// [FlutterError.defaultStackFilter].
|
|
///
|
|
/// The current stack is obtained using [StackTrace.current].
|
|
///
|
|
/// The `maxFrames` argument can be given to limit the stack to the given number
|
|
/// of lines. By default, all non-filtered stack lines are shown.
|
|
///
|
|
/// The `label` argument, if present, will be printed before the stack.
|
|
void debugPrintStack({ String label, int maxFrames }) {
|
|
if (label != null)
|
|
debugPrint(label);
|
|
Iterable<String> lines = StackTrace.current.toString().trimRight().split('\n');
|
|
if (maxFrames != null)
|
|
lines = lines.take(maxFrames);
|
|
debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
|
|
}
|