symbolize.dart 6.04 KB
Newer Older
1 2 3 4
// 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.

5 6
// @dart = 2.8

7 8 9 10 11 12 13 14 15 16 17 18
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';

19
/// Support for symbolizing a Dart stack trace.
20 21
///
/// This command accepts either paths to an input file containing the
22
/// stack trace and an output file for the symbolizing trace to be
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/// 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',
48
      help: 'A file path for a symbolized stack trace to be written to.'
49 50 51 52 53 54 55 56
    );
  }

  final Stdio _stdio;
  final FileSystem _fileSystem;
  final DwarfSymbolizationService _dwarfSymbolizationService;

  @override
57
  String get description => 'Symbolize a stack trace from an AOT-compiled Flutter app.';
58 59 60 61 62 63 64 65 66 67

  @override
  String get name => 'symbolize';

  @override
  bool get shouldUpdateCache => false;

  @override
  Future<void> validateCommand() {
    if (!argResults.wasParsed('debug-info')) {
68
      throwToolExit('"--debug-info" is required to symbolize stack traces.');
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
    }
    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);
      }
90
      output = outputFile.openWrite();
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    } 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();
  }
}

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
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: (dynamic error, StackTrace stackTrace, EventSink<String> sink) {
      sink.addError(error, stackTrace);
    }
  );
}

143 144
/// A service which decodes stack traces from Dart applications.
class DwarfSymbolizationService {
145 146 147 148 149 150 151 152 153 154 155 156 157
  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;
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

  /// 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
174
      .cast<List<int>>()
175 176
      .transform(const Utf8Decoder())
      .transform(const LineSplitter())
177
      .transform(_transformer(symbols))
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
      .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');
    }
  }
}