mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
220 lines
6.9 KiB
Dart
220 lines
6.9 KiB
Dart
// 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.');
|
|
}
|
|
final String debugInfoPath = stringArg('debug-info')!;
|
|
if (debugInfoPath.endsWith('.dSYM')
|
|
? !_fileSystem.isDirectorySync(debugInfoPath)
|
|
: !_fileSystem.isFileSync(debugInfoPath)) {
|
|
throwToolExit('$debugInfoPath 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;
|
|
}
|
|
|
|
String debugInfoPath = stringArg('debug-info')!;
|
|
|
|
// If it's a dSYM container, expand the path to the actual DWARF.
|
|
if (debugInfoPath.endsWith('.dSYM')) {
|
|
final Directory debugInfoDir = _fileSystem
|
|
.directory(debugInfoPath)
|
|
.childDirectory('Contents')
|
|
.childDirectory('Resources')
|
|
.childDirectory('DWARF');
|
|
|
|
final List<FileSystemEntity> dwarfFiles = debugInfoDir.listSync().whereType<File>().toList();
|
|
if (dwarfFiles.length == 1) {
|
|
debugInfoPath = dwarfFiles.first.path;
|
|
} else {
|
|
throwToolExit('Expected a single DWARF file in a dSYM container.');
|
|
}
|
|
}
|
|
|
|
final Uint8List symbols = _fileSystem.file(debugInfoPath).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');
|
|
}
|
|
}
|
|
}
|