blob: 314d5cd91e01f4c0b954b0d0056e8df39eba8aa3 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Logic for extracting crashes from text.
library symbolizer.parser;
import 'package:symbolizer/model.dart';
/// Returns [true] if the given text is likely to contain a crash.
bool containsCrash(String text) {
return text.contains(_CrashExtractor._androidCrashMarker) ||
text.contains(_CrashExtractor._iosCrashMarker);
}
class CrashExtractor {
const CrashExtractor();
/// Extract all crashes from the given [text] using [overrides].
List<Crash> extractCrashes(String text,
{SymbolizationOverrides? overrides}) =>
_CrashExtractor(text: text, overrides: overrides).crashes;
}
class _CrashExtractor {
final List<String> lines;
final SymbolizationOverrides? overrides;
final List<Crash> crashes = <Crash>[];
int lineNo = 0;
String? os;
String? arch;
String? mode;
String? format;
List<CrashFrame> frames = [];
RegExp? logLinePattern;
bool collectingFrames = false;
int? androidMajorVersion;
_CrashExtractor({required String text, this.overrides})
: lines = text.split(_lineEnding) {
final overrides = this.overrides;
if (overrides != null) {
final os = overrides.os;
if (overrides.format == 'internal' && os != null) {
parseAsCustomBacktrace(_internalFramePattern, os);
return;
} else if (overrides.force && os == 'ios') {
parseAsIosBacktrace();
return;
}
}
// Default processing.
processLines();
}
/// Extract only stack traces from the text which match the given regexp.
/// Used when 'internal' override is in effect.
void parseAsCustomBacktrace(RegExp pattern, String os) {
for (; lineNo < lines.length; lineNo++) {
final line = lines[lineNo];
final frameMatch = pattern.firstMatch(line);
if (frameMatch == null) {
if (collectingFrames) {
endCrash();
}
continue;
}
if (!collectingFrames) {
startCrash(os);
mode = 'release';
format = 'custom';
collectingFrames = true;
}
frames.add(CrashFrame.custom(
no: frames.length.toString().padLeft(2, '0'),
binary: frameMatch.namedGroup('binary')!,
pc: int.parse(frameMatch.namedGroup('pc')!),
symbol: frameMatch.namedGroup('symbol'),
offset: frameMatch.namedGroup('offset') != null
? int.parse(frameMatch.namedGroup('offset')!)
: null,
location: frameMatch.namedGroup('location'),
));
}
endCrash();
}
/// Extract only iOS like stack traces from the text. Used when
/// 'force ios' override is in effect.
void parseAsIosBacktrace() {
for (; lineNo < lines.length; lineNo++) {
final line = lines[lineNo];
var frame = parseIosFrame(line);
if (frame == null) {
if (collectingFrames) {
endCrash();
}
continue;
}
// Force resymbolization of frames that correspond to the Flutter
// framework by discarding symbol information already contained in
// the stack trace.
if (frame.binary == 'Flutter' && frame.symbol != 'Flutter') {
frame = frame.copyWith(
offset: null, symbol: CrashFrame.crashalyticsMissingSymbol);
}
// Allow frames that miss offset and instead contain `(Missing)`
// instead of symbol name. This can happen in crashalytics output.
if (frame.offset == null &&
frame.symbol != CrashFrame.crashalyticsMissingSymbol) {
continue;
}
if (!collectingFrames) {
startCrash('ios');
mode = 'release';
collectingFrames = true;
}
frames.add(frame);
}
endCrash();
}
IosCrashFrame? parseIosFrame(String line) {
final frameMatch = _iosFramePattern.firstMatch(line);
if (frameMatch == null) {
return null;
}
var rest = frameMatch.namedGroup('rest')!.trim();
var location = '';
if (rest.endsWith(')')) {
final open = rest.lastIndexOf('(');
if (open > 0) {
location = rest.substring(open + 1, rest.length - 2);
rest = rest.substring(0, open).trim();
}
}
final offsetMatch = _offsetSuffixPattern.firstMatch(rest);
String symbol;
int? offset;
if (offsetMatch != null) {
// In some rare cases offset would be computed against load base of 0
// in which case int.parse would refuse to parse it (because it is
// a decimal integer outside of range for signed 64-bit integer).
// Ignore such cases.
offset = int.tryParse(offsetMatch.namedGroup('offset')!);
symbol = offsetMatch.namedGroup('symbol')!.trim();
} else {
symbol = rest.trim();
}
return IosCrashFrame(
no: frameMatch.namedGroup('no')!,
binary: frameMatch.namedGroup('binary')!,
pc: int.parse(frameMatch.namedGroup('pc')!),
symbol: symbol,
offset: offset,
location: location,
);
}
void processLines() {
for (; lineNo < lines.length; lineNo++) {
var line = lines[lineNo];
// Strip markdown quote.
if (line.startsWith('> ')) {
line = line.substring(2);
}
// If we have an indication that we are processing raw logcat or flutter
// verbose output then strip the prefix characteristic for those log
// types.
if (logLinePattern != null) {
final m = logLinePattern!.firstMatch(line);
if (m != null) {
line = m.namedGroup('rest')!;
} else {
// No longer in the raw log output. If we started collecting a crash
// flush it and continue handling the line.
endCrash();
}
}
if (line.isEmpty) {
continue;
}
// First check for crash markers.
if (line.contains(_androidCrashMarker)) {
// Start of the Android crash.
startCrash('android');
continue;
}
if (line.contains(_iosCrashMarker)) {
// Start of the iOS crash.
startCrash('ios');
continue;
}
final dartvmCrashMatch = _dartvmCrashMarker.firstMatch(line);
if (dartvmCrashMatch != null) {
startCrash(dartvmCrashMatch.namedGroup('os')!);
format = 'dartvm';
arch = dartvmCrashMatch.namedGroup('arch');
if (arch == 'ia32') {
arch = 'x86';
}
continue;
}
if (format == 'dartvm') {
if (line.contains(_endOfDumpStackTracePattern)) {
endCrash();
continue;
}
final frameMatch = _dartvmFramePattern.firstMatch(line);
if (frameMatch != null) {
frames.add(CrashFrame.dartvm(
pc: int.parse(frameMatch.namedGroup('pc')!),
binary: frameMatch.namedGroup('binary')!,
offset: int.parse(frameMatch.namedGroup('offset')!),
));
}
}
if (os == 'android') {
// Handle `Build fingerprint: '...'` line.
final androidVersion = _androidBuildFingerprintPattern
.firstMatch(line)
?.namedGroup('version');
if (androidVersion != null) {
androidMajorVersion = int.tryParse(androidVersion.split('.').first);
}
// Handle `ABI: '...'` line.
final abiMatch = _androidAbiPattern.firstMatch(line);
if (abiMatch != null) {
arch = abiMatch.namedGroup('abi');
if (arch == 'x86_64') {
arch = 'x64';
}
continue;
}
// Handle backtrace: start.
if (_backtraceStartPattern.hasMatch(line)) {
collectingFrames = true;
continue;
}
// If backtrace has started then process line corresponding for a frame.
if (collectingFrames) {
final frameMatch = _androidFramePattern.firstMatch(line);
if (frameMatch != null) {
final rest = frameMatch.namedGroup('rest')!;
final buildIdMatch = _buildIdPattern.firstMatch(rest);
frames.add(CrashFrame.android(
no: frameMatch.namedGroup('no')!,
pc: int.parse(frameMatch.namedGroup('pc')!, radix: 16),
binary: frameMatch.namedGroup('binary')!,
rest: rest,
buildId: buildIdMatch?.namedGroup('id'),
));
} else {
endCrash();
}
}
continue;
}
if (os == 'ios') {
// Handle EOF marking the end of crash report.
if (line.contains(_eofPattern)) {
endCrash();
continue;
}
// Handle line that describes App.framework binary image.
// We use it as a marker to detect release mode builds.
if (line.contains(_appFrameworkPattern)) {
mode = 'release';
continue;
}
// Handle `Code Type: ...` line.
final abiMatch = _iosAbiPattern.firstMatch(line);
if (abiMatch != null) {
arch = abiMatch.namedGroup('abi') == 'ARM-64' ? 'arm64' : 'arm32';
continue;
}
// Handle `Thread ... Crashed:` line
if (line.contains(_crashedThreadPattern)) {
collectingFrames = true;
continue;
}
if (collectingFrames) {
final frame = parseIosFrame(line);
if (frame != null) {
frames.add(frame);
} else {
collectingFrames = false;
// Don't flush the crash yet - we want to read report until the
// end and check if it is a release build or not.
}
}
continue;
}
}
endCrash();
}
void startCrash(String os) {
endCrash();
this.os = os;
frames = <CrashFrame>[];
arch = mode = null;
logLinePattern = null;
collectingFrames = false;
format = 'native';
androidMajorVersion = null;
if (os == 'android') {
detectAndroidLogFormat();
}
}
void endCrash() {
// We are not interested in crashes where we did not collect any frames.
if (frames.isNotEmpty) {
if (os == 'android') {
androidMajorVersion ??= guessAndroidVersion();
}
crashes.add(Crash(
engineVariant: EngineVariant(
os: os!,
arch: overrides?.arch ?? arch,
mode: overrides?.mode ?? mode,
),
frames: frames,
format: format!,
androidMajorVersion: androidMajorVersion,
));
}
frames = [];
collectingFrames = false;
logLinePattern = null;
}
/// Android crashes might come from `adb logcat` or `flutter run -v`
/// output. In which case we need to strip prefix characteristic for
/// those.
void detectAndroidLogFormat() {
final line = lines[lineNo];
if (line.contains(_flutterLogPattern)) {
logLinePattern = _flutterLogPattern;
guessLaunchMode();
} else if (line.contains(_deviceLabPattern)) {
logLinePattern = _deviceLabPattern;
guessLaunchMode();
} else if (line.contains(_logcatPattern)) {
logLinePattern = _logcatPattern;
}
}
int? guessAndroidVersion() {
// On Android 11 has introduced a change[1] which mangles APK install
// locations with a random prefix (and suffix). We can use the presence of
// this prefix to detect that we are running on Android 11 or above.
//
// [1]: https://partner-android.googlesource.com/platform/frameworks/base/+/f56f1c5c587ed5af452ed1b339218dabc12c9f93
if (frames.any((f) => f.binary.startsWith('/data/app/~~'))) {
return 11;
}
return null; // Can't guess.
}
void guessLaunchMode() {
if (overrides?.mode != null) {
return;
}
// Try to look backwards through log lines to determine launch mode.
for (var i = lineNo - 1; i >= 0; i--) {
final logLineMatch = logLinePattern!.firstMatch(lines[i]);
if (logLineMatch == null) {
break;
}
final modeMatch =
_launchModePattern.firstMatch(logLineMatch.namedGroup('rest')!);
if (modeMatch != null) {
mode = modeMatch.namedGroup('mode');
return;
}
}
}
static final _androidCrashMarker =
'*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***';
static final _iosCrashMarker = RegExp(
r'(Incident Identifier:\s+([A-F0-9]+-?)+)|(Exception Type:\s+EXC_CRASH)');
static final _dartvmCrashMarker =
RegExp(r'version=.*on "(?<os>android)_(?<arch>arm|arm64|ia32|x64)"\s*$');
static final _lineEnding = RegExp(r'\r?\n');
static final _logcatPattern = RegExp(r'^.*?:(?=$|\s)\s*(?<rest>.*)$');
static final _flutterLogPattern =
RegExp(r'^\s*\[\s*(\+?\s*\d+\s*(s|ms|h))?\]\s+(?<rest>.*)$');
static final _deviceLabPattern = RegExp(
r'(\d+-\d+-\d+T\d+:\d+:\d+.\d+: )?(stdout|stderr):\s*\[\s*(\+?\s*\d+\s*(s|ms|h))?\]\s+(?<rest>.*)');
static final _androidAbiPattern =
RegExp(r"^\s*ABI: '(?<abi>x86|x86_64|x64|arm|arm64)'\s*$");
// Android Compatibility Definition mandates build fingerprint format to be:
//
// $(BRAND)/$(PRODUCT)/$(DEVICE):
// $(VERSION.RELEASE)/$(ID)/$(VERSION.INCREMENTAL):$(TYPE)/$(TAGS)
//
// For our purposes we are only interested in VERSION.RELEASE component of the
// fingerprint.
//
// See https://source.android.com/compatibility/11/android-11-cdd
static final _androidBuildFingerprintPattern =
RegExp(r"Build fingerprint: '[^:']+:(?<version>[\d\.]+)/[^']*'");
static final _backtraceStartPattern = RegExp(r'^\s*backtrace:\s*$');
static final _androidFramePattern = RegExp(
r'^\s*#(?<no>\d+)\s+pc\s+(0x)?(?<pc>[0-9a-f]+)\s+(?<binary>[^\s]+)(?<rest>.*)$');
static final _buildIdPattern = RegExp(r'\(BuildId: (?<id>[0-9a-f]+)\)');
static final _launchModePattern =
RegExp(r'^\s*Launching .* on .* in (?<mode>debug|release|profile) mode');
static final _eofPattern = RegExp(r'^\s*EOF\s*$');
static final _appFrameworkPattern = RegExp(
r'^\s*0x[a-f0-9]+\s+-\s+0x[a-f0-9]+\s+App\s+arm\w+\s+<[a-f0-9]+>\s+/var/containers');
static final _iosAbiPattern = RegExp(r'\s*Code\s+Type:\s+(?<abi>[\-\w]+)\s+');
static final _crashedThreadPattern = RegExp(r'^\s*Thread \d+ Crashed:');
static final _iosFramePattern = RegExp(
r'^\s*(?<no>\d+)\s+(?<binary>\S+)\s+(?<pc>0x[a-f0-9]+)\s+(?<rest>.*)$',
unicode: true,
caseSensitive: false);
static final _offsetSuffixPattern =
RegExp(r'^(?<symbol>.*)\s+\+\s+(?<offset>\d+)$');
static final _endOfDumpStackTracePattern = '-- End of DumpStackTrace';
static final _dartvmFramePattern = RegExp(
r'(^|\s+)pc\s+(?<pc>0x[a-f0-9]+)\s+fp\s+(?<fp>0x[a-f0-9]+)\s+(?<binary>[^\s+]+)\+(?<offset>0x[a-f0-9]+)');
final _internalFramePattern = RegExp(
r'(?<pc>0x[a-f0-9]+)\s*\((?<binary>\S+)\s+(\+\s+(?<offset>0x[a-f0-9]+)|(?<location>[^+][^\)]+))\)(?<symbol>.*)',
caseSensitive: false);
}