| // Copyright 2015 Google. All rights reserved. Use of this source code is |
| // governed by a BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:collection'; |
| |
| import '../webkit_inspection_protocol.dart'; |
| |
| class WipDebugger extends WipDomain { |
| final _scripts = <String, WipScript>{}; |
| |
| WipDebugger(WipConnection connection) : super(connection) { |
| onScriptParsed.listen((event) { |
| _scripts[event.script.scriptId] = event.script; |
| }); |
| onGlobalObjectCleared.listen((_) { |
| _scripts.clear(); |
| }); |
| } |
| |
| Future<WipResponse> enable() => sendCommand('Debugger.enable'); |
| |
| Future<WipResponse> disable() => sendCommand('Debugger.disable'); |
| |
| Future<String> getScriptSource(String scriptId) async { |
| return (await sendCommand('Debugger.getScriptSource', |
| params: {'scriptId': scriptId})) |
| .result['scriptSource'] as String; |
| } |
| |
| Future<WipResponse> pause() => sendCommand('Debugger.pause'); |
| |
| Future<WipResponse> resume() => sendCommand('Debugger.resume'); |
| |
| Future<WipResponse> stepInto({Map<String, dynamic> params}) => |
| sendCommand('Debugger.stepInto', params: params); |
| |
| Future<WipResponse> stepOut() => sendCommand('Debugger.stepOut'); |
| |
| Future<WipResponse> stepOver({Map<String, dynamic> params}) => |
| sendCommand('Debugger.stepOver', params: params); |
| |
| Future<WipResponse> setPauseOnExceptions(PauseState state) { |
| return sendCommand('Debugger.setPauseOnExceptions', |
| params: {'state': _pauseStateToString(state)}); |
| } |
| |
| /// Sets JavaScript breakpoint at a given location. |
| /// |
| /// - `location`: Location to set breakpoint in |
| /// - `condition`: Expression to use as a breakpoint condition. When |
| /// specified, debugger will only stop on the breakpoint if this expression |
| /// evaluates to true. |
| Future<SetBreakpointResponse> setBreakpoint( |
| WipLocation location, { |
| String condition, |
| }) async { |
| Map<String, dynamic> params = { |
| 'location': location.toJsonMap(), |
| }; |
| if (condition != null) { |
| params['condition'] = condition; |
| } |
| |
| final WipResponse response = |
| await sendCommand('Debugger.setBreakpoint', params: params); |
| |
| if (response.result.containsKey('exceptionDetails')) { |
| throw new ExceptionDetails( |
| response.result['exceptionDetails'] as Map<String, dynamic>); |
| } else { |
| return new SetBreakpointResponse(response.json); |
| } |
| } |
| |
| /// Removes JavaScript breakpoint. |
| Future<WipResponse> removeBreakpoint(String breakpointId) { |
| return sendCommand('Debugger.removeBreakpoint', |
| params: {'breakpointId': breakpointId}); |
| } |
| |
| /// Evaluates expression on a given call frame. |
| /// |
| /// - `callFrameId`: Call frame identifier to evaluate on |
| /// - `expression`: Expression to evaluate |
| /// - `returnByValue`: Whether the result is expected to be a JSON object that |
| /// should be sent by value |
| Future<RemoteObject> evaluateOnCallFrame( |
| String callFrameId, |
| String expression, { |
| bool returnByValue, |
| }) async { |
| Map<String, dynamic> params = { |
| 'callFrameId': callFrameId, |
| 'expression': expression, |
| }; |
| if (returnByValue != null) { |
| params['returnByValue'] = returnByValue; |
| } |
| |
| final WipResponse response = |
| await sendCommand('Debugger.evaluateOnCallFrame', params: params); |
| |
| if (response.result.containsKey('exceptionDetails')) { |
| throw new ExceptionDetails( |
| response.result['exceptionDetails'] as Map<String, dynamic>); |
| } else { |
| return new RemoteObject( |
| response.result['result'] as Map<String, dynamic>); |
| } |
| } |
| |
| /// Returns possible locations for breakpoint. scriptId in start and end range |
| /// locations should be the same. |
| /// |
| /// - `start`: Start of range to search possible breakpoint locations in |
| /// - `end`: End of range to search possible breakpoint locations in |
| /// (excluding). When not specified, end of scripts is used as end of range. |
| /// - `restrictToFunction`: Only consider locations which are in the same |
| /// (non-nested) function as start. |
| Future<List<WipBreakLocation>> getPossibleBreakpoints( |
| WipLocation start, { |
| WipLocation end, |
| bool restrictToFunction, |
| }) async { |
| Map<String, dynamic> params = { |
| 'start': start.toJsonMap(), |
| }; |
| if (end != null) { |
| params['end'] = end.toJsonMap(); |
| } |
| if (restrictToFunction != null) { |
| params['restrictToFunction'] = restrictToFunction; |
| } |
| |
| final WipResponse response = |
| await sendCommand('Debugger.getPossibleBreakpoints', params: params); |
| |
| if (response.result.containsKey('exceptionDetails')) { |
| throw new ExceptionDetails( |
| response.result['exceptionDetails'] as Map<String, dynamic>); |
| } else { |
| List locations = response.result['locations']; |
| return List.from(locations.map((map) => WipBreakLocation(map))); |
| } |
| } |
| |
| /// Enables or disables async call stacks tracking. |
| /// |
| /// maxDepth - Maximum depth of async call stacks. Setting to 0 will |
| /// effectively disable collecting async call stacks (default). |
| Future<WipResponse> setAsyncCallStackDepth(int maxDepth) { |
| return sendCommand('Debugger.setAsyncCallStackDepth', params: { |
| 'maxDepth': maxDepth, |
| }); |
| } |
| |
| Stream<DebuggerPausedEvent> get onPaused => eventStream('Debugger.paused', |
| (WipEvent event) => new DebuggerPausedEvent(event.json)); |
| |
| Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared => eventStream( |
| 'Debugger.globalObjectCleared', |
| (WipEvent event) => new GlobalObjectClearedEvent(event.json)); |
| |
| Stream<DebuggerResumedEvent> get onResumed => eventStream('Debugger.resumed', |
| (WipEvent event) => new DebuggerResumedEvent(event.json)); |
| |
| Stream<ScriptParsedEvent> get onScriptParsed => eventStream( |
| 'Debugger.scriptParsed', |
| (WipEvent event) => new ScriptParsedEvent(event.json)); |
| |
| Map<String, WipScript> get scripts => new UnmodifiableMapView(_scripts); |
| } |
| |
| String _pauseStateToString(PauseState state) { |
| switch (state) { |
| case PauseState.all: |
| return 'all'; |
| case PauseState.none: |
| return 'none'; |
| case PauseState.uncaught: |
| return 'uncaught'; |
| default: |
| throw new ArgumentError('unknown state: $state'); |
| } |
| } |
| |
| enum PauseState { all, none, uncaught } |
| |
| class ScriptParsedEvent extends WipEvent { |
| ScriptParsedEvent(Map<String, dynamic> json) : super(json); |
| |
| WipScript get script => new WipScript(params); |
| |
| String toString() => script.toString(); |
| } |
| |
| class GlobalObjectClearedEvent extends WipEvent { |
| GlobalObjectClearedEvent(Map<String, dynamic> json) : super(json); |
| } |
| |
| class DebuggerResumedEvent extends WipEvent { |
| DebuggerResumedEvent(Map<String, dynamic> json) : super(json); |
| } |
| |
| /// Fired when the virtual machine stopped on breakpoint or exception or any |
| /// other stop criteria. |
| class DebuggerPausedEvent extends WipEvent { |
| DebuggerPausedEvent(Map<String, dynamic> json) : super(json); |
| |
| /// Call stack the virtual machine stopped on. |
| List<WipCallFrame> getCallFrames() => (params['callFrames'] as List) |
| .map((frame) => new WipCallFrame(frame as Map<String, dynamic>)) |
| .toList(); |
| |
| /// Pause reason. |
| /// |
| /// Allowed Values: ambiguous, assert, debugCommand, DOM, EventListener, |
| /// exception, instrumentation, OOM, other, promiseRejection, XHR. |
| String get reason => params['reason'] as String; |
| |
| /// Object containing break-specific auxiliary properties. |
| Object get data => params['data']; |
| |
| /// Hit breakpoints IDs (optional). |
| List<String> get hitBreakpoints { |
| if (params['hitBreakpoints'] == null) return null; |
| return (params['hitBreakpoints'] as List).cast<String>(); |
| } |
| |
| /// Async stack trace, if any. |
| StackTrace get asyncStackTrace => params['asyncStackTrace'] == null |
| ? null |
| : StackTrace(params['asyncStackTrace']); |
| |
| String toString() => 'paused: ${reason}'; |
| } |
| |
| /// A debugger call frame. |
| /// |
| /// This class is for the 'debugger' domain. |
| class WipCallFrame { |
| final Map<String, dynamic> json; |
| |
| WipCallFrame(this.json); |
| |
| /// Call frame identifier. |
| /// |
| /// This identifier is only valid while the virtual machine is paused. |
| String get callFrameId => json['callFrameId'] as String; |
| |
| /// Name of the JavaScript function called on this call frame. |
| String get functionName => json['functionName'] as String; |
| |
| /// Location in the source code. |
| WipLocation get location => |
| new WipLocation(json['location'] as Map<String, dynamic>); |
| |
| /// JavaScript script name or url. |
| String get url => json['url'] as String; |
| |
| /// Scope chain for this call frame. |
| Iterable<WipScope> getScopeChain() => (json['scopeChain'] as List) |
| .map((scope) => new WipScope(scope as Map<String, dynamic>)); |
| |
| /// `this` object for this call frame. |
| RemoteObject get thisObject => |
| new RemoteObject(json['this'] as Map<String, dynamic>); |
| |
| /// The value being returned, if the function is at return point. |
| /// |
| /// (optional) |
| RemoteObject get returnValue { |
| return json.containsKey('returnValue') |
| ? new RemoteObject(json['returnValue'] as Map<String, dynamic>) |
| : null; |
| } |
| |
| String toString() => '[${functionName}]'; |
| } |
| |
| class WipLocation { |
| final Map<String, dynamic> json; |
| |
| WipLocation(this.json); |
| |
| WipLocation.fromValues(String scriptId, int lineNumber, {int columnNumber}) |
| : json = {} { |
| json['scriptId'] = scriptId; |
| json['lineNumber'] = lineNumber; |
| if (columnNumber != null) { |
| json['columnNumber'] = columnNumber; |
| } |
| } |
| |
| String get scriptId => json['scriptId']; |
| |
| int get lineNumber => json['lineNumber']; |
| |
| int get columnNumber => json['columnNumber']; |
| |
| Map<String, dynamic> toJsonMap() { |
| return json; |
| } |
| |
| String toString() => '[${scriptId}:${lineNumber}:${columnNumber}]'; |
| } |
| |
| class WipScript { |
| final Map<String, dynamic> json; |
| |
| WipScript(this.json); |
| |
| String get scriptId => json['scriptId'] as String; |
| |
| String get url => json['url'] as String; |
| |
| int get startLine => json['startLine'] as int; |
| |
| int get startColumn => json['startColumn'] as int; |
| |
| int get endLine => json['endLine'] as int; |
| |
| int get endColumn => json['endColumn'] as int; |
| |
| bool get isContentScript => json['isContentScript'] as bool; |
| |
| String get sourceMapURL => json['sourceMapURL'] as String; |
| |
| String toString() => '[script ${scriptId}: ${url}]'; |
| } |
| |
| class WipScope { |
| final Map<String, dynamic> json; |
| |
| WipScope(this.json); |
| |
| // "catch", "closure", "global", "local", "with" |
| String get scope => json['type'] as String; |
| |
| /// Name of the scope, null if unnamed closure or global scope |
| String get name => json['name'] as String; |
| |
| /// Object representing the scope. For global and with scopes it represents |
| /// the actual object; for the rest of the scopes, it is artificial transient |
| /// object enumerating scope variables as its properties. |
| RemoteObject get object => |
| new RemoteObject(json['object'] as Map<String, dynamic>); |
| } |
| |
| class WipBreakLocation extends WipLocation { |
| WipBreakLocation(Map<String, dynamic> json) : super(json); |
| |
| WipBreakLocation.fromValues(String scriptId, int lineNumber, |
| {int columnNumber, String type}) |
| : super.fromValues(scriptId, lineNumber, columnNumber: columnNumber) { |
| if (type != null) { |
| json['type'] = type; |
| } |
| } |
| |
| /// Allowed Values: `debuggerStatement`, `call`, `return`. |
| String get type => json['type']; |
| } |
| |
| /// The response from [WipDebugger.setBreakpoint]. |
| class SetBreakpointResponse extends WipResponse { |
| SetBreakpointResponse(Map<String, dynamic> json) : super(json); |
| |
| String get breakpointId => result['breakpointId']; |
| |
| WipLocation get actualLocation => WipLocation(result['actualLocation']); |
| } |