// 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 '../base/utils.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 _unitDebugInfoPathMap() { final map = {}; 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 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 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 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>(); outputController.stream.transformWithCallSite(utf8.decoder).listen(_stdio.stdoutWrite); output = IOSink(outputController); } // Configure input from either specified file or stdin. final Stream> input = (argResults?.wasParsed('input') ?? false) ? _fileSystem.file(stringArg('input')).openRead() : _stdio.stdin; final unitSymbols = { for (final MapEntry entry in _unitDebugInfoPathMap().entries) entry.key: entry.value.readAsBytesSync(), }; await _dwarfSymbolizationService.decodeWithUnits( input: input, output: output, unitSymbols: unitSymbols, ); return FlutterCommandResult.success(); } } typedef SymbolsTransformer = StreamTransformer Function(Uint8List); typedef UnitSymbolsTransformer = StreamTransformer Function(Map); StreamTransformer _defaultTransformer(Uint8List symbols) { return _defaultUnitsTransformer({rootLoadingUnitId: symbols}); } StreamTransformer _defaultUnitsTransformer(Map unitSymbols) { final map = {}; 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 _testUnitsTransformer(Map buffer) { return StreamTransformer.fromHandlers( handleData: (String data, EventSink sink) { sink.add(data); }, handleDone: (EventSink sink) { sink.close(); }, handleError: (Object error, StackTrace stackTrace, EventSink 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 decode({ required Stream> input, required IOSink output, required Uint8List symbols, }) async { await decodeWithUnits( input: input, output: output, unitSymbols: {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 decodeWithUnits({ required Stream> input, required IOSink output, required Map unitSymbols, }) async { final UnitSymbolsTransformer unitSymbolsTransformer = _transformer != null ? ((Map m) => _transformer(m[rootLoadingUnitId]!)) : _unitsTransformer; final onDone = Completer(); StreamSubscription? subscription; subscription = input .cast>() .transform(utf8LineDecoder) .transformWithCallSite(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'); } } }