symbolize.dart 6.88 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 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';

17
/// Support for symbolizing a Dart stack trace.
18 19
///
/// This command accepts either paths to an input file containing the
20
/// stack trace and an output file for the symbolizing trace to be
21 22 23 24
/// written, or it accepts a stack trace over stdin and outputs it
/// over stdout.
class SymbolizeCommand extends FlutterCommand {
  SymbolizeCommand({
25 26
    required Stdio stdio,
    required FileSystem fileSystem,
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
    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',
46
      help: 'A file path for a symbolized stack trace to be written to.'
47 48 49 50 51 52 53 54
    );
  }

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

  @override
55
  String get description => 'Symbolize a stack trace from an AOT-compiled Flutter app.';
56 57 58 59

  @override
  String get name => 'symbolize';

60 61 62
  @override
  final String category = FlutterCommandCategory.tools;

63 64 65 66 67
  @override
  bool get shouldUpdateCache => false;

  @override
  Future<void> validateCommand() {
68
    if (argResults?.wasParsed('debug-info') != true) {
69
      throwToolExit('"--debug-info" is required to symbolize stack traces.');
70
    }
71
    final String debugInfoPath = stringArg('debug-info')!;
72 73 74 75
    if (debugInfoPath.endsWith('.dSYM')
        ? !_fileSystem.isDirectorySync(debugInfoPath)
        : !_fileSystem.isFileSync(debugInfoPath)) {
      throwToolExit('$debugInfoPath does not exist.');
76
    }
77 78
    if ((argResults?.wasParsed('input') ?? false) && !_fileSystem.isFileSync(stringArg('input')!)) {
      throwToolExit('${stringArg('input')} does not exist.');
79 80 81 82 83 84 85 86 87 88
    }
    return super.validateCommand();
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
    Stream<List<int>> input;
    IOSink output;

    // Configure output to either specified file or stdout.
89
    if (argResults?.wasParsed('output') ?? false) {
90
      final File outputFile = _fileSystem.file(stringArg('output'));
91 92 93
      if (!outputFile.parent.existsSync()) {
        outputFile.parent.createSync(recursive: true);
      }
94
      output = outputFile.openWrite();
95 96 97 98 99 100 101 102 103 104
    } 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.
105
    if (argResults?.wasParsed('input') ?? false) {
106
      input = _fileSystem.file(stringArg('input')).openRead();
107 108 109 110
    } else {
      input = _stdio.stdin;
    }

111
    String debugInfoPath = stringArg('debug-info')!;
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129

    // 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();
130 131 132 133 134 135 136 137 138 139
    await _dwarfSymbolizationService.decode(
      input: input,
      output: output,
      symbols: symbols,
    );

    return FlutterCommandResult.success();
  }
}

140 141 142
typedef SymbolsTransformer = StreamTransformer<String, String> Function(Uint8List);

StreamTransformer<String, String> _defaultTransformer(Uint8List symbols) {
143
  final Dwarf? dwarf = Dwarf.fromBytes(symbols);
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
  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();
    },
159
    handleError: (Object error, StackTrace stackTrace, EventSink<String> sink) {
160 161 162 163 164
      sink.addError(error, stackTrace);
    }
  );
}

165 166
/// A service which decodes stack traces from Dart applications.
class DwarfSymbolizationService {
167 168 169 170 171 172 173 174 175 176 177 178 179
  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;
180 181 182 183 184 185 186 187 188

  /// 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({
189 190 191
    required Stream<List<int>> input,
    required IOSink output,
    required Uint8List symbols,
192 193
  }) async {
    final Completer<void> onDone = Completer<void>();
194
    StreamSubscription<void>? subscription;
195
    subscription = input
196
      .cast<List<int>>()
197 198
      .transform(const Utf8Decoder())
      .transform(const LineSplitter())
199
      .transform(_transformer(symbols))
200 201 202
      .listen((String line) {
        try {
          output.writeln(line);
203
        } on Exception catch (e, s) {
204
          subscription?.cancel().whenComplete(() {
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
            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');
    }
  }
}