Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Commit

Permalink
Add "flutter symbolize" command (flutter#49465)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonahwilliams authored Feb 10, 2020
1 parent ffc8559 commit 5681727
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 6 deletions.
3 changes: 2 additions & 1 deletion packages/flutter_tools/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ dart_library("flutter_tools") {
"//third_party/dart-pkg/pub/multi_server_socket",
"//third_party/dart-pkg/pub/multicast_dns",
"//third_party/dart-pkg/pub/mustache",
"//third_party/dart-pkg/pub/native_stack_traces",
"//third_party/dart-pkg/pub/node_preamble",
"//third_party/dart-pkg/pub/package_config",
"//third_party/dart-pkg/pub/path",
"//third_party/dart-pkg/pub/platform",
Expand All @@ -51,7 +53,6 @@ dart_library("flutter_tools") {
"//third_party/dart-pkg/pub/webkit_inspection_protocol",
"//third_party/dart-pkg/pub/xml",
"//third_party/dart-pkg/pub/yaml",
"//third_party/dart-pkg/pub/node_preamble",
]
}

Expand Down
6 changes: 6 additions & 0 deletions packages/flutter_tools/lib/executable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import 'src/commands/precache.dart';
import 'src/commands/run.dart';
import 'src/commands/screenshot.dart';
import 'src/commands/shell_completion.dart';
import 'src/commands/symbolize.dart';
import 'src/commands/test.dart';
import 'src/commands/train.dart';
import 'src/commands/unpack.dart';
import 'src/commands/update_packages.dart';
import 'src/commands/upgrade.dart';
import 'src/commands/version.dart';
import 'src/globals.dart' as globals;
import 'src/runner/flutter_command.dart';
import 'src/web/compile.dart';
import 'src/web/web_runner.dart';
Expand Down Expand Up @@ -94,6 +96,10 @@ Future<void> main(List<String> args) async {
UpdatePackagesCommand(hidden: !verboseHelp),
UpgradeCommand(),
VersionCommand(),
SymbolizeCommand(
stdio: globals.stdio,
fileSystem: globals.fs,
),
], verbose: verbose,
muteCommandLogging: muteCommandLogging,
verboseHelp: verboseHelp,
Expand Down
162 changes: 162 additions & 0 deletions packages/flutter_tools/lib/src/commands/symbolize.dart
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');
}
}
}
3 changes: 1 addition & 2 deletions packages/flutter_tools/lib/src/runner/flutter_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,7 @@ abstract class FlutterCommand extends Command<void> {
'symbol files can be stored for later use. These symbol files contain '
'the information needed to symbolize Dart stack traces. For an app built '
'with this flag, the \'flutter symbolize\' command with the right program '
'symbol file is required to obtain a human readable stack trace. This '
'command is tracked by https://github.com/flutter/flutter/issues/50206',
'symbol file is required to obtain a human readable stack trace.',
valueHelp: '/project-name/v1.2.3/',
);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/flutter_tools/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies:
webkit_inspection_protocol: 0.5.0
xml: 3.5.0
yaml: 2.2.0
native_stack_traces: 0.2.2
flutter_goldens_client:
path: ../flutter_goldens_client

Expand Down Expand Up @@ -62,12 +63,12 @@ dependencies:
build_config: 0.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
build_resolvers: 1.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
built_collection: 4.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
built_value: 7.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
built_value: 7.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
checked_yaml: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
devtools: 0.1.14 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
devtools: 0.1.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
devtools_server: 0.1.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fixnum: 0.10.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
Expand Down Expand Up @@ -127,4 +128,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true

# PUBSPEC CHECKSUM: 3a4b
# PUBSPEC CHECKSUM: 557d
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 {}

0 comments on commit 5681727

Please sign in to comment.