blob: 93d892020bc081ca93a6a46bc249640982264878 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. 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 'dart:convert';
import 'package:dwds/data/devtools_request.dart';
import 'package:dwds/data/extension_request.dart';
import 'package:dwds/data/serializers.dart';
import 'package:dwds/src/debugging/remote_debugger.dart';
import 'package:dwds/src/services/chrome_proxy_service.dart';
import 'package:sse/server/sse_handler.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
/// A remote debugger backed by the Dart Debug Extension
/// with an SSE connection.
class ExtensionDebugger implements RemoteDebugger {
/// A connection between the debugger and the background of
/// Dart Debug Extension
final SseConnection sseConnection;
/// A map from id to a completer associated with an [ExtensionRequest]
final _completers = <int, Completer>{};
final _eventStreams = <String, Stream>{};
var _completerId = 0;
String instanceId;
final _devToolsRequestController = StreamController<DevToolsRequest>();
Stream<DevToolsRequest> get devToolsRequestStream =>
_devToolsRequestController.stream;
final _notificationController = StreamController<WipEvent>.broadcast();
Stream<WipEvent> get onNotification => _notificationController.stream;
final _closeController = StreamController<WipEvent>.broadcast();
@override
Stream<WipEvent> get onClose => _closeController.stream;
@override
Stream<ConsoleAPIEvent> get onConsoleAPICalled => eventStream(
'Runtime.consoleAPICalled', (WipEvent event) => ConsoleAPIEvent(event));
@override
Stream<ExceptionThrownEvent> get onExceptionThrown => eventStream(
'Runtime.exceptionThrown',
(WipEvent event) => ExceptionThrownEvent(event));
final _scripts = <String, WipScript>{};
ExtensionDebugger(this.sseConnection) {
sseConnection.stream.listen((data) {
var message = serializers.deserialize(jsonDecode(data));
if (message is ExtensionResponse) {
var encodedResult = {
'result': json.decode(message.result),
'id': message.id
};
if (_completers[message.id] == null) {
throw StateError('Missing completer.');
}
_completers[message.id].complete(WipResponse(encodedResult));
} else if (message is ExtensionEvent) {
var map = {
'method': json.decode(message.method),
'params': json.decode(message.params)
};
_notificationController.sink.add(WipEvent(map));
} else if (message is DevToolsRequest) {
instanceId = message.instanceId;
_devToolsRequestController.sink.add(message);
}
}, onError: (_) {
close();
}, onDone: close);
onScriptParsed.listen((event) {
_scripts[event.script.scriptId] = event.script;
});
// Listens for a page reload.
onGlobalObjectCleared.listen((_) {
_scripts.clear();
});
}
/// Sends a [command] with optional [params] to Dart Debug Extension
/// over the SSE connection.
@override
Future<WipResponse> sendCommand(String command,
{Map<String, dynamic> params}) {
var completer = Completer<WipResponse>();
var id = newId();
_completers[id] = completer;
sseConnection.sink
.add(jsonEncode(serializers.serialize(ExtensionRequest((b) => b
..id = id
..command = command
..commandParams = jsonEncode(params ?? {})))));
return completer.future;
}
int newId() => _completerId++;
@override
void close() {
_closeController.add(WipEvent({}));
sseConnection.sink.close();
_notificationController.close();
_devToolsRequestController.close();
_closeController.close();
}
@override
Future disable() => sendCommand('Debugger.disable');
@override
Future enable() => sendCommand('Debugger.enable');
@override
Future<String> getScriptSource(String scriptId) async =>
(await sendCommand('Debugger.getScriptSource',
params: {'scriptId': scriptId}))
.result['scriptSource'] as String;
@override
Future<WipResponse> pause() => sendCommand('Debugger.pause');
@override
Future<WipResponse> resume() => sendCommand('Debugger.resume');
@override
Future<WipResponse> setPauseOnExceptions(PauseState state) =>
sendCommand('Debugger.setPauseOnExceptions',
params: {'state': _pauseStateToString(state)});
@override
Future<WipResponse> stepInto() => sendCommand('Debugger.stepInto');
@override
Future<WipResponse> stepOut() => sendCommand('Debugger.stepOut');
@override
Future<WipResponse> stepOver() => sendCommand('Debugger.stepOver');
@override
Future<void> enablePage() => sendCommand('Page.enable');
@override
Future<RemoteObject> evaluate(String expression) async {
final response = await sendCommand('Runtime.evaluate', params: {
'expression': expression,
});
if (response.result.containsKey('exceptionDetails')) {
throw ChromeDebugException(
response.result['exceptionDetails'] as Map<String, dynamic>);
} else {
return RemoteObject(response.result['result'] as Map<String, dynamic>);
}
}
@override
Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
return _eventStreams
.putIfAbsent(
method,
() => onNotification
.where((event) => event.method == method)
.map(transformer))
.cast();
}
@override
Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared => eventStream(
'Page.frameStartedLoading',
(WipEvent event) => GlobalObjectClearedEvent(event));
@override
Stream<DebuggerPausedEvent> get onPaused => eventStream(
'Debugger.paused', (WipEvent event) => DebuggerPausedEvent(event));
@override
Stream<DebuggerResumedEvent> get onResumed => eventStream(
'Debugger.resumed', (WipEvent event) => DebuggerResumedEvent(event));
@override
Stream<ScriptParsedEvent> get onScriptParsed => eventStream(
'Debugger.scriptParsed', (WipEvent event) => ScriptParsedEvent(event));
@override
Map<String, WipScript> get scripts => 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 ArgumentError('unknown state: $state');
}
}
}