This repository has been archived by the owner on Apr 29, 2024. It is now read-only.
forked from flutter/flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add "flutter symbolize" command (flutter#49465)
- Loading branch information
1 parent
ffc8559
commit 5681727
Showing
6 changed files
with
277 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// 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 symbolicating a Dart stack trace. | ||
/// | ||
/// This command accepts either paths to an input file containing the | ||
/// stack trace and an output file for the symbolicated 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', | ||
valueHelp: 'A file path for a symbolicated 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 application.'; | ||
|
||
@override | ||
String get name => 'symbolize'; | ||
|
||
@override | ||
bool get shouldUpdateCache => false; | ||
|
||
@override | ||
Future<void> validateCommand() { | ||
if (!argResults.wasParsed('debug-info')) { | ||
throwToolExit('"--debug-info" is required to symbolicate stack traces.'); | ||
} | ||
if (!_fileSystem.isFileSync(stringArg('debug-info'))) { | ||
throwToolExit('${stringArg('debug-info')} does not exist.'); | ||
} | ||
if (argResults.wasParsed('input') && !_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')) { | ||
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')) { | ||
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(); | ||
} | ||
} | ||
|
||
/// A service which decodes stack traces from Dart applications. | ||
class DwarfSymbolizationService { | ||
const DwarfSymbolizationService(); | ||
|
||
/// 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 Dwarf dwarf = Dwarf.fromBytes(symbols); | ||
if (dwarf == null) { | ||
throwToolExit('Failed to decode symbols file'); | ||
} | ||
|
||
final Completer<void> onDone = Completer<void>(); | ||
StreamSubscription<void> subscription; | ||
subscription = input | ||
.transform(const Utf8Decoder()) | ||
.transform(const LineSplitter()) | ||
.transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true)) | ||
.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'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// 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:typed_data'; | ||
|
||
import 'package:file/memory.dart'; | ||
import 'package:flutter_tools/src/base/common.dart'; | ||
import 'package:flutter_tools/src/cache.dart'; | ||
import 'package:flutter_tools/src/commands/symbolize.dart'; | ||
import 'package:flutter_tools/src/convert.dart'; | ||
import 'package:mockito/mockito.dart'; | ||
|
||
import '../../src/common.dart'; | ||
import '../../src/context.dart'; | ||
import '../../src/mocks.dart'; | ||
|
||
|
||
void main() { | ||
MemoryFileSystem fileSystem; | ||
MockStdio stdio; | ||
SymbolizeCommand command; | ||
MockDwarfSymbolizationService mockDwarfSymbolizationService; | ||
|
||
setUpAll(() { | ||
Cache.disableLocking(); | ||
}); | ||
|
||
setUp(() { | ||
fileSystem = MemoryFileSystem.test(); | ||
stdio = MockStdio(); | ||
mockDwarfSymbolizationService = MockDwarfSymbolizationService(); | ||
command = SymbolizeCommand( | ||
stdio: stdio, | ||
fileSystem: fileSystem, | ||
dwarfSymbolizationService: mockDwarfSymbolizationService, | ||
); | ||
applyMocksToCommand(command); | ||
}); | ||
|
||
|
||
testUsingContext('symbolize exits when --debug-info argument is missing', () async { | ||
final Future<void> result = createTestCommandRunner(command) | ||
.run(const <String>['symbolize']); | ||
|
||
expect(result, throwsToolExit(message: '"--debug-info" is required to symbolicate stack traces.')); | ||
}); | ||
|
||
testUsingContext('symbolize exits when --debug-info file is missing', () async { | ||
final Future<void> result = createTestCommandRunner(command) | ||
.run(const <String>['symbolize', '--debug-info=app.debug']); | ||
|
||
expect(result, throwsToolExit(message: 'app.debug does not exist.')); | ||
}); | ||
|
||
testUsingContext('symbolize exits when --input file is missing', () async { | ||
fileSystem.file('app.debug').createSync(); | ||
final Future<void> result = createTestCommandRunner(command) | ||
.run(const <String>['symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result']); | ||
|
||
expect(result, throwsToolExit(message: '')); | ||
}); | ||
|
||
testUsingContext('symbolize succeedes when DwarfSymbolizationService does not throw', () async { | ||
fileSystem.file('app.debug').writeAsBytesSync(<int>[1, 2, 3]); | ||
fileSystem.file('foo.stack').writeAsStringSync('hello'); | ||
|
||
when(mockDwarfSymbolizationService.decode( | ||
input: anyNamed('input'), | ||
output: anyNamed('output'), | ||
symbols: anyNamed('symbols')) | ||
).thenAnswer((Invocation invocation) async { | ||
// Data is passed correctly to service | ||
expect((await (invocation.namedArguments[#input] as Stream<List<int>>).toList()).first, | ||
utf8.encode('hello')); | ||
expect(invocation.namedArguments[#symbols] as Uint8List, <int>[1, 2, 3,]); | ||
return; | ||
}); | ||
|
||
await createTestCommandRunner(command) | ||
.run(const <String>['symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result']); | ||
}); | ||
|
||
testUsingContext('symbolize throws when DwarfSymbolizationService throws', () async { | ||
fileSystem.file('app.debug').writeAsBytesSync(<int>[1, 2, 3]); | ||
fileSystem.file('foo.stack').writeAsStringSync('hello'); | ||
|
||
when(mockDwarfSymbolizationService.decode( | ||
input: anyNamed('input'), | ||
output: anyNamed('output'), | ||
symbols: anyNamed('symbols')) | ||
).thenThrow(ToolExit('test')); | ||
|
||
expect( | ||
createTestCommandRunner(command).run(const <String>[ | ||
'symbolize', '--debug-info=app.debug', '--input=foo.stack', '--output=results/foo.result']), | ||
throwsToolExit(message: 'test'), | ||
); | ||
}); | ||
} | ||
|
||
class MockDwarfSymbolizationService extends Mock implements DwarfSymbolizationService {} |