| // 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'; |
| |
| /// 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.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; |
| |
| @override |
| Future<void> validateCommand() { |
| if (argResults?.wasParsed('debug-info') != true) { |
| throwToolExit('"--debug-info" is required to symbolize stack traces.'); |
| } |
| if (!_fileSystem.isFileSync(stringArg('debug-info')!)) { |
| throwToolExit('${stringArg('debug-info')} does not exist.'); |
| } |
| if ((argResults?.wasParsed('input') ?? false) && !_fileSystem.isFileSync(stringArg('input')!)) { |
| throwToolExit('${stringArg('input')} does not exist.'); |
| } |
| return super.validateCommand(); |
| } |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| Stream<List<int>> input; |
| IOSink output; |
| |
| // Configure output to either specified file or stdout. |
| 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 StreamController<List<int>> outputController = StreamController<List<int>>(); |
| outputController |
| .stream |
| .transform(utf8.decoder) |
| .listen(_stdio.stdoutWrite); |
| output = IOSink(outputController); |
| } |
| |
| // Configure input from either specified file or stdin. |
| if (argResults?.wasParsed('input') ?? false) { |
| input = _fileSystem.file(stringArg('input')).openRead(); |
| } else { |
| input = _stdio.stdin; |
| } |
| |
| final Uint8List symbols = _fileSystem.file(stringArg('debug-info')).readAsBytesSync(); |
| await _dwarfSymbolizationService.decode( |
| input: input, |
| output: output, |
| symbols: symbols, |
| ); |
| |
| return FlutterCommandResult.success(); |
| } |
| } |
| |
| typedef SymbolsTransformer = StreamTransformer<String, String> Function(Uint8List); |
| |
| StreamTransformer<String, String> _defaultTransformer(Uint8List symbols) { |
| final Dwarf? dwarf = Dwarf.fromBytes(symbols); |
| if (dwarf == null) { |
| throwToolExit('Failed to decode symbols file'); |
| } |
| return DwarfStackTraceDecoder(dwarf, includeInternalFrames: true); |
| } |
| |
| // A no-op transformer for `DwarfSymbolizationService.test` |
| StreamTransformer<String, String> _testTransformer(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; |
| |
| /// Create a DwarfSymbolizationService with a no-op transformer for testing. |
| @visibleForTesting |
| factory DwarfSymbolizationService.test() { |
| return const DwarfSymbolizationService( |
| symbolsTransformer: _testTransformer |
| ); |
| } |
| |
| final SymbolsTransformer _transformer; |
| |
| /// 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 { |
| final Completer<void> onDone = Completer<void>(); |
| StreamSubscription<void>? subscription; |
| subscription = input |
| .cast<List<int>>() |
| .transform(const Utf8Decoder()) |
| .transform(const LineSplitter()) |
| .transform(_transformer(symbols)) |
| .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'); |
| } |
| } |
| } |