Bug 898485 - [app manager] Implement an abstract connection manager. r=past

This commit is contained in:
Paul Rouget 2013-08-21 08:56:40 +02:00
parent 1141bcf097
commit 1dd29166e6
5 changed files with 355 additions and 1 deletions

View File

@ -52,6 +52,7 @@ var BuiltinProvider = {
"devtools/server": "resource://gre/modules/devtools/server",
"devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
"devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
"devtools/client": "resource://gre/modules/devtools/client",
// Allow access to xpcshell test items from the loader.
"xpcshell-test": "resource://test"
@ -87,7 +88,7 @@ var SrcdirProvider = {
let serverURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "server"));
let webconsoleURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "webconsole"));
let cssLogicURI = this.fileURI(OS.Path.join(toolkitURI, "styleinspector", "css-logic"));
let clientURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "client"));
let mainURI = this.fileURI(OS.Path.join(srcdir, "browser", "devtools", "main.js"));
this.loader = new loader.Loader({
modules: {
@ -97,6 +98,7 @@ var SrcdirProvider = {
"": "resource://gre/modules/commonjs/",
"devtools/server": serverURI,
"devtools/toolkit/webconsole": webconsoleURI,
"devtools/client": clientURI,
"devtools": devtoolsURI,
"devtools/styleinspector/css-logic": cssLogicURI,
"main": mainURI

View File

@ -13,3 +13,4 @@ include $(topsrcdir)/config/rules.mk
libs::
$(INSTALL) $(IFLAGS1) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
$(INSTALL) $(IFLAGS1) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/client

View File

@ -0,0 +1,252 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cu} = require("chrome");
const {setTimeout, clearTimeout} = require('sdk/timers');
const EventEmitter = require("devtools/shared/event-emitter");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
/**
* Connection Manager.
*
* To use this module:
* const {ConnectionManager} = require("devtools/client/connection-manager");
*
* # ConnectionManager
*
* Methods:
* Connection createConnection(host, port)
* void destroyConnection(connection)
*
* Properties:
* Array connections
*
* # Connection
*
* A connection is a wrapper around a debugger client. It has a simple
* API to instantiate a connection to a debugger server. Once disconnected,
* no need to re-create a Connection object. Calling `connect()` again
* will re-create a debugger client.
*
* Methods:
* connect() Connect to host:port. Expect a "connecting" event. If
* host is not specified, a local pipe is used
* disconnect() Disconnect if connected. Expect a "disconnecting" event
*
* Properties:
* host IP address or hostname
* port Port
* logs Current logs. "newlog" event notifies new available logs
* store Reference to a local data store (see below)
* status Connection status:
* Connection.Status.CONNECTED
* Connection.Status.DISCONNECTED
* Connection.Status.CONNECTING
* Connection.Status.DISCONNECTING
* Connection.Status.DESTROYED
*
* Events (as in event-emitter.js):
* Connection.Events.CONNECTING Trying to connect to host:port
* Connection.Events.CONNECTED Connection is successful
* Connection.Events.DISCONNECTING Trying to disconnect from server
* Connection.Events.DISCONNECTED Disconnected (at client request, or because of a timeout or connection error)
* Connection.Events.STATUS_CHANGED The connection status (connection.status) has changed
* Connection.Events.TIMEOUT Connection timeout
* Connection.Events.HOST_CHANGED Host has changed
* Connection.Events.PORT_CHANGED Port has changed
* Connection.Events.NEW_LOG A new log line is available
*
*/
let ConnectionManager = {
_connections: new Set(),
createConnection: function(host, port) {
let c = new Connection(host, port);
c.once("destroy", (event) => this.destroyConnection(c));
this._connections.add(c);
this.emit("new", c);
return c;
},
destroyConnection: function(connection) {
if (this._connections.has(connection)) {
this._connections.delete(connection);
if (connection.status != Connection.Status.DESTROYED) {
connection.destroy();
}
}
},
get connections() {
return [c for (c of this._connections)];
},
}
EventEmitter.decorate(ConnectionManager);
let lastID = -1;
function Connection(host, port) {
EventEmitter.decorate(this);
this.uid = ++lastID;
this.host = host;
this.port = port;
this._setStatus(Connection.Status.DISCONNECTED);
this._onDisconnected = this._onDisconnected.bind(this);
this._onConnected = this._onConnected.bind(this);
this._onTimeout = this._onTimeout.bind(this);
}
Connection.Status = {
CONNECTED: "connected",
DISCONNECTED: "disconnected",
CONNECTING: "connecting",
DISCONNECTING: "disconnecting",
DESTROYED: "destroyed",
}
Connection.Events = {
CONNECTED: Connection.Status.CONNECTED,
DISCONNECTED: Connection.Status.DISCONNECTED,
CONNECTING: Connection.Status.CONNECTING,
DISCONNECTING: Connection.Status.DISCONNECTING,
DESTROYED: Connection.Status.DESTROYED,
TIMEOUT: "timeout",
STATUS_CHANGED: "status-changed",
HOST_CHANGED: "host-changed",
PORT_CHANGED: "port-changed",
NEW_LOG: "new_log"
}
Connection.prototype = {
logs: "",
log: function(str) {
this.logs += "\n" + str;
this.emit(Connection.Events.NEW_LOG, str);
},
get client() {
return this._client
},
get host() {
return this._host
},
set host(value) {
if (this._host && this._host == value)
return;
this._host = value;
this.emit(Connection.Events.HOST_CHANGED);
},
get port() {
return this._port
},
set port(value) {
if (this._port && this._port == value)
return;
this._port = value;
this.emit(Connection.Events.PORT_CHANGED);
},
disconnect: function(force) {
if (this.status == Connection.Status.DESTROYED) {
return;
}
clearTimeout(this._timeoutID);
if (this.status == Connection.Status.CONNECTED ||
this.status == Connection.Status.CONNECTING) {
this.log("disconnecting");
this._setStatus(Connection.Status.DISCONNECTING);
this._client.close();
}
},
connect: function() {
if (this.status == Connection.Status.DESTROYED) {
return;
}
if (!this._client) {
this.log("connecting to " + this.host + ":" + this.port);
this._setStatus(Connection.Status.CONNECTING);
let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
this._timeoutID = setTimeout(this._onTimeout, delay);
let transport;
if (!this._host) {
transport = DebuggerServer.connectPipe();
} else {
transport = debuggerSocketConnect(this._host, this._port);
}
this._client = new DebuggerClient(transport);
this._client.addOneTimeListener("closed", this._onDisconnected);
this._client.connect(this._onConnected);
} else {
let msg = "Can't connect. Client is not fully disconnected";
this.log(msg);
throw new Error(msg);
}
},
destroy: function() {
this.log("killing connection");
clearTimeout(this._timeoutID);
if (this._client) {
this._client.close();
this._client = null;
}
this._setStatus(Connection.Status.DESTROYED);
},
get status() {
return this._status
},
_setStatus: function(value) {
if (this._status && this._status == value)
return;
this._status = value;
this.emit(value);
this.emit(Connection.Events.STATUS_CHANGED, value);
},
_onDisconnected: function() {
clearTimeout(this._timeoutID);
switch (this.status) {
case Connection.Status.CONNECTED:
this.log("disconnected (unexpected)");
break;
case Connection.Status.CONNECTING:
this.log("Connection error");
break;
default:
this.log("disconnected");
}
this._client = null;
this._setStatus(Connection.Status.DISCONNECTED);
},
_onConnected: function() {
this.log("connected");
clearTimeout(this._timeoutID);
this._setStatus(Connection.Status.CONNECTED);
},
_onTimeout: function() {
this.log("connection timeout");
this.emit(Connection.Events.TIMEOUT, str);
this.disconnect();
},
}
exports.ConnectionManager = ConnectionManager;
exports.Connection = Connection;

View File

@ -36,6 +36,7 @@ MOCHITEST_CHROME_FILES = \
test_styles-modify.html \
test_unsafeDereference.html \
nonchrome_unsafeDereference.html \
test_connection-manager.html \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,98 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 898485 - [app manager] Implement an abstract connection manager
-->
<head>
<meta charset="utf-8">
<title>Mozilla Bug</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script>
window.onload = function() {
SimpleTest.waitForExplicitFinish();
var Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
var {ConnectionManager, Connection} = devtools.require("devtools/client/connection-manager");
var orgCount = ConnectionManager.connections.length;
ConnectionManager.once("new", (event, c) => {
is(ConnectionManager.connections[orgCount], c, "new event fired, with correct connection");
});
var c1 = ConnectionManager.createConnection();
var c2 = ConnectionManager.createConnection();
is(ConnectionManager.connections[orgCount], c1, "Connection 1 registered");
is(ConnectionManager.connections[orgCount + 1], c2, "Connection 2 registered");
c1.once(Connection.Events.DESTROYED, function() {
is(ConnectionManager.connections.length, orgCount + 1, "Connection 1 destroyed");
var c = c2;
var eventsRef = "connecting connected disconnecting disconnected host-changed disconnected destroyed";
var events = [];
var s = Connection.Status;
is(c.status, s.DISCONNECTED, "disconnected");
c.once(Connection.Events.CONNECTING, function(e) { events.push(e); is(c.status, s.CONNECTING, "connecting"); });
c.once(Connection.Events.CONNECTED, function(e) { events.push(e); is(c.status, s.CONNECTED, "connected"); c.disconnect()});
c.once(Connection.Events.DISCONNECTING, function(e) { events.push(e); is(c.status, s.DISCONNECTING, "disconnecting"); });
c.once(Connection.Events.DISCONNECTED, function(e) { events.push(e); is(c.status, s.DISCONNECTED, "disconnected"); testError()});
c.once(Connection.Events.DESTROYED, function(e) { events.push(e); is(c.status, s.DESTROYED, "destroyed"); finish()});
c.connect();
function testStore() {
c.store.on("set", function(e,path) {
if (path.join(".") == "device.width") {
is(c.store.object.device.width, window.screen.width, "Store is fed with valid data");
c.disconnect();
}
});
}
function testError() {
c.once(Connection.Events.DISCONNECTED, function(e) {
events.push(e);
ConnectionManager.destroyConnection(c);
});
c.once(Connection.Events.HOST_CHANGED, function(e) {
events.push(e);
c.connect();
});
c.port = 1;
c.host = "localhost";
}
function finish() {
is(events.join(" "), eventsRef, "Events received in the right order");
DebuggerServer.destroy();
SimpleTest.finish();
}
});
ConnectionManager.destroyConnection(c1);
}
</script>
</pre>
</body>
</html>