OLD | NEW |
| (Empty) |
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 library stack_trace.chain; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection'; | |
9 import 'dart:math' as math; | |
10 | |
11 import 'frame.dart'; | |
12 import 'stack_zone_specification.dart'; | |
13 import 'trace.dart'; | |
14 import 'utils.dart'; | |
15 | |
16 /// A function that handles errors in the zone wrapped by [Chain.capture]. | |
17 typedef void ChainHandler(error, Chain chain); | |
18 | |
19 /// The line used in the string representation of stack chains to represent | |
20 /// the gap between traces. | |
21 const _gap = '===== asynchronous gap ===========================\n'; | |
22 | |
23 /// A chain of stack traces. | |
24 /// | |
25 /// A stack chain is a collection of one or more stack traces that collectively | |
26 /// represent the path from [main] through nested function calls to a particular | |
27 /// code location, usually where an error was thrown. Multiple stack traces are | |
28 /// necessary when using asynchronous functions, since the program's stack is | |
29 /// reset before each asynchronous callback is run. | |
30 /// | |
31 /// Stack chains can be automatically tracked using [Chain.capture]. This sets | |
32 /// up a new [Zone] in which the current stack chain is tracked and can be | |
33 /// accessed using [new Chain.current]. Any errors that would be top-leveled in | |
34 /// the zone can be handled, along with their associated chains, with the | |
35 /// `onError` callback. For example: | |
36 /// | |
37 /// Chain.capture(() { | |
38 /// // ... | |
39 /// }, onError: (error, stackChain) { | |
40 /// print("Caught error $error\n" | |
41 /// "$stackChain"); | |
42 /// }); | |
43 class Chain implements StackTrace { | |
44 | |
45 /// The stack traces that make up this chain. | |
46 /// | |
47 /// Like the frames in a stack trace, the traces are ordered from most local | |
48 /// to least local. The first one is the trace where the actual exception was | |
49 /// raised, the second one is where that callback was scheduled, and so on. | |
50 final List<Trace> traces; | |
51 | |
52 /// The [StackZoneSpecification] for the current zone. | |
53 static StackZoneSpecification get _currentSpec => | |
54 Zone.current[#stack_trace.stack_zone.spec]; | |
55 | |
56 /// Runs [callback] in a [Zone] in which the current stack chain is tracked | |
57 /// and automatically associated with (most) errors. | |
58 /// | |
59 /// If [onError] is passed, any error in the zone that would otherwise go | |
60 /// unhandled is passed to it, along with the [Chain] associated with that | |
61 /// error. Note that if [callback] produces multiple unhandled errors, | |
62 /// [onError] may be called more than once. If [onError] isn't passed, the | |
63 /// parent Zone's `unhandledErrorHandler` will be called with the error and | |
64 /// its chain. | |
65 /// | |
66 /// Note that even if [onError] isn't passed, this zone will still be an error | |
67 /// zone. This means that any errors that would cross the zone boundary are | |
68 /// considered unhandled. | |
69 /// | |
70 /// If [callback] returns a value, it will be returned by [capture] as well. | |
71 static capture(callback(), {ChainHandler onError}) { | |
72 var spec = new StackZoneSpecification(onError); | |
73 return runZoned(() { | |
74 try { | |
75 return callback(); | |
76 } catch (error, stackTrace) { | |
77 // TODO(nweiz): Don't special-case this when issue 19566 is fixed. | |
78 return Zone.current.handleUncaughtError(error, stackTrace); | |
79 } | |
80 }, zoneSpecification: spec.toSpec(), zoneValues: { | |
81 #stack_trace.stack_zone.spec: spec | |
82 }); | |
83 } | |
84 | |
85 /// Returns [futureOrStream] unmodified. | |
86 /// | |
87 /// Prior to Dart 1.7, this was necessary to ensure that stack traces for | |
88 /// exceptions reported with [Completer.completeError] and | |
89 /// [StreamController.addError] were tracked correctly. | |
90 @Deprecated("Chain.track is not necessary in Dart 1.7+.") | |
91 static track(futureOrStream) => futureOrStream; | |
92 | |
93 /// Returns the current stack chain. | |
94 /// | |
95 /// By default, the first frame of the first trace will be the line where | |
96 /// [Chain.current] is called. If [level] is passed, the first trace will | |
97 /// start that many frames up instead. | |
98 /// | |
99 /// If this is called outside of a [capture] zone, it just returns a | |
100 /// single-trace chain. | |
101 factory Chain.current([int level=0]) { | |
102 if (_currentSpec != null) return _currentSpec.currentChain(level + 1); | |
103 return new Chain([new Trace.current(level + 1)]); | |
104 } | |
105 | |
106 /// Returns the stack chain associated with [trace]. | |
107 /// | |
108 /// The first stack trace in the returned chain will always be [trace] | |
109 /// (converted to a [Trace] if necessary). If there is no chain associated | |
110 /// with [trace] or if this is called outside of a [capture] zone, this just | |
111 /// returns a single-trace chain containing [trace]. | |
112 /// | |
113 /// If [trace] is already a [Chain], it will be returned as-is. | |
114 factory Chain.forTrace(StackTrace trace) { | |
115 if (trace is Chain) return trace; | |
116 if (_currentSpec == null) return new Chain([new Trace.from(trace)]); | |
117 return _currentSpec.chainFor(trace); | |
118 } | |
119 | |
120 /// Parses a string representation of a stack chain. | |
121 /// | |
122 /// Specifically, this parses the output of [Chain.toString]. | |
123 factory Chain.parse(String chain) { | |
124 if (chain.isEmpty) return new Chain([]); | |
125 return new Chain( | |
126 chain.split(_gap).map((trace) => new Trace.parseFriendly(trace))); | |
127 } | |
128 | |
129 /// Returns a new [Chain] comprised of [traces]. | |
130 Chain(Iterable<Trace> traces) | |
131 : traces = new UnmodifiableListView<Trace>(traces.toList()); | |
132 | |
133 /// Returns a terser version of [this]. | |
134 /// | |
135 /// This calls [Trace.terse] on every trace in [traces], and discards any | |
136 /// trace that contain only internal frames. | |
137 Chain get terse => foldFrames((_) => false, terse: true); | |
138 | |
139 /// Returns a new [Chain] based on [this] where multiple stack frames matching | |
140 /// [predicate] are folded together. | |
141 /// | |
142 /// This means that whenever there are multiple frames in a row that match | |
143 /// [predicate], only the last one is kept. In addition, traces that are | |
144 /// composed entirely of frames matching [predicate] are omitted. | |
145 /// | |
146 /// This is useful for limiting the amount of library code that appears in a | |
147 /// stack trace by only showing user code and code that's called by user code. | |
148 /// | |
149 /// If [terse] is true, this will also fold together frames from the core | |
150 /// library or from this package, and simplify core library frames as in | |
151 /// [Trace.terse]. | |
152 Chain foldFrames(bool predicate(Frame frame), {bool terse: false}) { | |
153 var foldedTraces = traces.map( | |
154 (trace) => trace.foldFrames(predicate, terse: terse)); | |
155 var nonEmptyTraces = foldedTraces.where((trace) { | |
156 // Ignore traces that contain only folded frames. | |
157 if (trace.frames.length > 1) return true; | |
158 | |
159 // In terse mode, the trace may have removed an outer folded frame, | |
160 // leaving a single non-folded frame. We can detect a folded frame because | |
161 // it has no line information. | |
162 if (!terse) return false; | |
163 return trace.frames.single.line != null; | |
164 }); | |
165 | |
166 // If all the traces contain only internal processing, preserve the last | |
167 // (top-most) one so that the chain isn't empty. | |
168 if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) { | |
169 return new Chain([foldedTraces.last]); | |
170 } | |
171 | |
172 return new Chain(nonEmptyTraces); | |
173 } | |
174 | |
175 /// Converts [this] to a [Trace]. | |
176 /// | |
177 /// The trace version of a chain is just the concatenation of all the traces | |
178 /// in the chain. | |
179 Trace toTrace() => new Trace(flatten(traces.map((trace) => trace.frames))); | |
180 | |
181 String toString() { | |
182 // Figure out the longest path so we know how much to pad. | |
183 var longest = traces.map((trace) { | |
184 return trace.frames.map((frame) => frame.location.length) | |
185 .fold(0, math.max); | |
186 }).fold(0, math.max); | |
187 | |
188 // Don't call out to [Trace.toString] here because that doesn't ensure that | |
189 // padding is consistent across all traces. | |
190 return traces.map((trace) { | |
191 return trace.frames.map((frame) { | |
192 return '${padRight(frame.location, longest)} ${frame.member}\n'; | |
193 }).join(); | |
194 }).join(_gap); | |
195 } | |
196 } | |
OLD | NEW |