blob: 099664f5ab4255e059eb0cac2b109e39f9d880f1 [file] [log] [blame]
// 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.
/**
* A library to connect to a Webkit Inspection Protocol server (like Chrome).
*/
library wip;
import 'dart:async';
import 'dart:convert';
import 'dart:io' show HttpClient, HttpClientResponse, WebSocket;
import 'package:logging/logging.dart' show Logger;
import 'src/console.dart';
import 'src/debugger.dart';
import 'src/dom.dart';
import 'src/log.dart';
import 'src/page.dart';
import 'src/runtime.dart';
export 'src/console.dart';
export 'src/debugger.dart';
export 'src/dom.dart';
export 'src/log.dart';
export 'src/page.dart';
export 'src/runtime.dart';
/**
* A class to connect to a Chrome instance and reflect on its available tabs.
*
* This assumes the browser has been started with the `--remote-debugging-port`
* flag. The data is read from the `http://{host}:{port}/json` url.
*/
class ChromeConnection {
final HttpClient _client = new HttpClient();
final Uri url;
ChromeConnection(String host, [int port = 9222])
: url = Uri.parse('http://${host}:${port}/');
// TODO(DrMarcII): consider changing this to return Stream<ChromeTab>.
Future<List<ChromeTab>> getTabs() async {
var response = await getUrl('/json');
var respBody = await utf8.decodeStream(response);
List<Map<String, String>> data = jsonDecode(respBody);
return data.map((m) => new ChromeTab(m)).toList();
}
Future<ChromeTab> getTab(bool accept(ChromeTab tab),
{Duration retryFor}) async {
var start = new DateTime.now();
var end = start;
if (retryFor != null) {
end = start.add(retryFor);
}
while (true) {
try {
for (var tab in await getTabs()) {
if (accept(tab)) {
return tab;
}
}
if (end.isBefore(new DateTime.now())) {
return null;
}
} catch (e) {
if (end.isBefore(new DateTime.now())) {
rethrow;
}
}
await new Future.delayed(new Duration(milliseconds: 25));
}
}
Future<HttpClientResponse> getUrl(String path) async {
var request = await _client.getUrl(url.resolve(path));
return await request.close();
}
void close() => _client.close(force: true);
}
class ChromeTab {
final Map _map;
ChromeTab(this._map);
String get description => _map['description'];
String get devtoolsFrontendUrl => _map['devtoolsFrontendUrl'];
String get faviconUrl => _map['faviconUrl'];
/// Ex. `E1999E8A-EE27-0450-9900-5BFF4C69CA83`.
String get id => _map['id'];
String get title => _map['title'];
/// Ex. `background_page`, `page`.
String get type => _map['type'];
String get url => _map['url'];
/// Ex. `ws://localhost:1234/devtools/page/4F98236D-4EB0-7C6C-5DD1-AF9B6BE4BC71`.
String get webSocketDebuggerUrl => _map['webSocketDebuggerUrl'];
bool get hasIcon => _map.containsKey('faviconUrl');
bool get isChromeExtension => url.startsWith('chrome-extension://');
bool get isBackgroundPage => type == 'background_page';
Future<WipConnection> connect() =>
WipConnection.connect(webSocketDebuggerUrl);
String toString() => url;
}
/**
* A Webkit Inspection Protocol (WIP) connection.
*/
class WipConnection {
static final _logger = new Logger('WipConnection');
/**
* The WebSocket URL.
*/
final String url;
final WebSocket _ws;
int _nextId = 0;
WipConsole _console; // ignore: deprecated_member_use
@Deprecated('This domain is deprecated - use Runtime or Log instead')
WipConsole get console => _console;
WipDebugger _debugger;
WipDebugger get debugger => _debugger;
WipDom _dom;
WipDom get dom => _dom;
WipPage _page;
WipPage get page => _page;
WipLog _log;
WipLog get log => _log;
WipRuntime _runtime;
WipRuntime get runtime => _runtime;
final Map _completers = <int, Completer<WipResponse>>{};
final StreamController _closeController =
new StreamController<WipConnection>.broadcast();
final StreamController _notificationController =
new StreamController<WipEvent>.broadcast();
static Future<WipConnection> connect(String url) {
return WebSocket.connect(url).then((socket) {
return new WipConnection._(url, socket);
});
}
WipConnection._(this.url, this._ws) {
_console = new WipConsole(this); // ignore: deprecated_member_use
_debugger = new WipDebugger(this);
_dom = new WipDom(this);
_page = new WipPage(this);
_log = new WipLog(this);
_runtime = new WipRuntime(this);
_ws.listen((data) {
var json = jsonDecode(data);
if (json.containsKey('id')) {
_handleResponse(json);
} else {
_handleNotification(json);
}
}, onDone: _handleClose);
}
Stream<WipConnection> get onClose => _closeController.stream;
Stream<WipEvent> get onNotification => _notificationController.stream;
Future close() => _ws.close();
String toString() => url;
Future<WipResponse> sendCommand(String method,
[Map<String, dynamic> params]) {
_logger.finest('Sending command: $method($params)');
var completer = new Completer<WipResponse>();
var json = {'id': _nextId++, 'method': method};
if (params != null) {
json['params'] = params;
}
_completers[json['id']] = completer;
_ws.add(jsonEncode(json));
return completer.future;
}
void _handleNotification(Map<String, dynamic> json) {
_logger.finest('Received notification: $json');
_notificationController.add(new WipEvent(json));
}
void _handleResponse(Map<String, dynamic> event) {
var completer = _completers.remove(event['id']);
if (event.containsKey('error')) {
_logger.info('Received error: $event');
completer.completeError(new WipError(event));
} else {
_logger.finest('Received response: $event');
completer.complete(new WipResponse(event));
}
}
void _handleClose() {
_closeController.add(this);
_closeController.close();
_notificationController.close();
}
}
class WipEvent {
final String method;
final Map<String, dynamic> params;
WipEvent(Map<String, dynamic> map)
: method = map['method'],
params = map['params'];
String toString() => 'WipEvent: $method($params)';
}
class WipError {
final int id;
final dynamic error;
WipError(Map<String, dynamic> json)
: id = json['id'],
error = json['error'];
String toString() => 'WipError $id: $error';
}
class WipResponse {
final int id;
final Map<String, dynamic> result;
WipResponse(Map<String, dynamic> json)
: id = json['id'],
result = json['result'];
String toString() => 'WipResponse $id: $result';
}
typedef WipEvent WipEventTransformer(WipEvent event);
/// @optional
const String optional = 'optional';
abstract class WipDomain {
Map<String, Stream> _eventStreams = {};
final WipConnection connection;
var _onClosed;
Stream<WipDomain> get onClosed => _onClosed;
WipDomain(WipConnection connection) : this.connection = connection {
this._onClosed =
new StreamTransformer.fromHandlers(handleData: (event, EventSink sink) {
sink.add(this);
}).bind(connection.onClose);
}
Stream<WipEvent> eventStream(String method, WipEventTransformer transformer) {
return _eventStreams.putIfAbsent(
method,
() => new StreamTransformer.fromHandlers(
handleData: (WipEvent event, EventSink<WipEvent> sink) {
if (event.method == method) {
sink.add(transformer(event));
}
},
).bind(connection.onNotification),
);
}
Future<WipResponse> sendCommand(
String method, {
Map<String, dynamic> params,
}) {
return connection.sendCommand(method, params);
}
}
class WrappedWipEvent implements WipEvent {
final WipEvent _wrapped;
WrappedWipEvent(this._wrapped);
@override
String get method => _wrapped.method;
@override
Map<String, dynamic> get params => _wrapped.params;
}