blob: 92177ed878a85f947e27c84287740f191597bee0 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'dart:async';
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:native_stack_traces/native_stack_traces.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../convert.dart';
import '../runner/flutter_command.dart';
const rootLoadingUnitId = 1;
/// Support for symbolizing a Dart stack trace.
///
/// This command accepts either paths to an input file containing the
/// stack trace and an output file for the symbolizing trace to be
/// written, or it accepts a stack trace over stdin and outputs it
/// over stdout.
class SymbolizeCommand extends FlutterCommand {
SymbolizeCommand({
required Stdio stdio,
required FileSystem fileSystem,
DwarfSymbolizationService dwarfSymbolizationService = const DwarfSymbolizationService(),
}) : _stdio = stdio,
_fileSystem = fileSystem,
_dwarfSymbolizationService = dwarfSymbolizationService {
argParser.addOption(
'debug-info',
abbr: 'd',
valueHelp: '/out/android/app.arm64.symbols',
help: 'A path to the symbols file generated with "--split-debug-info".',
);
argParser.addMultiOption(
'unit-id-debug-info',
abbr: 'u',
valueHelp: '2:/out/android/app.arm64.symbols-2.part.so',
help:
'A loading unit id and the path to the symbols file for that'
' unit generated with "--split-debug-info".',
);
argParser.addOption(
'input',
abbr: 'i',
valueHelp: '/crashes/stack_trace.err',
help: 'A file path containing a Dart stack trace.',
);
argParser.addOption(
'output',
abbr: 'o',
help: 'A file path for a symbolized stack trace to be written to.',
);
}
final Stdio _stdio;
final FileSystem _fileSystem;
final DwarfSymbolizationService _dwarfSymbolizationService;
@override
String get description => 'Symbolize a stack trace from an AOT-compiled Flutter app.';
@override
String get name => 'symbolize';
@override
final String category = FlutterCommandCategory.tools;
@override
bool get shouldUpdateCache => false;
File _handleDSYM(String fileName) {
final FileSystemEntityType type = _fileSystem.typeSync(fileName);
final bool isDSYM = fileName.endsWith('.dSYM');
if (type == FileSystemEntityType.notFound) {
throw FileNotFoundException(fileName);
}
if (type == FileSystemEntityType.directory) {
if (!isDSYM) {
throw StateError('$fileName is a directory, not a file');
}
final Directory dwarfDir = _fileSystem
.directory(fileName)
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF');
// The DWARF directory inside the .dSYM contains a single MachO file.
return dwarfDir.listSync().single as File;
}
if (isDSYM) {
throw StateError('$fileName is not a dSYM package directory');
}
return _fileSystem.file(fileName);
}
Map<int, File> _unitDebugInfoPathMap() {
final map = <int, File>{};
final String? rootInfo = stringArg('debug-info');
if (rootInfo != null) {
map[rootLoadingUnitId] = _handleDSYM(rootInfo);
}
for (final String arg in stringsArg('unit-id-debug-info')) {
final int separatorIndex = arg.indexOf(':');
final String unitIdString = arg.substring(0, separatorIndex);
final int unitId = int.parse(unitIdString);
final String unitDebugPath = arg.substring(separatorIndex + 1);
if (map.containsKey(unitId) && map[unitId]!.path != unitDebugPath) {
throw StateError(
'Different paths were given for the same loading unit'
' $unitId: "${map[unitId]!.path}" and "$unitDebugPath".',
);
}
map[unitId] = _handleDSYM(unitDebugPath);
}
return map;
}
@override
Future<void> validateCommand() async {
if (argResults?.wasParsed('debug-info') != true &&
argResults?.wasParsed('unit-id-debug-info') != true) {
throwToolExit(
'Either "--debug-info" or "--unit-id-debug-info" is required to symbolize stack traces.',
);
}
for (final String arg in stringsArg('unit-id-debug-info')) {
final int separatorIndex = arg.indexOf(':');
if (separatorIndex == -1) {
throwToolExit(
'The argument to "--unit-id-debug-info" must contain a unit ID and path,'
' separated by ":": "$arg".',
);
}
final String unitIdString = arg.substring(0, separatorIndex);
final int? unitId = int.tryParse(unitIdString);
if (unitId == null) {
throwToolExit(
'The argument to "--unit-id-debug-info" must begin with'
' a unit ID: "$unitIdString" is not an integer.',
);
}
}
late final Map<int, File> map;
try {
map = _unitDebugInfoPathMap();
} on Object catch (e) {
throwToolExit(e.toString());
}
if (!map.containsKey(rootLoadingUnitId)) {
throwToolExit(
'Missing debug info for the root loading unit'
' (id $rootLoadingUnitId).',
);
}
if ((argResults?.wasParsed('input') ?? false) &&
!await _fileSystem.isFile(stringArg('input')!)) {
throwToolExit('${stringArg('input')} does not exist.');
}
return super.validateCommand();
}
@override
Future<FlutterCommandResult> runCommand() async {
// Configure output to either specified file or stdout.
late final IOSink output;
if (argResults?.wasParsed('output') ?? false) {
final File outputFile = _fileSystem.file(stringArg('output'));
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
}
output = outputFile.openWrite();
} else {
final outputController = StreamController<List<int>>();
outputController.stream.transform(utf8.decoder).listen(_stdio.stdoutWrite);
output = IOSink(outputController);
}
// Configure input from either specified file or stdin.
final Stream<List<int>> input = (argResults?.wasParsed('input') ?? false)
? _fileSystem.file(stringArg('input')).openRead()
: _stdio.stdin;
final unitSymbols = <int, Uint8List>{
for (final MapEntry<int, File> entry in _unitDebugInfoPathMap().entries)
entry.key: entry.value.readAsBytesSync(),
};
await _dwarfSymbolizationService.decodeWithUnits(
input: input,
output: output,
unitSymbols: unitSymbols,
);
return FlutterCommandResult.success();
}
}
typedef SymbolsTransformer = StreamTransformer<String, String> Function(Uint8List);
typedef UnitSymbolsTransformer = StreamTransformer<String, String> Function(Map<int, Uint8List>);
StreamTransformer<String, String> _defaultTransformer(Uint8List symbols) {
return _defaultUnitsTransformer(<int, Uint8List>{rootLoadingUnitId: symbols});
}
StreamTransformer<String, String> _defaultUnitsTransformer(Map<int, Uint8List> unitSymbols) {
final map = <int, Dwarf>{};
for (final int unitId in unitSymbols.keys) {
final Uint8List symbols = unitSymbols[unitId]!;
final Dwarf? dwarf = Dwarf.fromBytes(symbols);
if (dwarf == null) {
throwToolExit('Failed to decode symbols file for loading unit $unitId');
}
map[unitId] = dwarf;
}
if (!map.containsKey(rootLoadingUnitId)) {
throwToolExit('Missing symbols file for root loading unit (id $rootLoadingUnitId)');
}
return DwarfStackTraceDecoder(
map[rootLoadingUnitId]!,
includeInternalFrames: true,
dwarfByUnitId: map,
);
}
// A no-op transformer for `DwarfSymbolizationService.test`
StreamTransformer<String, String> _testUnitsTransformer(Map<int, Uint8List> buffer) {
return StreamTransformer<String, String>.fromHandlers(
handleData: (String data, EventSink<String> sink) {
sink.add(data);
},
handleDone: (EventSink<String> sink) {
sink.close();
},
handleError: (Object error, StackTrace stackTrace, EventSink<String> sink) {
sink.addError(error, stackTrace);
},
);
}
/// A service which decodes stack traces from Dart applications.
class DwarfSymbolizationService {
const DwarfSymbolizationService({SymbolsTransformer symbolsTransformer = _defaultTransformer})
: _transformer = symbolsTransformer,
_unitsTransformer = _defaultUnitsTransformer;
const DwarfSymbolizationService.withUnits({
UnitSymbolsTransformer unitSymbolsTransformer = _defaultUnitsTransformer,
}) : _transformer = null,
_unitsTransformer = unitSymbolsTransformer;
/// Create a DwarfSymbolizationService with a no-op transformer for testing.
@visibleForTesting
factory DwarfSymbolizationService.test() {
return const DwarfSymbolizationService.withUnits(unitSymbolsTransformer: _testUnitsTransformer);
}
final SymbolsTransformer? _transformer;
final UnitSymbolsTransformer _unitsTransformer;
/// Decode a stack trace from [input] and place the results in [output].
///
/// Requires [symbols] to be a buffer created from the `--split-debug-info`
/// command line flag.
///
/// Throws a [ToolExit] if the symbols cannot be parsed or the stack trace
/// cannot be decoded.
Future<void> decode({
required Stream<List<int>> input,
required IOSink output,
required Uint8List symbols,
}) async {
await decodeWithUnits(
input: input,
output: output,
unitSymbols: <int, Uint8List>{rootLoadingUnitId: symbols},
);
}
/// Decode a stack trace from [input] and place the results in [output].
///
/// Requires [unitSymbols] to map integer unit IDs to buffers created from
/// the `--split-debug-info` command line flag.
///
/// Throws a [ToolExit] if the symbols cannot be parsed or the stack trace
/// cannot be decoded.
Future<void> decodeWithUnits({
required Stream<List<int>> input,
required IOSink output,
required Map<int, Uint8List> unitSymbols,
}) async {
final UnitSymbolsTransformer unitSymbolsTransformer = _transformer != null
? ((Map<int, Uint8List> m) => _transformer(m[rootLoadingUnitId]!))
: _unitsTransformer;
final onDone = Completer<void>();
StreamSubscription<void>? subscription;
subscription = input
.cast<List<int>>()
.transform(const Utf8Decoder())
.transform(const LineSplitter())
.transform(unitSymbolsTransformer(unitSymbols))
.listen(
(String line) {
try {
output.writeln(line);
} on Exception catch (e, s) {
subscription?.cancel().whenComplete(() {
if (!onDone.isCompleted) {
onDone.completeError(e, s);
}
});
}
},
onDone: onDone.complete,
onError: onDone.completeError,
);
try {
await onDone.future;
await output.close();
} on Exception catch (err) {
throwToolExit('Failed to symbolize stack trace:\n $err');
}
}
}