diff --git a/gulpfile.js b/gulpfile.js index 0ef8f08720..fa29b2a741 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,6 +31,7 @@ var _HTLM_DEFAULT_SCRIPTS_JS = [ {src: '/rtts_assert/lib/rtts_assert.js', mimeType: 'text/javascript'}, {src: '/deps/es6-module-loader-sans-promises.src.js', mimeType: 'text/javascript'}, {src: '/deps/zone.js', mimeType: 'text/javascript'}, + {src: '/deps/long-stack-trace-zone.js', mimeType: 'text/javascript'}, {src: '/deps/system.src.js', mimeType: 'text/javascript'}, {src: '/deps/extension-register.js', mimeType: 'text/javascript'}, {src: '/deps/runtime_paths.js', mimeType: 'text/javascript'}, @@ -68,6 +69,7 @@ var CONFIG = { "node_modules/systemjs/dist/system.src.js", "node_modules/systemjs/lib/extension-register.js", "node_modules/zone.js/zone.js", + "node_modules/zone.js/long-stack-trace-zone.js", "tools/build/runtime_paths.js", "node_modules/angular/angular.js" ] diff --git a/karma-js.conf.js b/karma-js.conf.js index 3c995e1fda..1fecb6ae1b 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -20,6 +20,7 @@ module.exports = function(config) { 'node_modules/systemjs/dist/system.src.js', 'node_modules/systemjs/lib/extension-register.js', 'node_modules/zone.js/zone.js', + 'node_modules/zone.js/long-stack-trace-zone.js', 'tools/build/file2modulename.js', 'test-main.js' diff --git a/modules/core/pubspec.yaml b/modules/core/pubspec.yaml index b301e29f39..0205544945 100644 --- a/modules/core/pubspec.yaml +++ b/modules/core/pubspec.yaml @@ -2,6 +2,7 @@ name: core environment: sdk: '>=1.4.0' dependencies: + stack_trace: '1.1.1' change_detection: path: ../change_detection di: diff --git a/modules/core/src/application.js b/modules/core/src/application.js index 2ccf94934f..fd6edabb6e 100644 --- a/modules/core/src/application.js +++ b/modules/core/src/application.js @@ -1,5 +1,5 @@ import {Injector, bind, OpaqueToken} from 'di/di'; -import {Type, FIELD, isBlank, isPresent, BaseException, assertionsEnabled} from 'facade/lang'; +import {Type, FIELD, isBlank, isPresent, BaseException, assertionsEnabled, print} from 'facade/lang'; import {DOM, Element} from 'facade/dom'; import {Compiler, CompilerCache} from './compiler/compiler'; import {ProtoView} from './compiler/view'; @@ -11,7 +11,8 @@ import {RecordRange} from 'change_detection/record_range'; import {TemplateLoader} from './compiler/template_loader'; import {DirectiveMetadataReader} from './compiler/directive_metadata_reader'; import {AnnotatedType} from './compiler/annotated_type'; -import {ListWrapper} from 'facade/collection'; +import {List, ListWrapper} from 'facade/collection'; +import {PromiseWrapper} from 'facade/async'; import {VmTurnZone} from 'core/zone/vm_turn_zone'; import {LifeCycle} from 'core/life_cycle/life_cycle'; @@ -77,24 +78,47 @@ function _injectorBindings(appComponentType) { documentDependentBindings(appComponentType)); } +function _createVmZone(givenReporter:Function){ + var defaultErrorReporter = (exception, stackTrace) => { + var longStackTrace = ListWrapper.join(stackTrace, "\n\n-----async gap-----\n"); + print(`${exception}\n\n${longStackTrace}`); + throw exception; + }; + + var reporter = isPresent(givenReporter) ? givenReporter : defaultErrorReporter; + + var zone = new VmTurnZone({enableLongStackTrace: assertionsEnabled()}); + zone.initCallbacks({onErrorHandler: reporter}); + return zone; +} + // Multiple calls to this method are allowed. Each application would only share // _rootInjector, which is not user-configurable by design, thus safe to share. -export function bootstrap(appComponentType: Type, bindings=null) { - // TODO(rado): prepopulate template cache, so applications with only - // index.html and main.js are possible. +export function bootstrap(appComponentType: Type, bindings=null, givenBootstrapErrorReporter=null) { + var bootstrapProcess = PromiseWrapper.completer(); + + var zone = _createVmZone(givenBootstrapErrorReporter); + zone.run(() => { + // TODO(rado): prepopulate template cache, so applications with only + // index.html and main.js are possible. - var zone = new VmTurnZone(); - return zone.run(() => { if (isBlank(_rootInjector)) _rootInjector = new Injector(_rootBindings); var appInjector = _rootInjector.createChild(_injectorBindings(appComponentType)); if (isPresent(bindings)) appInjector = appInjector.createChild(bindings); - return appInjector.asyncGet(LifeCycle). - then((lc) => { - lc.registerWith(zone); - lc.tick(); - }). - then((_) => appInjector); + PromiseWrapper.then(appInjector.asyncGet(LifeCycle), + (lc) => { + lc.registerWith(zone); + lc.tick(); //the first tick that will bootstrap the app + + bootstrapProcess.complete(appInjector); + }, + + (err) => { + bootstrapProcess.reject(err) + }); }); + + return bootstrapProcess.promise; } diff --git a/modules/core/src/life_cycle/life_cycle.js b/modules/core/src/life_cycle/life_cycle.js index 417cb5fef0..6654a91d19 100644 --- a/modules/core/src/life_cycle/life_cycle.js +++ b/modules/core/src/life_cycle/life_cycle.js @@ -1,6 +1,7 @@ -import {FIELD} from 'facade/lang'; +import {FIELD, print} from 'facade/lang'; import {ChangeDetector} from 'change_detection/change_detector'; import {VmTurnZone} from 'core/zone/vm_turn_zone'; +import {ListWrapper} from 'facade/collection'; export class LifeCycle { _changeDetector:ChangeDetector; @@ -10,7 +11,15 @@ export class LifeCycle { } registerWith(zone:VmTurnZone) { + // temporary error handler, we should inject one + var errorHandler = (exception, stackTrace) => { + var longStackTrace = ListWrapper.join(stackTrace, "\n\n-----async gap-----\n"); + print(`${exception}\n\n${longStackTrace}`); + throw exception; + }; + zone.initCallbacks({ + onErrorHandler: errorHandler, onTurnDone: () => this.tick() }); } diff --git a/modules/core/src/zone/vm_turn_zone.dart b/modules/core/src/zone/vm_turn_zone.dart index 06f63c3b38..e5d0f4e664 100644 --- a/modules/core/src/zone/vm_turn_zone.dart +++ b/modules/core/src/zone/vm_turn_zone.dart @@ -1,45 +1,68 @@ library angular.zone; import 'dart:async' as async; +import 'package:stack_trace/stack_trace.dart' show Chain; class VmTurnZone { Function _onTurnStart; Function _onTurnDone; Function _onScheduleMicrotask; + Function _onErrorHandler; async.Zone _outerZone; async.Zone _innerZone; int _nestedRunCounter; - VmTurnZone() { + VmTurnZone({bool enableLongStackTrace}) { _nestedRunCounter = 0; _outerZone = async.Zone.current; - _innerZone = _outerZone.fork(specification: new async.ZoneSpecification( + _innerZone = _createInnerZoneWithErrorHandling(enableLongStackTrace); + } + + initCallbacks({Function onTurnStart, Function onTurnDone, Function onScheduleMicrotask, Function onErrorHandler}) { + this._onTurnStart = onTurnStart; + this._onTurnDone = onTurnDone; + this._onScheduleMicrotask = onScheduleMicrotask; + this._onErrorHandler = onErrorHandler; + } + + dynamic run(fn()) => _innerZone.run(fn); + + dynamic runOutsideAngular(fn()) => _outerZone.run(fn); + + + async.Zone _createInnerZoneWithErrorHandling(bool enableLongStackTrace) { + if (enableLongStackTrace) { + return Chain.capture(() { + return _createInnerZone(async.Zone.current); + }, onError: this._onErrorWithLongStackTrace); + } else { + return async.runZoned(() { + return _createInnerZone(async.Zone.current); + }, onError: this._onErrorWithoutLongStackTrace); + } + } + + async.Zone _createInnerZone(async.Zone zone) { + return zone.fork(specification: new async.ZoneSpecification( run: _onRun, runUnary: _onRunUnary, scheduleMicrotask: _onMicrotask )); } - - initCallbacks({Function onTurnStart, Function onTurnDone, Function onScheduleMicrotask}) { - this._onTurnStart = onTurnStart; - this._onTurnDone = onTurnDone; - this._onScheduleMicrotask = onScheduleMicrotask; - } - - dynamic run(fn()) => _innerZone.run(fn); - - dynamic runOutsideAngular(fn()) => _outerZone.run(fn); - dynamic _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { _nestedRunCounter++; try { if (_nestedRunCounter == 1 && _onTurnStart != null) delegate.run(zone, _onTurnStart); - return fn(); - + } catch (e, s) { + if (_onErrorHandler != null && _nestedRunCounter == 1) { + _onErrorHandler(e, [s.toString()]); + } else { + rethrow; + } } finally { _nestedRunCounter--; if (_nestedRunCounter == 0 && _onTurnDone != null) _finishTurn(zone, delegate); @@ -47,10 +70,10 @@ class VmTurnZone { } dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) => - _onRunBase(self, delegate, zone, () => delegate.run(zone, fn)); + _onRunBase(self, delegate, zone, () => delegate.run(zone, fn)); dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn(args), args) => - _onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args)); + _onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args)); void _finishTurn(zone, delegate) { delegate.run(zone, _onTurnDone); @@ -58,9 +81,25 @@ class VmTurnZone { _onMicrotask(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn) { if (this._onScheduleMicrotask != null) { - this._onScheduleMicrotask(fn); + _onScheduleMicrotask(fn); } else { delegate.scheduleMicrotask(zone, fn); } } + + _onErrorWithLongStackTrace(exception, Chain chain) { + final traces = chain.terse.traces.map((t) => t.toString()).toList(); + _onError(exception, traces, chain.traces[0]); + } + _onErrorWithoutLongStackTrace(exception, StackTrace trace) { + _onError(exception, [trace.toString()], trace); + } + + _onError(exception, List traces, StackTrace singleTrace) { + if (_onErrorHandler != null) { + _onErrorHandler(exception, traces); + } else { + _outerZone.handleUncaughtError(exception, singleTrace); + } + } } diff --git a/modules/core/src/zone/vm_turn_zone.es6 b/modules/core/src/zone/vm_turn_zone.es6 index 4a00dafc66..2377c0d6a7 100644 --- a/modules/core/src/zone/vm_turn_zone.es6 +++ b/modules/core/src/zone/vm_turn_zone.es6 @@ -1,5 +1,5 @@ -import {List, ListWrapper} from 'facade/collection'; -import {normalizeBlank} from 'facade/lang'; +import {List, ListWrapper, StringMapWrapper} from 'facade/collection'; +import {normalizeBlank, isPresent} from 'facade/lang'; export class VmTurnZone { _outerZone; @@ -7,24 +7,24 @@ export class VmTurnZone { _onTurnStart:Function; _onTurnDone:Function; + _onErrorHandler:Function; _nestedRunCounter:number; - constructor() { + constructor({enableLongStackTrace}) { this._nestedRunCounter = 0; this._onTurnStart = null; this._onTurnDone = null; + this._onErrorHandler = null; this._outerZone = window.zone; - this._innerZone = this._outerZone.fork({ - beforeTask: () => this._beforeTask(), - afterTask: () => this._afterTask() - }); + this._innerZone = this._createInnerZone(this._outerZone, enableLongStackTrace); } - initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask} = {}) { + initCallbacks({onTurnStart, onTurnDone, onScheduleMicrotask, onErrorHandler} = {}) { this._onTurnStart = normalizeBlank(onTurnStart); this._onTurnDone = normalizeBlank(onTurnDone); + this._onErrorHandler = normalizeBlank(onErrorHandler); } run(fn) { @@ -35,6 +35,29 @@ export class VmTurnZone { return this._outerZone.run(fn); } + _createInnerZone(zone, enableLongStackTrace) { + var vmTurnZone = this; + var errorHandling; + + if (enableLongStackTrace) { + errorHandling = StringMapWrapper.merge(Zone.longStackTraceZone, { + onError: function (e) { + vmTurnZone._onError(this, e) + } + }); + } else { + errorHandling = { + onError: function (e) { + vmTurnZone._onError(this, e) + } + }; + } + + return zone.fork(errorHandling).fork({ + beforeTask: () => {this._beforeTask()}, + afterTask: () => {this._afterTask()} + }); + } _beforeTask(){ this._nestedRunCounter ++; @@ -49,4 +72,18 @@ export class VmTurnZone { this._onTurnDone(); } } + + _onError(zone, e) { + if (isPresent(this._onErrorHandler)) { + var trace = [normalizeBlank(e.stack)]; + + while (zone && zone.constructedAtException) { + trace.push(zone.constructedAtException.get()); + zone = zone.parent; + } + this._onErrorHandler(e, trace); + } else { + throw e; + } + } } \ No newline at end of file diff --git a/modules/core/test/application_spec.js b/modules/core/test/application_spec.js index 109cf46163..a689f0cc8c 100644 --- a/modules/core/test/application_spec.js +++ b/modules/core/test/application_spec.js @@ -54,7 +54,7 @@ export function main() { describe('bootstrap factory method', () => { it('should throw if no element is found', (done) => { - var injectorPromise = bootstrap(HelloRootCmp); + var injectorPromise = bootstrap(HelloRootCmp, [], (e,t) => {throw e;}); PromiseWrapper.then(injectorPromise, null, (reason) => { expect(reason.message).toContain( 'The app selector "hello-app" did not match any elements'); diff --git a/modules/core/test/packages b/modules/core/test/packages new file mode 120000 index 0000000000..a16c405015 --- /dev/null +++ b/modules/core/test/packages @@ -0,0 +1 @@ +../packages \ No newline at end of file diff --git a/modules/core/test/zone/vm_turn_zone_spec.js b/modules/core/test/zone/vm_turn_zone_spec.js index c5758dbd9b..4238cafc7c 100644 --- a/modules/core/test/zone/vm_turn_zone_spec.js +++ b/modules/core/test/zone/vm_turn_zone_spec.js @@ -10,7 +10,7 @@ export function main() { beforeEach(() => { log = new Log(); - zone = new VmTurnZone(); + zone = new VmTurnZone({enableLongStackTrace: true}); zone.initCallbacks({ onTurnStart: log.fn('onTurnStart'), onTurnDone: log.fn('onTurnDone') @@ -73,12 +73,95 @@ export function main() { }); describe("exceptions", () => { - it('should rethrow exceptions from the body', () => { + var trace, exception, saveStackTrace; + beforeEach(() => { + trace = null; + exception = null; + saveStackTrace = (e, t) => { + exception = e; + trace = t; + }; + }); + + it('should call the on error callback when it is defined', () => { + zone.initCallbacks({onErrorHandler: saveStackTrace}); + + zone.run(() => { + throw new BaseException('aaa'); + }); + + expect(exception).toBeDefined(); + }); + + it('should rethrow exceptions from the body when no callback defined', () => { expect(() => { zone.run(() => { - throw new BaseException('hello'); + throw new BaseException('bbb'); }); - }).toThrowError('hello'); + }).toThrowError('bbb'); + }); + + it('should produce long stack traces', (done) => { + zone.initCallbacks({onErrorHandler: saveStackTrace}); + + var c = PromiseWrapper.completer(); + + zone.run(function () { + PromiseWrapper.setTimeout(function () { + PromiseWrapper.setTimeout(function () { + c.complete(null); + throw new BaseException('ccc'); + }, 0); + }, 0); + }); + + c.promise.then((_) => { + // then number of traces for JS and Dart is different + expect(trace.length).toBeGreaterThan(1); + done(); + }); + }); + + it('should produce long stack traces (when using promises)', (done) => { + zone.initCallbacks({onErrorHandler: saveStackTrace}); + + var c = PromiseWrapper.completer(); + + zone.run(function () { + PromiseWrapper.resolve(null).then((_) => { + return PromiseWrapper.resolve(null).then((__) => { + c.complete(null); + throw new BaseException("ddd"); + }); + }); + }); + + c.promise.then((_) => { + // then number of traces for JS and Dart is different + expect(trace.length).toBeGreaterThan(1); + done(); + }); + }); + + it('should disable long stack traces', (done) => { + var zone = new VmTurnZone({enableLongStackTrace: false}); + zone.initCallbacks({onErrorHandler: saveStackTrace}); + + var c = PromiseWrapper.completer(); + + zone.run(function () { + PromiseWrapper.setTimeout(function () { + PromiseWrapper.setTimeout(function () { + c.complete(null); + throw new BaseException('ccc'); + }, 0); + }, 0); + }); + + c.promise.then((_) => { + expect(trace.length).toEqual(1); + done(); + }); }); }); }); diff --git a/modules/facade/src/async.dart b/modules/facade/src/async.dart index e96806df35..2e40450b02 100644 --- a/modules/facade/src/async.dart +++ b/modules/facade/src/async.dart @@ -12,11 +12,11 @@ class PromiseWrapper { return new Future.error(obj); } - static Future all(List promises){ + static Future all(List promises) { return Future.wait(promises); } - static Future then(Future promise, Function success, Function onError){ + static Future then(Future promise, Function success, Function onError) { if (success == null) return promise.catchError(onError); return promise.then(success, onError: onError); } @@ -24,13 +24,24 @@ class PromiseWrapper { static completer(){ return new _Completer(new Completer()); } + + static setTimeout(fn, millis) { + new Timer(new Duration(milliseconds: millis), fn); + } } class _Completer { Completer c; + _Completer(this.c); get promise => c.future; - get complete => c.complete; - get reject => c.completeError; + + complete(v) { + c.complete(v); + } + + reject(v) { + c.completeError(v); + } } diff --git a/modules/facade/src/async.es6 b/modules/facade/src/async.es6 index 89c778f58a..4d8e1df870 100644 --- a/modules/facade/src/async.es6 +++ b/modules/facade/src/async.es6 @@ -33,4 +33,8 @@ export class PromiseWrapper { reject: reject }; } + + static setTimeout(fn, millis) { + window.setTimeout(fn, millis); + } } \ No newline at end of file diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 00053eb6ee..d8a55d96f9 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -59,6 +59,24 @@ export class StringMapWrapper { } } } + + static merge(m1, m2) { + var m = {}; + + for (var attr in m1) { + if (m1.hasOwnProperty(attr)){ + m[attr] = m1[attr]; + } + } + + for (var attr in m2) { + if (m2.hasOwnProperty(attr)){ + m[attr] = m2[attr]; + } + } + + return m; + } } export class ListWrapper { diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index 76b796ef24..70606e88cd 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -214,4 +214,8 @@ export function assertionsEnabled() { } catch (e) { return true; } +} + +export function print(obj) { + console.log(obj); } \ No newline at end of file