Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:async';
6 : import 'dart:math' as math;
7 :
8 : import 'frame.dart';
9 : import 'lazy_chain.dart';
10 : import 'stack_zone_specification.dart';
11 : import 'trace.dart';
12 : import 'utils.dart';
13 :
14 : /// A function that handles errors in the zone wrapped by [Chain.capture].
15 : @Deprecated("Will be removed in stack_trace 2.0.0.")
16 : typedef void ChainHandler(error, Chain chain);
17 :
18 : /// An opaque key used to track the current [StackZoneSpecification].
19 : final _specKey = new Object();
20 :
21 : /// A chain of stack traces.
22 : ///
23 : /// A stack chain is a collection of one or more stack traces that collectively
24 : /// represent the path from [main] through nested function calls to a particular
25 : /// code location, usually where an error was thrown. Multiple stack traces are
26 : /// necessary when using asynchronous functions, since the program's stack is
27 : /// reset before each asynchronous callback is run.
28 : ///
29 : /// Stack chains can be automatically tracked using [Chain.capture]. This sets
30 : /// up a new [Zone] in which the current stack chain is tracked and can be
31 : /// accessed using [new Chain.current]. Any errors that would be top-leveled in
32 : /// the zone can be handled, along with their associated chains, with the
33 : /// `onError` callback. For example:
34 : ///
35 : /// Chain.capture(() {
36 : /// // ...
37 : /// }, onError: (error, stackChain) {
38 : /// print("Caught error $error\n"
39 : /// "$stackChain");
40 : /// });
41 : class Chain implements StackTrace {
42 : /// The stack traces that make up this chain.
43 : ///
44 : /// Like the frames in a stack trace, the traces are ordered from most local
45 : /// to least local. The first one is the trace where the actual exception was
46 : /// raised, the second one is where that callback was scheduled, and so on.
47 : final List<Trace> traces;
48 :
49 : /// The [StackZoneSpecification] for the current zone.
50 0 : static StackZoneSpecification get _currentSpec => Zone.current[_specKey];
51 :
52 : /// If [when] is `true`, runs [callback] in a [Zone] in which the current
53 : /// stack chain is tracked and automatically associated with (most) errors.
54 : ///
55 : /// If [when] is `false`, this does not track stack chains. Instead, it's
56 : /// identical to [runZoned], except that it wraps any errors in [new
57 : /// Chain.forTrace]—which will only wrap the trace unless there's a different
58 : /// [Chain.capture] active. This makes it easy for the caller to only capture
59 : /// stack chains in debug mode or during development.
60 : ///
61 : /// If [onError] is passed, any error in the zone that would otherwise go
62 : /// unhandled is passed to it, along with the [Chain] associated with that
63 : /// error. Note that if [callback] produces multiple unhandled errors,
64 : /// [onError] may be called more than once. If [onError] isn't passed, the
65 : /// parent Zone's `unhandledErrorHandler` will be called with the error and
66 : /// its chain.
67 : ///
68 : /// Note that even if [onError] isn't passed, this zone will still be an error
69 : /// zone. This means that any errors that would cross the zone boundary are
70 : /// considered unhandled.
71 : ///
72 : /// If [callback] returns a value, it will be returned by [capture] as well.
73 : static T capture<T>(T callback(),
74 : {void onError(error, Chain chain), bool when: true}) {
75 : if (!when) {
76 : var newOnError;
77 : if (onError != null) {
78 : newOnError = (error, stackTrace) {
79 0 : onError(
80 : error,
81 : stackTrace == null
82 0 : ? new Chain.current()
83 0 : : new Chain.forTrace(stackTrace));
84 : };
85 : }
86 :
87 0 : return runZoned(callback, onError: newOnError);
88 : }
89 :
90 5 : var spec = new StackZoneSpecification(onError);
91 5 : return runZoned(() {
92 : try {
93 5 : return callback();
94 : } catch (error, stackTrace) {
95 : // TODO(nweiz): Don't special-case this when issue 19566 is fixed.
96 0 : Zone.current.handleUncaughtError(error, stackTrace);
97 : return null;
98 : }
99 : },
100 5 : zoneSpecification: spec.toSpec(),
101 15 : zoneValues: {_specKey: spec, StackZoneSpecification.disableKey: false});
102 : }
103 :
104 : /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
105 : /// [callback] in a [Zone] in which chain capturing is disabled.
106 : ///
107 : /// If [callback] returns a value, it will be returned by [disable] as well.
108 : static T disable<T>(T callback(), {bool when: true}) {
109 : var zoneValues =
110 0 : when ? {_specKey: null, StackZoneSpecification.disableKey: true} : null;
111 :
112 0 : return runZoned(callback, zoneValues: zoneValues);
113 : }
114 :
115 : /// Returns [futureOrStream] unmodified.
116 : ///
117 : /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
118 : /// exceptions reported with [Completer.completeError] and
119 : /// [StreamController.addError] were tracked correctly.
120 : @Deprecated("Chain.track is not necessary in Dart 1.7+.")
121 : static track(futureOrStream) => futureOrStream;
122 :
123 : /// Returns the current stack chain.
124 : ///
125 : /// By default, the first frame of the first trace will be the line where
126 : /// [Chain.current] is called. If [level] is passed, the first trace will
127 : /// start that many frames up instead.
128 : ///
129 : /// If this is called outside of a [capture] zone, it just returns a
130 : /// single-trace chain.
131 : factory Chain.current([int level = 0]) {
132 0 : if (_currentSpec != null) return _currentSpec.currentChain(level + 1);
133 :
134 0 : var chain = new Chain.forTrace(StackTrace.current);
135 0 : return new LazyChain(() {
136 : // JS includes a frame for the call to StackTrace.current, but the VM
137 : // doesn't, so we skip an extra frame in a JS context.
138 0 : var first = new Trace(
139 0 : chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
140 0 : original: chain.traces.first.original.toString());
141 0 : return new Chain([first]..addAll(chain.traces.skip(1)));
142 : });
143 : }
144 :
145 : /// Returns the stack chain associated with [trace].
146 : ///
147 : /// The first stack trace in the returned chain will always be [trace]
148 : /// (converted to a [Trace] if necessary). If there is no chain associated
149 : /// with [trace] or if this is called outside of a [capture] zone, this just
150 : /// returns a single-trace chain containing [trace].
151 : ///
152 : /// If [trace] is already a [Chain], it will be returned as-is.
153 : factory Chain.forTrace(StackTrace trace) {
154 0 : if (trace is Chain) return trace;
155 0 : if (_currentSpec != null) return _currentSpec.chainFor(trace);
156 0 : return new LazyChain(() => new Chain.parse(trace.toString()));
157 : }
158 :
159 : /// Parses a string representation of a stack chain.
160 : ///
161 : /// If [chain] is the output of a call to [Chain.toString], it will be parsed
162 : /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
163 : /// and returned as a single-trace chain.
164 : factory Chain.parse(String chain) {
165 0 : if (chain.isEmpty) return new Chain([]);
166 0 : if (chain.contains(vmChainGap)) {
167 0 : return new Chain(
168 0 : chain.split(vmChainGap).map((trace) => new Trace.parseVM(trace)));
169 : }
170 0 : if (!chain.contains(chainGap)) return new Chain([new Trace.parse(chain)]);
171 :
172 0 : return new Chain(
173 0 : chain.split(chainGap).map((trace) => new Trace.parseFriendly(trace)));
174 : }
175 :
176 : /// Returns a new [Chain] comprised of [traces].
177 0 : Chain(Iterable<Trace> traces) : traces = new List<Trace>.unmodifiable(traces);
178 :
179 : /// Returns a terser version of [this].
180 : ///
181 : /// This calls [Trace.terse] on every trace in [traces], and discards any
182 : /// trace that contain only internal frames.
183 : ///
184 : /// This won't do anything with a raw JavaScript trace, since there's no way
185 : /// to determine which frames come from which Dart libraries. However, the
186 : /// [`source_map_stack_trace`][source_map_stack_trace] package can be used to
187 : /// convert JavaScript traces into Dart-style traces.
188 : ///
189 : /// [source_map_stack_trace]: https://pub.dartlang.org/packages/source_map_stack_trace
190 0 : Chain get terse => foldFrames((_) => false, terse: true);
191 :
192 : /// Returns a new [Chain] based on [this] where multiple stack frames matching
193 : /// [predicate] are folded together.
194 : ///
195 : /// This means that whenever there are multiple frames in a row that match
196 : /// [predicate], only the last one is kept. In addition, traces that are
197 : /// composed entirely of frames matching [predicate] are omitted.
198 : ///
199 : /// This is useful for limiting the amount of library code that appears in a
200 : /// stack trace by only showing user code and code that's called by user code.
201 : ///
202 : /// If [terse] is true, this will also fold together frames from the core
203 : /// library or from this package, and simplify core library frames as in
204 : /// [Trace.terse].
205 : Chain foldFrames(bool predicate(Frame frame), {bool terse: false}) {
206 : var foldedTraces =
207 0 : traces.map((trace) => trace.foldFrames(predicate, terse: terse));
208 0 : var nonEmptyTraces = foldedTraces.where((trace) {
209 : // Ignore traces that contain only folded frames.
210 0 : if (trace.frames.length > 1) return true;
211 0 : if (trace.frames.isEmpty) return false;
212 :
213 : // In terse mode, the trace may have removed an outer folded frame,
214 : // leaving a single non-folded frame. We can detect a folded frame because
215 : // it has no line information.
216 : if (!terse) return false;
217 0 : return trace.frames.single.line != null;
218 : });
219 :
220 : // If all the traces contain only internal processing, preserve the last
221 : // (top-most) one so that the chain isn't empty.
222 0 : if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
223 0 : return new Chain([foldedTraces.last]);
224 : }
225 :
226 0 : return new Chain(nonEmptyTraces);
227 : }
228 :
229 : /// Converts [this] to a [Trace].
230 : ///
231 : /// The trace version of a chain is just the concatenation of all the traces
232 : /// in the chain.
233 0 : Trace toTrace() => new Trace(traces.expand((trace) => trace.frames));
234 :
235 : String toString() {
236 : // Figure out the longest path so we know how much to pad.
237 0 : var longest = traces.map((trace) {
238 0 : return trace.frames
239 0 : .map((frame) => frame.location.length)
240 0 : .fold(0, math.max);
241 0 : }).fold(0, math.max);
242 :
243 : // Don't call out to [Trace.toString] here because that doesn't ensure that
244 : // padding is consistent across all traces.
245 0 : return traces.map((trace) {
246 0 : return trace.frames.map((frame) {
247 0 : return '${frame.location.padRight(longest)} ${frame.member}\n';
248 0 : }).join();
249 0 : }).join(chainGap);
250 : }
251 : }
|