Wrap a zone exception handler around our Chrome event handlers (#732)
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index 4977c94..f88295f 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart
@@ -5,7 +5,8 @@ import 'dart:async'; import 'package:logging/logging.dart'; -import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; +import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' + hide StackTrace; import '../../asset_handler.dart'; import '../../dwds.dart' show LogWriter; @@ -20,8 +21,6 @@ import 'remote_debugger.dart'; import 'sources.dart'; -final _logger = Logger('Debugger'); - /// Converts from ExceptionPauseMode strings to [PauseState] enums. /// /// Values defined in: @@ -33,6 +32,8 @@ }; class Debugger extends Domain { + static final logger = Logger('Debugger'); + final AssetHandler _assetHandler; final LogWriter _logWriter; final RemoteDebugger _remoteDebugger; @@ -173,9 +174,13 @@ // We must add a listener before enabling the debugger otherwise we will // miss events. // Allow a null debugger/connection for unit tests. - _remoteDebugger?.onScriptParsed?.listen(sources.scriptParsed); - _remoteDebugger?.onPaused?.listen(_pauseHandler); - _remoteDebugger?.onResumed?.listen(_resumeHandler); + runZoned(() { + _remoteDebugger?.onScriptParsed?.listen(sources.scriptParsed); + _remoteDebugger?.onPaused?.listen(_pauseHandler); + _remoteDebugger?.onResumed?.listen(_resumeHandler); + }, onError: (e, StackTrace s) { + logger.warning('Error handling Chrome event', e, s); + }); handleErrorIfPresent(await _remoteDebugger?.sendCommand('Page.enable')); handleErrorIfPresent(await _remoteDebugger?.enable() as WipResponse); @@ -445,7 +450,7 @@ _streamNotify('Debug', event); } on ChromeDebugException catch (e, s) { if (e.exception.description.contains('require is not defined')) { - _logger.warning( + logger.warning( 'Error handling pause event, app does not appear to be loaded', e, s);
diff --git a/dwds/test/debugger_test.dart b/dwds/test/debugger_test.dart index 71843bf..b0ac91e 100644 --- a/dwds/test/debugger_test.dart +++ b/dwds/test/debugger_test.dart
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. @TestOn('vm') +import 'dart:async'; import 'package:dwds/dwds.dart' show ModuleStrategy; import 'package:dwds/src/debugging/debugger.dart'; import 'package:dwds/src/debugging/inspector.dart'; @@ -11,6 +12,8 @@ import 'package:dwds/src/utilities/shared.dart'; import 'package:source_maps/parser.dart'; import 'package:test/test.dart'; +import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' + show DebuggerPausedEvent; import 'fixtures/context.dart'; import 'fixtures/debugger_data.dart'; @@ -20,11 +23,14 @@ AppInspector inspector; Debugger debugger; FakeWebkitDebugger webkitDebugger; +StreamController<DebuggerPausedEvent> pausedController; void main() async { setUpAll(() async { globalModuleStrategy = ModuleStrategy.requireJS; webkitDebugger = FakeWebkitDebugger(); + pausedController = StreamController<DebuggerPausedEvent>(); + webkitDebugger.onPaused = pausedController.stream; debugger = await Debugger.create( null, webkitDebugger, @@ -65,4 +71,21 @@ var frame1Variables = firstFrame.vars.map((each) => each.name).toList(); expect(frame1Variables, ['a', 'b']); }); + + group('errors', () { + setUp(() { + // We need to provide an Isolate so that the code doesn't bail out on a null + // check before it has a chance to throw. + inspector = FakeInspector(fakeIsolate: simpleIsolate); + }); + + test('errors in the zone are caught and logged', () async { + // Add a DebuggerPausedEvent with a null parameter to provoke an error. + pausedController.sink.add(DebuggerPausedEvent(null)); + expect( + Debugger.logger.onRecord, + emitsThrough(predicate( + (log) => log.message == 'Error handling Chrome event'))); + }); + }); }
diff --git a/dwds/test/fixtures/fakes.dart b/dwds/test/fixtures/fakes.dart index 2b5b915..3dfffc7 100644 --- a/dwds/test/fixtures/fakes.dart +++ b/dwds/test/fixtures/fakes.dart
@@ -20,8 +20,26 @@ import 'debugger_data.dart'; +/// Constructs a trivial Isolate we can use when we need to provide one but +/// don't want go through initialization. +Isolate get simpleIsolate => Isolate( + id: '1', + number: '1', + name: 'fake', + libraries: [], + exceptionPauseMode: 'abc', + breakpoints: [], + pauseOnExit: false, + pauseEvent: null, + startTime: 0, + livePorts: 0, + runnable: false, + ); + class FakeInspector extends Domain implements AppInspector { - FakeInspector() : super.forInspector(); + FakeInspector({this.fakeIsolate}) : super.forInspector(); + + Isolate fakeIsolate; @override Object noSuchMethod(Invocation invocation) { @@ -45,9 +63,9 @@ @override Future<ScriptRef> scriptWithId(String scriptId) => null; @override - Isolate checkIsolate(String isolateId) => null; + Isolate checkIsolate(String isolateId) => fakeIsolate; @override - Isolate get isolate => null; + Isolate get isolate => fakeIsolate; @override IsolateRef get isolateRef => null; @override @@ -119,7 +137,7 @@ Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared => null; @override - Stream<DebuggerPausedEvent> get onPaused => null; + Stream<DebuggerPausedEvent> onPaused; @override Stream<DebuggerResumedEvent> get onResumed => null;