// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. library stack_trace.chain; import 'dart:async'; import 'dart:collection'; import 'stack_zone_specification.dart'; import 'trace.dart'; import 'utils.dart'; /// A function that handles errors in the zone wrapped by [Chain.capture]. typedef void ChainHandler(error, Chain chain); /// A chain of stack traces. /// /// A stack chain is a collection of one or more stack traces that collectively /// represent the path from [main] through nested function calls to a particular /// code location, usually where an error was thrown. Multiple stack traces are /// necessary when using asynchronous functions, since the program's stack is /// reset before each asynchronous callback is run. /// /// Stack chains can be automatically tracked using [Chain.capture]. This sets /// up a new [Zone] in which the current stack chain is tracked and can be /// accessed using [new Chain.current]. Any errors that would be top-leveled in /// the zone can be handled, along with their associated chains, with the /// `onError` callback. /// /// For the most part [Chain.capture] will notice when an error is thrown and /// associate the correct stack chain with it; the chain can be accessed using /// [new Chain.forTrace]. However, there are some cases where exceptions won't /// be automatically detected: any [Future] constructor, /// [Completer.completeError], [Stream.addError], and libraries that use these. /// For these, all you need to do is wrap the Future or Stream in a call to /// [Chain.track] and the errors will be tracked correctly. class Chain implements StackTrace { /// The line used in the string representation of stack chains to represent /// the gap between traces. static const _GAP = '===== asynchronous gap ===========================\n'; /// The stack traces that make up this chain. /// /// Like the frames in a stack trace, the traces are ordered from most local /// to least local. The first one is the trace where the actual exception was /// raised, the second one is where that callback was scheduled, and so on. final List traces; /// The [StackZoneSpecification] for the current zone. static StackZoneSpecification get _currentSpec => Zone.current[#stack_trace.stack_zone.spec]; /// Runs [callback] in a [Zone] in which the current stack chain is tracked /// and automatically associated with (most) errors. /// /// If [onError] is passed, any error in the zone that would otherwise go /// unhandled is passed to it, along with the [Chain] associated with that /// error. Note that if [callback] produces multiple unhandled errors, /// [onError] may be called more than once. If [onError] isn't passed, the /// parent Zone's `unhandledErrorHandler` will be called with the error and /// its chain. /// /// For the most part an error thrown in the zone will have the correct stack /// chain associated with it. However, there are some cases where exceptions /// won't be automatically detected: any [Future] constructor, /// [Completer.completeError], [Stream.addError], and libraries that use /// these. For these, all you need to do is wrap the Future or Stream in a /// call to [Chain.track] and the errors will be tracked correctly. /// /// Note that even if [onError] isn't passed, this zone will still be an error /// zone. This means that any errors that would cross the zone boundary are /// considered unhandled. /// /// If [callback] returns a value, it will be returned by [capture] as well. /// /// Currently, capturing stack chains doesn't work when using dart2js due to /// issues [15171] and [15105]. Stack chains reported on dart2js will contain /// only one trace. /// /// [15171]: https://code.google.com/p/dart/issues/detail?id=15171 /// [15105]: https://code.google.com/p/dart/issues/detail?id=15105 static capture(callback(), {ChainHandler onError}) { var spec = new StackZoneSpecification(onError); return runZoned(callback, zoneSpecification: spec.toSpec(), zoneValues: { #stack_trace.stack_zone.spec: spec }); } /// Ensures that any errors emitted by [futureOrStream] have the correct stack /// chain information associated with them. /// /// For the most part an error thrown within a [capture] zone will have the /// correct stack chain automatically associated with it. However, there are /// some cases where exceptions won't be automatically detected: any [Future] /// constructor, [Completer.completeError], [Stream.addError], and libraries /// that use these. /// /// This returns a [Future] or [Stream] that will emit the same values and /// errors as [futureOrStream]. The only exception is that if [futureOrStream] /// emits an error without a stack trace, one will be added in the return /// value. /// /// If this is called outside of a [capture] zone, it just returns /// [futureOrStream] as-is. /// /// As the name suggests, [futureOrStream] may be either a [Future] or a /// [Stream]. static track(futureOrStream) { if (_currentSpec == null) return futureOrStream; if (futureOrStream is Future) { return _currentSpec.trackFuture(futureOrStream, 1); } else { return _currentSpec.trackStream(futureOrStream, 1); } } /// Returns the current stack chain. /// /// By default, the first frame of the first trace will be the line where /// [Chain.current] is called. If [level] is passed, the first trace will /// start that many frames up instead. /// /// If this is called outside of a [capture] zone, it just returns a /// single-trace chain. factory Chain.current([int level=0]) { if (_currentSpec != null) return _currentSpec.currentChain(level + 1); return new Chain([new Trace.current(level + 1)]); } /// Returns the stack chain associated with [trace]. /// /// The first stack trace in the returned chain will always be [trace] /// (converted to a [Trace] if necessary). If there is no chain associated /// with [trace] or if this is called outside of a [capture] zone, this just /// returns a single-trace chain containing [trace]. /// /// If [trace] is already a [Chain], it will be returned as-is. factory Chain.forTrace(StackTrace trace) { if (trace is Chain) return trace; if (_currentSpec == null) return new Chain([new Trace.from(trace)]); return _currentSpec.chainFor(trace); } /// Parses a string representation of a stack chain. /// /// Specifically, this parses the output of [Chain.toString]. factory Chain.parse(String chain) => new Chain(chain.split(_GAP).map((trace) => new Trace.parseFriendly(trace))); /// Returns a new [Chain] comprised of [traces]. Chain(Iterable traces) : traces = new UnmodifiableListView(traces.toList()); /// Returns a terser version of [this]. /// /// This calls [Trace.terse] on every trace in [traces], and discards any /// trace that contain only internal frames. Chain get terse { var terseTraces = traces.map((trace) => trace.terse); var nonEmptyTraces = terseTraces.where((trace) { // Ignore traces that contain only internal processing. return trace.frames.length > 1; }); // If all the traces contain only internal processing, preserve the last // (top-most) one so that the chain isn't empty. if (nonEmptyTraces.isEmpty && terseTraces.isNotEmpty) { return new Chain([terseTraces.last]); } return new Chain(nonEmptyTraces); } /// Converts [this] to a [Trace]. /// /// The trace version of a chain is just the concatenation of all the traces /// in the chain. Trace toTrace() => new Trace(flatten(traces.map((trace) => trace.frames))); String toString() => traces.join(_GAP); }