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