--- a/b2g/chrome/content/desktop.js
+++ b/b2g/chrome/content/desktop.js
@@ -51,18 +51,18 @@ function setupButtons() {
GlobalSimulatorScreen.flipScreen();
rotateButton.classList.remove('active');
});
}
function checkDebuggerPort() {
// XXX: To be removed once bug 942756 lands.
// We are hacking 'unix-domain-socket' pref by setting a tcp port (number).
- // DebuggerServer.openListener detects that it isn't a file path (string),
- // and starts listening on the tcp port given here as command line argument.
+ // SocketListener.open detects that it isn't a file path (string), and starts
+ // listening on the tcp port given here as command line argument.
// Get the command line arguments that were passed to the b2g client
let args;
try {
let service = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"].getService(Ci.nsISupports);
args = service.wrappedJSObject.cmdLine;
} catch(e) {}
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -12,20 +12,16 @@ XPCOMUtils.defineLazyGetter(this, "Debug
});
XPCOMUtils.defineLazyGetter(this, "devtools", function() {
const { devtools } =
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
return devtools;
});
-XPCOMUtils.defineLazyGetter(this, "discovery", function() {
- return devtools.require("devtools/toolkit/discovery/discovery");
-});
-
XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() {
const { B2GTabList } =
devtools.require("resource://gre/modules/DebuggerActors.js");
return B2GTabList;
});
let RemoteDebugger = {
_promptDone: false,
@@ -140,18 +136,20 @@ let USBRemoteDebugger = {
RemoteDebugger.initServer();
let portOrPath =
Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
"/data/local/debugger-socket";
try {
debug("Starting USB debugger on " + portOrPath);
- this._listener = DebuggerServer.openListener(portOrPath);
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = portOrPath;
this._listener.allowConnection = RemoteDebugger.prompt;
+ this._listener.open();
// Temporary event, until bug 942756 lands and offers a way to know
// when the server is up and running.
Services.obs.notifyObservers(null, "debugger-server-started", null);
} catch (e) {
debug("Unable to start USB debugger server: " + e);
}
},
@@ -176,33 +174,35 @@ let WiFiRemoteDebugger = {
if (this._listener) {
return;
}
RemoteDebugger.initServer();
try {
debug("Starting WiFi debugger");
- this._listener = DebuggerServer.openListener(-1);
+ this._listener = DebuggerServer.createListener();
+ this._listener.portOrPath = -1 /* any available port */;
this._listener.allowConnection = RemoteDebugger.prompt;
+ this._listener.discoverable = true;
+ this._listener.encryption = true;
+ this._listener.open();
let port = this._listener.port;
debug("Started WiFi debugger on " + port);
- discovery.addService("devtools", { port: port });
} catch (e) {
debug("Unable to start WiFi debugger server: " + e);
}
},
stop: function() {
if (!this._listener) {
return;
}
try {
- discovery.removeService("devtools");
this._listener.close();
this._listener = null;
} catch (e) {
debug("Unable to stop WiFi debugger server: " + e);
}
}
};
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -547,16 +547,17 @@ SocialShare = {
for (let provider of providers) {
let button = document.createElement("toolbarbutton");
button.setAttribute("class", "toolbarbutton share-provider-button");
button.setAttribute("type", "radio");
button.setAttribute("group", "share-providers");
button.setAttribute("image", provider.iconURL);
button.setAttribute("tooltip", "share-button-tooltip");
button.setAttribute("origin", provider.origin);
+ button.setAttribute("label", provider.name);
button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
if (provider == selectedProvider) {
this.defaultButton = button;
}
hbox.insertBefore(button, addButton);
}
if (!this.defaultButton) {
this.defaultButton = addButton;
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -48,16 +48,24 @@
}
this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
if (event.target != this.contentDocument)
return;
this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
this.isActive = !this.minimized;
this._deferredChatLoaded.resolve(this);
}, true);
+
+ // load content.js to support webrtc, fullscreen, etc.
+ this.addEventListener("load", function loaded(event) {
+ this.removeEventListener("load", loaded, true);
+ let mm = this.content.messageManager;
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+ }, true);
+
if (this.src)
this.setAttribute("src", this.src);
]]></constructor>
<field name="_deferredChatLoaded" readonly="true">
Promise.defer();
</field>
--- a/browser/base/content/test/chat/browser_chatwindow.js
+++ b/browser/base/content/test/chat/browser_chatwindow.js
@@ -1,16 +1,40 @@
/* 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/. */
Components.utils.import("resource://gre/modules/Promise.jsm", this);
let chatbar = document.getElementById("pinnedchats");
+function waitForCondition(condition, errorMsg) {
+ let deferred = Promise.defer();
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() { clearInterval(interval); deferred.resolve(); };
+ return deferred.promise;
+}
+
add_chat_task(function* testOpenCloseChat() {
let chatbox = yield promiseOpenChat("http://example.com");
Assert.strictEqual(chatbox, chatbar.selectedChat);
// we requested a "normal" chat, so shouldn't be minimized
Assert.ok(!chatbox.minimized, "chat is not minimized");
Assert.equal(chatbar.childNodes.length, 1, "should be 1 chat open");
@@ -94,21 +118,25 @@ add_chat_task(function* testSecondTopLev
// Test that chats are created in the correct window.
add_chat_task(function* testChatWindowChooser() {
let chat = yield promiseOpenChat("http://example.com");
Assert.equal(numChatsInWindow(window), 1, "first window has the chat");
// create a second window - this will be the "most recent" and will
// therefore be the window that hosts the new chat (see bug 835111)
let secondWindow = OpenBrowserWindow();
yield promiseOneEvent(secondWindow, "load");
+ Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window");
Assert.equal(numChatsInWindow(secondWindow), 0, "second window starts with no chats");
yield promiseOpenChat("http://example.com#2");
Assert.equal(numChatsInWindow(secondWindow), 1, "second window now has chats");
Assert.equal(numChatsInWindow(window), 1, "first window still has 1 chat");
chat.close();
+
+ // a bit heavy handed, but handy fixing bug 1090633
+ yield waitForCondition(function () !chat.parentNode, "chat has been detached");
Assert.equal(numChatsInWindow(window), 0, "first window now has no chats");
// now open another chat - it should still open in the second.
yield promiseOpenChat("http://example.com#3");
Assert.equal(numChatsInWindow(window), 0, "first window still has no chats");
Assert.equal(numChatsInWindow(secondWindow), 2, "second window has both chats");
// focus the first window, and open yet another chat - it
// should open in the first window.
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -955,17 +955,18 @@
<binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
<resources>
<stylesheet src="chrome://browser/skin/searchbar.css"/>
</resources>
<content ignorekeys="true" level="top" consumeoutsideclicks="false">
<xul:hbox xbl:inherits="collapsed=showonlysettings" anonid="searchbar-engine"
class="search-panel-header search-panel-current-engine">
<xul:image class="searchbar-engine-image" xbl:inherits="src"/>
- <xul:label anonid="searchbar-engine-name" flex="1" crop="end"/>
+ <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
+ role="presentation"/>
</xul:hbox>
<xul:tree anonid="tree" flex="1"
class="autocomplete-tree plain search-panel-tree"
hidecolumnpicker="true" seltype="single">
<xul:treecols anonid="treecols">
<xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
</xul:treecols>
<xul:treechildren class="autocomplete-treebody"/>
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -451,16 +451,30 @@ let MozLoopServiceInternal = {
let credentials;
if (sessionToken) {
// true = use a hex key, as required by the server (see bug 1032738).
credentials = deriveHawkCredentials(sessionToken, "sessionToken",
2 * 32, true);
}
+ if (payloadObj) {
+ // Note: we must copy the object rather than mutate it, to avoid
+ // mutating the values of the object passed in.
+ let newPayloadObj = {};
+ for (let property of Object.getOwnPropertyNames(payloadObj)) {
+ if (typeof payloadObj[property] == "string") {
+ newPayloadObj[property] = CommonUtils.encodeUTF8(payloadObj[property]);
+ } else {
+ newPayloadObj[property] = payloadObj[property];
+ }
+ };
+ payloadObj = newPayloadObj;
+ }
+
return gHawkClient.request(path, method, credentials, payloadObj).then((result) => {
this.clearError("network");
return result;
}, (error) => {
if (error.code == 401) {
this.clearSessionToken(sessionType);
if (sessionType == LOOP_SESSION_TYPE.FXA) {
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
}
+ function _getContactDisplayName(contact) {
+ if (contact.name && contact.name[0]) {
+ return contact.name[0];
+ }
+ return _getPreferredEmail(contact).value;
+ }
+
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({displayName: 'CallIdentifierView',
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
* via children properties.
*/
var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
propTypes: {
contact: React.PropTypes.object
},
render: function() {
- var contactName;
-
- if (this.props.contact.name &&
- this.props.contact.name[0]) {
- contactName = this.props.contact.name[0];
- } else {
- contactName = _getPreferredEmail(this.props.contact).value;
- }
+ var contactName = _getContactDisplayName(this.props.contact);
document.title = contactName;
return (
React.DOM.div({className: "call-window"},
CallIdentifierView({
peerIdentifier: contactName,
showIcons: false}),
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
- this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+ roomOwner: navigator.mozLoop.userProfile.email,
+ roomName: _getContactDisplayName(this.props.contact)
+ }));
},
render: function() {
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title")),
React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")),
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(e => e.pref) || contact.email[0];
}
+ function _getContactDisplayName(contact) {
+ if (contact.name && contact.name[0]) {
+ return contact.name[0];
+ }
+ return _getPreferredEmail(contact).value;
+ }
+
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
* via children properties.
*/
var ConversationDetailView = React.createClass({
propTypes: {
contact: React.PropTypes.object
},
render: function() {
- var contactName;
-
- if (this.props.contact.name &&
- this.props.contact.name[0]) {
- contactName = this.props.contact.name[0];
- } else {
- contactName = _getPreferredEmail(this.props.contact).value;
- }
+ var contactName = _getContactDisplayName(this.props.contact);
document.title = contactName;
return (
<div className="call-window">
<CallIdentifierView
peerIdentifier={contactName}
showIcons={false} />
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
- this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+ roomOwner: navigator.mozLoop.userProfile.email,
+ roomName: _getContactDisplayName(this.props.contact)
+ }));
},
render: function() {
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
<p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -21,16 +21,17 @@
}
/* desktop version */
.fx-embedded .conversation-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
+ /* note that .room-invitation-overlay top matches this */
height: 26px;
}
/* standalone version */
.standalone .conversation-toolbar {
padding: 20px;
height: 64px;
}
@@ -746,17 +747,18 @@ html, .fx-embedded, #main,
.fx-embedded .room-conversation .conversation-toolbar .btn-hangup {
background-image: url("../img/icons-16x16.svg#leave");
}
.room-invitation-overlay {
position: absolute;
background: rgba(0, 0, 0, .6);
- top: 0;
+ /* This matches .fx-embedded .conversation toolbar height */
+ top: 26px;
right: 0;
bottom: 0;
left: 0;
text-align: center;
color: #fff;
z-index: 1010;
}
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -71,20 +71,22 @@ loop.shared.actions = (function() {
/**
* Used to signal when the window is being unloaded.
*/
WindowUnload: Action.define("windowUnload", {
}),
/**
- * Fetch a new call url from the server, intended to be sent over email when
+ * Fetch a new room url from the server, intended to be sent over email when
* a contact can't be reached.
*/
- FetchEmailLink: Action.define("fetchEmailLink", {
+ FetchRoomEmailLink: Action.define("fetchRoomEmailLink", {
+ roomOwner: String,
+ roomName: String
}),
/**
* Used to cancel call setup.
*/
CancelCall: Action.define("cancelCall", {
}),
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -205,17 +205,17 @@ loop.store = loop.store || {};
"connectionProgress",
"connectCall",
"hangupCall",
"remotePeerDisconnected",
"cancelCall",
"retryCall",
"mediaConnected",
"setMute",
- "fetchEmailLink"
+ "fetchRoomEmailLink"
]);
this.setStoreState({
contact: actionData.contact,
outgoing: windowType === "outgoing",
windowId: actionData.windowId,
callType: actionData.callType,
callState: CALL_STATES.GATHER,
@@ -318,28 +318,31 @@ loop.store = loop.store || {};
*/
setMute: function(actionData) {
var newState = {};
newState[actionData.type + "Muted"] = !actionData.enabled;
this.setStoreState(newState);
},
/**
- * Fetches a new call URL intended to be sent over email when a contact
+ * Fetches a new room URL intended to be sent over email when a contact
* can't be reached.
*/
- fetchEmailLink: function() {
- // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
- // a user-set string.
- this.client.requestCallUrl("", function(err, callUrlData) {
+ fetchRoomEmailLink: function(actionData) {
+ this.mozLoop.rooms.create({
+ roomName: actionData.roomName,
+ roomOwner: actionData.roomOwner,
+ maxSize: loop.store.MAX_ROOM_CREATION_SIZE,
+ expiresIn: loop.store.DEFAULT_EXPIRES_IN
+ }, function(err, createdRoomData) {
if (err) {
this.trigger("error:emailLink");
return;
}
- this.setStoreState({"emailLink": callUrlData.callUrl});
+ this.setStoreState({"emailLink": createdRoomData.roomUrl});
}.bind(this));
},
/**
* Obtains the outgoing call data from the server and handles the
* result.
*/
_setupOutgoingCall: function() {
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -12,16 +12,30 @@ loop.store = loop.store || {};
/**
* Shared actions.
* @type {Object}
*/
var sharedActions = loop.shared.actions;
/**
+ * Maximum size given to createRoom; only 2 is supported (and is
+ * always passed) because that's what the user-experience is currently
+ * designed and tested to handle.
+ * @type {Number}
+ */
+ var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;
+
+ /**
+ * The number of hours for which the room will exist - default 8 weeks
+ * @type {Number}
+ */
+ var DEFAULT_EXPIRES_IN = loop.store.DEFAULT_EXPIRES_IN = 24 * 7 * 8;
+
+ /**
* Room validation schema. See validate.js.
* @type {Object}
*/
var roomSchema = {
roomToken: String,
roomUrl: String,
roomName: String,
maxSize: Number,
@@ -56,23 +70,23 @@ loop.store = loop.store || {};
*/
loop.store.RoomStore = loop.store.createStore({
/**
* Maximum size given to createRoom; only 2 is supported (and is
* always passed) because that's what the user-experience is currently
* designed and tested to handle.
* @type {Number}
*/
- maxRoomCreationSize: 2,
+ maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
/**
* The number of hours for which the room will exist - default 8 weeks
* @type {Number}
*/
- defaultExpiresIn: 24 * 7 * 8,
+ defaultExpiresIn: DEFAULT_EXPIRES_IN,
/**
* Registered actions.
* @type {Array}
*/
actions: [
"createRoom",
"createRoomError",
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -51,17 +51,20 @@ describe("loop.conversationViews", funct
return {
version: "42",
channel: "test",
platform: "test"
};
},
getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
- })
+ }),
+ userProfile: {
+ email: "bob@invalid.tld"
+ }
};
fakeWindow = {
navigator: { mozLoop: fakeMozLoop },
close: sandbox.stub(),
};
loop.shared.mixins.setRootObject(fakeWindow);
@@ -236,22 +239,25 @@ describe("loop.conversationViews", funct
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
});
describe("CallFailedView", function() {
var store, fakeAudio;
- function mountTestComponent(props) {
+ var contact = {email: [{value: "test@test.tld"}]};
+
+ function mountTestComponent(options) {
+ options = options || {};
return TestUtils.renderIntoDocument(
loop.conversationViews.CallFailedView({
dispatcher: dispatcher,
store: store,
- contact: {email: [{value: "test@test.tld"}]}
+ contact: options.contact
}));
}
beforeEach(function() {
store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: {}
@@ -261,101 +267,121 @@ describe("loop.conversationViews", funct
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
});
it("should dispatch a retryCall action when the retry button is pressed",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
var retryBtn = view.getDOMNode().querySelector('.btn-retry');
React.addons.TestUtils.Simulate.click(retryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "retryCall"));
});
it("should dispatch a cancelCall action when the cancel button is pressed",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
React.addons.TestUtils.Simulate.click(cancelBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
- it("should dispatch a fetchEmailLink action when the cancel button is pressed",
+ it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
- sinon.match.hasOwn("name", "fetchEmailLink"));
+ sinon.match.hasOwn("name", "fetchRoomEmailLink"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("roomOwner", fakeMozLoop.userProfile.email));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("roomName", "test@test.tld"));
+ });
+
+ it("should name the created room using the contact name when available",
+ function() {
+ view = mountTestComponent({contact: {
+ email: [{value: "test@test.tld"}],
+ name: ["Mr Fake ContactName"]
+ }});
+
+ var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
+
+ React.addons.TestUtils.Simulate.click(emailLinkBtn);
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
});
it("should disable the email link button once the action is dispatched",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn);
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
});
it("should compose an email once the email link is received", function() {
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail,
"http://fake.invalid/", "test@test.tld");
});
it("should close the conversation window once the email link is received",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(fakeWindow.close);
});
it("should display an error message in case email link retrieval failed",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
store.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".error")).not.eql(null);
});
it("should allow retrying to get a call url if it failed previously",
function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
store.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
});
it("should play a failure sound, once", function() {
- view = mountTestComponent();
+ view = mountTestComponent({contact: contact});
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
});
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -37,16 +37,19 @@ describe("loop.store.ConversationStore",
};
fakeMozLoop = {
getLoopPref: sandbox.stub(),
addConversationContext: sandbox.stub(),
calls: {
setCallInProgress: sandbox.stub(),
clearCallInProgress: sandbox.stub()
+ },
+ rooms: {
+ create: sandbox.stub()
}
};
dispatcher = new loop.Dispatcher();
client = {
setupOutgoingCall: sinon.stub(),
requestCallUrl: sinon.stub()
};
@@ -696,41 +699,53 @@ describe("loop.store.ConversationStore",
type: "video",
enabled: false
}));
expect(store.getStoreState("videoMuted")).eql(true);
});
});
- describe("#fetchEmailLink", function() {
+ describe("#fetchRoomEmailLink", function() {
it("should request a new call url to the server", function() {
- store.fetchEmailLink(new sharedActions.FetchEmailLink());
+ store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+ roomOwner: "bob@invalid.tld",
+ roomName: "FakeRoomName"
+ }));
- sinon.assert.calledOnce(client.requestCallUrl);
- sinon.assert.calledWith(client.requestCallUrl, "");
+ sinon.assert.calledOnce(fakeMozLoop.rooms.create);
+ sinon.assert.calledWithMatch(fakeMozLoop.rooms.create, {
+ roomOwner: "bob@invalid.tld",
+ roomName: "FakeRoomName"
+ });
});
- it("should update the emailLink attribute when the new call url is received",
+ it("should update the emailLink attribute when the new room url is received",
function() {
- client.requestCallUrl = function(callId, cb) {
- cb(null, {callUrl: "http://fake.invalid/"});
+ fakeMozLoop.rooms.create = function(roomData, cb) {
+ cb(null, {roomUrl: "http://fake.invalid/"});
};
- store.fetchEmailLink(new sharedActions.FetchEmailLink());
+ store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+ roomOwner: "bob@invalid.tld",
+ roomName: "FakeRoomName"
+ }));
expect(store.getStoreState("emailLink")).eql("http://fake.invalid/");
});
it("should trigger an error:emailLink event in case of failure",
function() {
var trigger = sandbox.stub(store, "trigger");
- client.requestCallUrl = function(callId, cb) {
- cb("error");
+ fakeMozLoop.rooms.create = function(roomData, cb) {
+ cb(new Error("error"));
};
- store.fetchEmailLink(new sharedActions.FetchEmailLink());
+ store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+ roomOwner: "bob@invalid.tld",
+ roomName: "FakeRoomName"
+ }));
sinon.assert.calledOnce(trigger);
sinon.assert.calledWithExactly(trigger, "error:emailLink");
});
});
describe("Events", function() {
describe("Websocket progress", function() {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_hawk_request.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Unit tests for handling hawkRequest
+ */
+
+"use strict";
+
+Cu.import("resource://services-common/utils.js");
+
+add_task(function* request_with_unicode() {
+ const unicodeName = "yøü";
+
+ loopServer.registerPathHandler("/fake", (request, response) => {
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let jsonBody = JSON.parse(body);
+ Assert.equal(jsonBody.name, CommonUtils.encodeUTF8(unicodeName));
+
+ response.setStatusLine(null, 200, "OK");
+ response.processAsync();
+ response.finish();
+ });
+
+ yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/fake", "POST", {name: unicodeName}).then(
+ () => Assert.ok(true, "Should have accepted"),
+ () => Assert.ok(false, "Should have accepted"));
+});
+
+function run_test() {
+ setupFakeLoopServer();
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("loop.hawk-session-token");
+ Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+ MozLoopService.errors.clear();
+ });
+
+ run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -6,16 +6,17 @@ skip-if = toolkit == 'gonk'
[test_loopapi_hawk_request.js]
[test_looppush_initialize.js]
[test_looprooms.js]
[test_loopservice_directcall.js]
[test_loopservice_dnd.js]
[test_loopservice_expiry.js]
[test_loopservice_hawk_errors.js]
+[test_loopservice_hawk_request.js]
[test_loopservice_loop_prefs.js]
[test_loopservice_initialize.js]
[test_loopservice_locales.js]
[test_loopservice_notification.js]
[test_loopservice_registration.js]
[test_loopservice_registration_retry.js]
[test_loopservice_restart.js]
[test_loopservice_token_invalid.js]
--- a/browser/components/preferences/permissions.xul
+++ b/browser/components/preferences/permissions.xul
@@ -68,15 +68,13 @@
accesskey="&removepermission.accesskey;"
icon="remove" label="&removepermission.label;"
oncommand="gPermissionManager.onPermissionDeleted();"/>
<button id="removeAllPermissions"
icon="clear" label="&removeallpermissions.label;"
accesskey="&removeallpermissions.accesskey;"
oncommand="gPermissionManager.onAllPermissionsDeleted();"/>
<spacer flex="1"/>
-#ifndef XP_MACOSX
<button oncommand="close();" icon="close"
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
-#endif
</hbox>
</hbox>
</window>
--- a/browser/components/preferences/translation.xul
+++ b/browser/components/preferences/translation.xul
@@ -75,15 +75,13 @@
accesskey="&removeSite.accesskey;"
icon="remove" label="&removeSite.label;"
oncommand="gTranslationExceptions.onSiteDeleted();"/>
<button id="removeAllSites"
icon="clear" label="&removeAllSites.label;"
accesskey="&removeAllSites.accesskey;"
oncommand="gTranslationExceptions.onAllSitesDeleted();"/>
<spacer flex="1"/>
-#ifndef XP_MACOSX
<button oncommand="close();" icon="close"
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
-#endif
</hbox>
</hbox>
</window>
--- a/browser/devtools/debugger/test/browser_dbg_chrome-create.js
+++ b/browser/devtools/debugger/test/browser_dbg_chrome-create.js
@@ -5,16 +5,17 @@
* Tests that a chrome debugger can be created in a new process.
*/
let gProcess;
function test() {
// Windows XP and 8.1 test slaves are terribly slow at this test.
requestLongerTimeout(5);
+ Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
initChromeDebugger(aOnClose).then(aProcess => {
gProcess = aProcess;
info("Starting test...");
performTest();
});
}
@@ -51,10 +52,11 @@ function aOnClose() {
info("process exit value: " + gProcess._dbgProcess.exitValue);
info("profile path: " + gProcess._dbgProfilePath);
finish();
}
registerCleanupFunction(function() {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
gProcess = null;
});
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -137,17 +137,19 @@ BrowserToolboxProcess.prototype = {
if (!this.debuggerServer.initialized) {
this.debuggerServer.init();
this.debuggerServer.addBrowserActors();
dumpn("initialized and added the browser actors for the DebuggerServer.");
}
let chromeDebuggingPort =
Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
- this.debuggerServer.openListener(chromeDebuggingPort);
+ let listener = this.debuggerServer.createListener();
+ listener.portOrPath = chromeDebuggingPort;
+ listener.open();
dumpn("Finished initializing the chrome toolbox server.");
dumpn("Started listening on port: " + chromeDebuggingPort);
},
/**
* Initializes a profile for the remote debugger process.
*/
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -36,51 +36,55 @@ window.addEventListener("DOMContentLoade
}
if (port) {
document.getElementById("port").value = port;
}
let form = document.querySelector("#connection-form form");
form.addEventListener("submit", function() {
- window.submit();
+ window.submit().catch(e => {
+ Cu.reportError(e);
+ // Bug 921850: catch rare exception from DebuggerClient.socketConnect
+ showError("unexpected");
+ });
});
}, true);
/**
* Called when the "connect" button is clicked.
*/
-function submit() {
+let submit = Task.async(function*() {
// Show the "connecting" screen
document.body.classList.add("connecting");
let host = document.getElementById("host").value;
let port = document.getElementById("port").value;
// Save the host/port values
try {
Services.prefs.setCharPref("devtools.debugger.remote-host", host);
Services.prefs.setIntPref("devtools.debugger.remote-port", port);
} catch(e) {
// Fails in e10s mode, but not a critical feature.
}
// Initiate the connection
- let transport;
- try {
- transport = DebuggerClient.socketConnect(host, port);
- } catch(e) {
- // Bug 921850: catch rare exception from DebuggerClient.socketConnect
- showError("unexpected");
- return;
- }
+ let transport = yield DebuggerClient.socketConnect({ host, port });
gClient = new DebuggerClient(transport);
let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
- gClient.connect(onConnectionReady);
+ let response = yield clientConnect();
+ yield onConnectionReady(...response);
+});
+
+function clientConnect() {
+ let deferred = promise.defer();
+ gClient.connect((...args) => deferred.resolve(args));
+ return deferred.promise;
}
/**
* Connection is ready. List actors and build buttons.
*/
let onConnectionReady = Task.async(function*(aType, aTraits) {
clearTimeout(gConnectionTimeout);
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -7,61 +7,62 @@ const { classes: Cc, interfaces: Ci, uti
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { DebuggerClient } =
Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
let { ViewHelpers } =
Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
/**
* Shortcuts for accessing various debugger preferences.
*/
let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
chromeDebuggingHost: ["Char", "chrome-debugging-host"],
chromeDebuggingPort: ["Int", "chrome-debugging-port"]
});
let gToolbox, gClient;
-function connect() {
+let connect = Task.async(function*() {
window.removeEventListener("load", connect);
// Initiate the connection
- let transport = DebuggerClient.socketConnect(
- Prefs.chromeDebuggingHost,
- Prefs.chromeDebuggingPort
- );
+ let transport = yield DebuggerClient.socketConnect({
+ host: Prefs.chromeDebuggingHost,
+ port: Prefs.chromeDebuggingPort
+ });
gClient = new DebuggerClient(transport);
gClient.connect(() => {
let addonID = getParameterByName("addonID");
if (addonID) {
gClient.listAddons(({addons}) => {
let addonActor = addons.filter(addon => addon.id === addonID).pop();
openToolbox(addonActor);
});
} else {
gClient.listTabs(openToolbox);
}
});
-}
+});
// Certain options should be toggled since we can assume chrome debugging here
function setPrefDefaults() {
Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
Services.prefs.setBoolPref("devtools.profiler.ui.show-platform-data", true);
Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", false);
}
window.addEventListener("load", function() {
let cmdClose = document.getElementById("toolbox-cmd-close");
cmdClose.addEventListener("command", onCloseCommand);
setPrefDefaults();
- connect();
+ connect().catch(Cu.reportError);
});
function onCloseCommand(event) {
window.close();
}
function openToolbox(form) {
let options = {
--- a/browser/devtools/performance/test/browser_perf-details.js
+++ b/browser/devtools/performance/test/browser_perf-details.js
@@ -20,27 +20,25 @@ function spawnTest () {
// Select waterfall view
viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
command($("toolbarbutton[data-view='waterfall']"));
[_, viewName] = yield viewChanged;
is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
checkViews(DetailsView, doc, "waterfall");
-
yield teardown(panel);
finish();
}
function checkViews (DetailsView, doc, currentView) {
- for (let viewName in DetailsView.views) {
- let view = DetailsView.views[viewName].el;
- let button = doc.querySelector("toolbarbutton[data-view='" + viewName + "']");
+ for (let viewName in DetailsView.viewIndexes) {
+ let button = doc.querySelector(`toolbarbutton[data-view="${viewName}"]`);
+ is(DetailsView.el.selectedIndex, DetailsView.viewIndexes[currentView],
+ `DetailsView correctly has ${currentView} selected.`);
if (viewName === currentView) {
- ok(!view.getAttribute("hidden"), view + " view displayed");
- ok(button.getAttribute("checked"), view + " button checked");
+ ok(button.getAttribute("checked"), `${viewName} button checked`);
} else {
- ok(view.getAttribute("hidden"), view + " view hidden");
- ok(!button.getAttribute("checked"), view + " button not checked");
+ ok(!button.getAttribute("checked"), `${viewName} button not checked`);
}
}
}
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -40,16 +40,24 @@ let DetailsView = {
* Select one of the DetailView's subviews to be rendered,
* hiding the others.
*
* @params {String} selectedView
* Name of the view to be shown.
*/
selectView: function (selectedView) {
this.el.selectedIndex = this.viewIndexes[selectedView];
+
+ for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
+ if (button.getAttribute("data-view") === selectedView)
+ button.setAttribute("checked", true);
+ else
+ button.removeAttribute("checked");
+ }
+
this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
},
/**
* Called when a view button is clicked.
*/
_onViewToggle: function (e) {
this.selectView(e.target.getAttribute("data-view"));
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -511,17 +511,17 @@ var Scratchpad = {
{
let deferred = promise.defer();
let reject = aReason => deferred.reject(aReason);
this.execute().then(([aString, aError, aResult]) => {
let resolve = () => deferred.resolve([aString, aError, aResult]);
if (aError) {
- this.writeAsErrorComment(aError.exception).then(resolve, reject);
+ this.writeAsErrorComment(aError).then(resolve, reject);
}
else {
this.editor.dropSelection();
resolve();
}
}, reject);
return deferred.promise;
@@ -538,17 +538,17 @@ var Scratchpad = {
{
let deferred = promise.defer();
let reject = aReason => deferred.reject(aReason);
this.execute().then(([aString, aError, aResult]) => {
let resolve = () => deferred.resolve([aString, aError, aResult]);
if (aError) {
- this.writeAsErrorComment(aError.exception).then(resolve, reject);
+ this.writeAsErrorComment(aError).then(resolve, reject);
}
else {
this.editor.dropSelection();
this.sidebar.open(aString, aResult).then(resolve, reject);
}
}, reject);
return deferred.promise;
@@ -603,17 +603,17 @@ var Scratchpad = {
{
let deferred = promise.defer();
let reject = aReason => deferred.reject(aReason);
this.execute().then(([aString, aError, aResult]) => {
let resolve = () => deferred.resolve([aString, aError, aResult]);
if (aError) {
- this.writeAsErrorComment(aError.exception).then(resolve, reject);
+ this.writeAsErrorComment(aError).then(resolve, reject);
}
else if (VariablesView.isPrimitive({ value: aResult })) {
this._writePrimitiveAsComment(aResult).then(resolve, reject);
}
else {
let objectClient = new ObjectClient(this.debuggerClient, aResult);
objectClient.getDisplayString(aResponse => {
if (aResponse.error) {
@@ -664,17 +664,17 @@ var Scratchpad = {
const onReply = ({ data }) => {
if (data.id !== id) {
return;
}
this.prettyPrintWorker.removeEventListener("message", onReply, false);
if (data.error) {
let errorString = DevToolsUtils.safeErrorString(data.error);
- this.writeAsErrorComment(errorString);
+ this.writeAsErrorComment({ exception: errorString });
deferred.reject(errorString);
} else {
this.editor.setText(data.code);
deferred.resolve(data.code);
}
};
this.prettyPrintWorker.addEventListener("message", onReply, false);
@@ -691,17 +691,17 @@ var Scratchpad = {
/**
* Parse the text and return an AST. If we can't parse it, write an error
* comment and return false.
*/
_parseText: function SP__parseText(aText) {
try {
return Reflect.parse(aText);
} catch (e) {
- this.writeAsErrorComment(DevToolsUtils.safeErrorString(e));
+ this.writeAsErrorComment({ exception: DevToolsUtils.safeErrorString(e) });
return false;
}
},
/**
* Determine if the given AST node location contains the given cursor
* position.
*
@@ -908,43 +908,54 @@ var Scratchpad = {
let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
this.editor.setSelection(from, to);
},
/**
* Write out an error at the current insertion point as a block comment
* @param object aValue
- * The Error object to write out the message and stack trace
+ * The error object to write out the message and stack trace. It must
+ * contain an |exception| property with the actual error thrown, but it
+ * will often be the entire response of an evaluateJS request.
* @return Promise
* The promise that indicates when writing the comment completes.
*/
writeAsErrorComment: function SP_writeAsErrorComment(aError)
{
let deferred = promise.defer();
- if (VariablesView.isPrimitive({ value: aError })) {
- let type = aError.type;
+ if (VariablesView.isPrimitive({ value: aError.exception })) {
+ let error = aError.exception;
+ let type = error.type;
if (type == "undefined" ||
type == "null" ||
type == "Infinity" ||
type == "-Infinity" ||
type == "NaN" ||
type == "-0") {
deferred.resolve(type);
}
else if (type == "longString") {
- deferred.resolve(aError.initial + "\u2026");
+ deferred.resolve(error.initial + "\u2026");
}
else {
- deferred.resolve(aError);
+ deferred.resolve(error);
}
- }
- else {
- let objectClient = new ObjectClient(this.debuggerClient, aError);
+ } else if ("preview" in aError.exception) {
+ let error = aError.exception;
+ let stack = this._constructErrorStack(error.preview);
+ if (typeof aError.exceptionMessage == "string") {
+ deferred.resolve(aError.exceptionMessage + stack);
+ } else {
+ deferred.resolve(stack);
+ }
+ } else {
+ // If there is no preview information, we need to ask the server for more.
+ let objectClient = new ObjectClient(this.debuggerClient, aError.exception);
objectClient.getPrototypeAndProperties(aResponse => {
if (aResponse.error) {
deferred.reject(aResponse);
return;
}
let { ownProperties, safeGetterValues } = aResponse;
let error = Object.create(null);
@@ -953,32 +964,17 @@ var Scratchpad = {
for (let key of Object.keys(safeGetterValues)) {
error[key] = safeGetterValues[key].getterValue;
}
for (let key of Object.keys(ownProperties)) {
error[key] = ownProperties[key].value;
}
- // Assemble the best possible stack we can given the properties we have.
- let stack;
- if (typeof error.stack == "string" && error.stack) {
- stack = error.stack;
- }
- else if (typeof error.fileName == "string") {
- stack = "@" + error.fileName;
- if (typeof error.lineNumber == "number") {
- stack += ":" + error.lineNumber;
- }
- }
- else if (typeof error.lineNumber == "number") {
- stack = "@" + error.lineNumber;
- }
-
- stack = stack ? "\n" + stack.replace(/\n$/, "") : "";
+ let stack = this._constructErrorStack(error);
if (typeof error.message == "string") {
deferred.resolve(error.message + stack);
}
else {
objectClient.getDisplayString(aResponse => {
if (aResponse.error) {
deferred.reject(aResponse);
@@ -995,16 +991,47 @@ var Scratchpad = {
}
return deferred.promise.then(aMessage => {
console.error(aMessage);
this.writeAsComment("Exception: " + aMessage);
});
},
+ /**
+ * Assembles the best possible stack from the properties of the provided
+ * error.
+ */
+ _constructErrorStack(error) {
+ let stack;
+ if (typeof error.stack == "string" && error.stack) {
+ stack = error.stack;
+ } else if (typeof error.fileName == "string") {
+ stack = "@" + error.fileName;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ }
+ } else if (typeof error.filename == "string") {
+ stack = "@" + error.filename;
+ if (typeof error.lineNumber == "number") {
+ stack += ":" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+ } else if (typeof error.lineNumber == "number") {
+ stack = "@" + error.lineNumber;
+ if (typeof error.columnNumber == "number") {
+ stack += ":" + error.columnNumber;
+ }
+ }
+
+ return stack ? "\n" + stack.replace(/\n$/, "") : "";
+ },
+
// Menu Operations
/**
* Open a new Scratchpad window.
*
* @return nsIWindow
*/
openScratchpad: function SP_openScratchpad()
--- a/browser/devtools/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
@@ -36,17 +36,17 @@ function runTests()
result: message + openComment + "Hello World!" + closeComment,
label: "message display output"
},
{
// Display error1, throw new Error("Ouch")
method: "display",
code: error1,
result: error1 + openComment +
- "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+ "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
label: "error display output"
},
{
// Display error2, throw "A thrown string"
method: "display",
code: error2,
result: error2 + openComment + "Exception: A thrown string" + closeComment,
label: "thrown string display output"
@@ -57,34 +57,34 @@ function runTests()
code: error3,
result: error3 + openComment + "Exception: [object Object]" + closeComment,
label: "thrown object display output"
},
{
// Display error4, document.body.appendChild(document.body)
method: "display",
code: error4,
- result: error4 + openComment + "Exception: Node cannot be inserted " +
+ result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
"at the specified point in the hierarchy\n@" +
scratchpad.uniqueName + ":1:0" + closeComment,
label: "Alternative format error display output"
},
{
// Run message
method: "run",
code: message,
result: message,
label: "message run output"
},
{
// Run error1, throw new Error("Ouch")
method: "run",
code: error1,
result: error1 + openComment +
- "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+ "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
label: "error run output"
},
{
// Run error2, throw "A thrown string"
method: "run",
code: error2,
result: error2 + openComment + "Exception: A thrown string" + closeComment,
label: "thrown string run output"
@@ -95,16 +95,16 @@ function runTests()
code: error3,
result: error3 + openComment + "Exception: [object Object]" + closeComment,
label: "thrown object run output"
},
{
// Run error4, document.body.appendChild(document.body)
method: "run",
code: error4,
- result: error4 + openComment + "Exception: Node cannot be inserted " +
+ result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
"at the specified point in the hierarchy\n@" +
scratchpad.uniqueName + ":1:0" + closeComment,
label: "Alternative format error run output"
}];
runAsyncTests(scratchpad, tests).then(finish);
}
--- a/browser/devtools/scratchpad/test/browser_scratchpad_display_outputs_errors.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_display_outputs_errors.js
@@ -31,42 +31,42 @@ function runTests()
method: "display",
code: message,
result: message + openComment + "Hello World!" + closeComment,
label: "message display output"
},
{
method: "display",
code: error,
- result: error + openComment + "Exception: Ouch!\n@" +
+ result: error + openComment + "Exception: Error: Ouch!\n@" +
scratchpad.uniqueName + ":1:7" + closeComment,
label: "error display output",
},
{
method: "display",
code: syntaxError,
- result: syntaxError + openComment + "Exception: expected expression, got end of script\n@" +
+ result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
scratchpad.uniqueName + ":1" + closeComment,
label: "syntaxError display output",
},
{
method: "run",
code: message,
result: message,
label: "message run output",
},
{
method: "run",
code: error,
- result: error + openComment + "Exception: Ouch!\n@" +
+ result: error + openComment + "Exception: Error: Ouch!\n@" +
scratchpad.uniqueName + ":1:7" + closeComment,
label: "error run output",
},
{
method: "run",
code: syntaxError,
- result: syntaxError + openComment + "Exception: expected expression, got end of script\n@" +
+ result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
scratchpad.uniqueName + ":1" + closeComment,
label: "syntaxError run output",
}];
runAsyncTests(scratchpad, tests).then(finish);
}
--- a/browser/devtools/scratchpad/test/head.js
+++ b/browser/devtools/scratchpad/test/head.js
@@ -4,16 +4,17 @@
"use strict";
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {DevToolsUtils} = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
let gScratchpadWindow; // Reference to the Scratchpad chrome window object
gDevTools.testing = true;
SimpleTest.registerCleanupFunction(() => {
gDevTools.testing = false;
});
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -410,16 +410,26 @@ Tooltip.prototype = {
_onBaseNodeMouseMove: function(event) {
if (event.target !== this._lastHovered) {
this.hide();
this._lastHovered = event.target;
setNamedTimeout(this.uid, this._showDelay, () => {
this.isValidHoverTarget(event.target).then(target => {
this.show(target);
+ }).catch((reason) => {
+ if (reason === false) {
+ // isValidHoverTarget rejects with false if the tooltip should
+ // not be shown. This can be safely ignored.
+ return;
+ }
+ // Report everything else. Reason might be error that should not be
+ // hidden.
+ console.error("isValidHoverTarget rejected with an unexpected reason:");
+ console.error(reason);
});
});
}
},
/**
* Is the given target DOMNode a valid node for toggling the tooltip on hover.
* This delegates to the user-defined _targetNodeCb callback.
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -341,16 +341,18 @@ let AppManager = exports.AppManager = {
deferred.resolve();
} else {
deferred.reject();
}
}
this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
try {
+ // Reset the connection's state to defaults
+ this.connection.resetOptions();
this.selectedRuntime.connect(this.connection).then(
() => {},
deferred.reject.bind(deferred));
} catch(e) {
console.error(e);
deferred.reject();
}
}, deferred.reject);
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -444,16 +444,17 @@ WiFiRuntime.prototype = {
type: RuntimeTypes.WIFI,
connect: function(connection) {
let service = discovery.getRemoteService("devtools", this.deviceName);
if (!service) {
return promise.reject("Can't find device: " + this.name);
}
connection.host = service.host;
connection.port = service.port;
+ connection.encryption = service.encryption;
connection.connect();
return promise.resolve();
},
get id() {
return this.deviceName;
},
get name() {
return this.deviceName;
--- a/browser/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -231,17 +231,19 @@ var BrowserUI = {
*/
runDebugServer: function runDebugServer(aPort) {
let port = aPort || Services.prefs.getIntPref(debugServerPortChanged);
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
DebuggerServer.addActors('chrome://browser/content/dbg-metro-actors.js');
}
- DebuggerServer.openListener(port);
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = port;
+ listener.open();
},
stopDebugServer: function stopDebugServer() {
if (DebuggerServer.initialized) {
DebuggerServer.destroy();
}
},
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -1640,17 +1640,17 @@ this.UITour = {
},
notify(eventName, params) {
let winEnum = Services.wm.getEnumerator("navigator:browser");
while (winEnum.hasMoreElements()) {
let window = winEnum.getNext();
if (window.closed)
continue;
-debugger;
+
let originTabs = this.originTabs.get(window);
if (!originTabs)
continue;
for (let tab of originTabs) {
let messageManager = tab.linkedBrowser.messageManager;
let detail = {
event: eventName,
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -65,16 +65,23 @@ this.webrtcUI = {
let browser = aStream.browser;
let browserWindow = browser.ownerDocument.defaultView;
let tab = browserWindow.gBrowser &&
browserWindow.gBrowser.getTabForBrowser(browser);
return {uri: state.documentURI, tab: tab, browser: browser, types: types};
});
},
+ swapBrowserForNotification: function(aOldBrowser, aNewBrowser) {
+ for (let stream of this._streams) {
+ if (stream.browser == aOldBrowser)
+ stream.browser = aNewBrowser;
+ };
+ },
+
showSharingDoorhanger: function(aActiveStream, aType) {
let browserWindow = aActiveStream.browser.ownerDocument.defaultView;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
} else {
aActiveStream.browser.focus();
}
browserWindow.focus();
@@ -179,41 +186,43 @@ function prompt(aBrowser, aRequest) {
let mainLabel;
if (sharingScreen) {
mainLabel = stringBundle.getString("getUserMedia.shareSelectedItems.label");
}
else {
let string = stringBundle.getString("getUserMedia.shareSelectedDevices.label");
mainLabel = PluralForm.get(requestTypes.length, string);
}
+
+ let notification; // Used by action callbacks.
let mainAction = {
label: mainLabel,
accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
// The real callback will be set during the "showing" event. The
// empty function here is so that PopupNotifications.show doesn't
// reject the action.
callback: function() {}
};
let secondaryActions = [
{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
- denyRequest(aBrowser, aRequest);
+ denyRequest(notification.browser, aRequest);
}
}
];
if (!sharingScreen) { // Bug 1037438: implement 'never' for screen sharing.
secondaryActions.push({
label: stringBundle.getString("getUserMedia.never.label"),
accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
callback: function () {
- denyRequest(aBrowser, aRequest);
+ denyRequest(notification.browser, aRequest);
// Let someone save "Never" for http sites so that they can be stopped from
// bothering you with doorhangers.
let perms = Services.perms;
if (audioDevices.length)
perms.add(uri, "microphone", perms.DENY_ACTION);
if (videoDevices.length)
perms.add(uri, "camera", perms.DENY_ACTION);
}
@@ -277,20 +286,20 @@ function prompt(aBrowser, aRequest) {
// and will grant audio access immediately.
if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
// All permissions we were about to request are already persistently set.
let allowedDevices = [];
if (videoDevices.length && camPerm == perms.ALLOW_ACTION)
allowedDevices.push(videoDevices[0].deviceIndex);
if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
allowedDevices.push(audioDevices[0].deviceIndex);
- aBrowser.messageManager.sendAsyncMessage("webrtc:Allow",
- {callID: aRequest.callID,
- windowID: aRequest.windowID,
- devices: allowedDevices});
+ let mm = this.browser.messageManager;
+ mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+ windowID: aRequest.windowID,
+ devices: allowedDevices});
this.remove();
return true;
}
}
function listDevices(menupopup, devices) {
while (menupopup.lastChild)
menupopup.removeChild(menupopup.lastChild);
@@ -399,36 +408,38 @@ function prompt(aBrowser, aRequest) {
allowedDevices.push(audioDeviceIndex);
if (aRemember) {
perms.add(uri, "microphone",
allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (!allowedDevices.length) {
- denyRequest(aBrowser, aRequest);
+ denyRequest(notification.browser, aRequest);
return;
}
- aBrowser.messageManager.sendAsyncMessage("webrtc:Allow",
- {callID: aRequest.callID,
- windowID: aRequest.windowID,
- devices: allowedDevices});
+ let mm = notification.browser.messageManager
+ mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+ windowID: aRequest.windowID,
+ devices: allowedDevices});
};
return false;
}
};
let anchorId = "webRTC-shareDevices-notification-icon";
if (requestTypes.length == 1 && requestTypes[0] == "Microphone")
anchorId = "webRTC-shareMicrophone-notification-icon";
if (requestTypes.indexOf("Screen") != -1)
anchorId = "webRTC-shareScreen-notification-icon";
- chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
- anchorId, mainAction, secondaryActions, options);
+ notification =
+ chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
+ anchorId, mainAction, secondaryActions,
+ options);
}
function getGlobalIndicator() {
#ifndef XP_MACOSX
const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xul";
const features = "chrome,dialog=yes,titlebar=no,popup=yes";
return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
@@ -696,16 +707,17 @@ function updateBrowserSpecificIndicator(
} else if (aState.microphone) {
captureState = "Microphone";
}
let chromeWin = aBrowser.ownerDocument.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let windowId = aState.windowId;
+ let notification; // Used by action callbacks.
let mainAction = {
label: stringBundle.getString("getUserMedia.continueSharing.label"),
accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
callback: function () {},
dismiss: true
};
let secondaryActions = [{
label: stringBundle.getString("getUserMedia.stopSharing.label"),
@@ -716,73 +728,89 @@ function updateBrowserSpecificIndicator(
let perms = Services.perms;
if (aState.camera &&
perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
perms.remove(host, "camera");
if (aState.microphone &&
perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
perms.remove(host, "microphone");
- aBrowser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
+ let mm = notification.browser.messageManager;
+ mm.sendAsyncMessage("webrtc:StopSharing", windowId);
}
}];
let options = {
hideNotNow: true,
dismissed: true,
- eventCallback: function(aTopic) {
+ eventCallback: function(aTopic, aNewBrowser) {
if (aTopic == "shown") {
let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
let popupId = captureState == "Microphone" ? "Microphone" : "Devices";
PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId);
}
- return aTopic == "swapping";
+
+ if (aTopic == "swapping") {
+ webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
+ return true;
+ }
+
+ return false;
}
};
if (captureState) {
let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon"
: "webRTC-sharingDevices-notification-icon";
let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
- chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
- anchorId, mainAction, secondaryActions, options);
+ notification =
+ chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
+ anchorId, mainAction, secondaryActions, options);
}
else {
removeBrowserNotification(aBrowser,"webRTC-sharingDevices");
}
// Now handle the screen sharing indicator.
if (!aState.screen) {
removeBrowserNotification(aBrowser,"webRTC-sharingScreen");
return;
}
+ let screenSharingNotif; // Used by action callbacks.
options = {
hideNotNow: true,
dismissed: true,
- eventCallback: function(aTopic) {
+ eventCallback: function(aTopic, aNewBrowser) {
if (aTopic == "shown") {
let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharingScreen");
}
- return aTopic == "swapping";
+
+ if (aTopic == "swapping") {
+ webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
+ return true;
+ }
+
+ return false;
}
};
secondaryActions = [{
label: stringBundle.getString("getUserMedia.stopSharing.label"),
accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
callback: function () {
- aBrowser.messageManager.sendAsyncMessage("webrtc:StopSharing",
- "screen:" + windowId);
+ let mm = screenSharingNotif.browser.messageManager;
+ mm.sendAsyncMessage("webrtc:StopSharing", "screen:" + windowId);
}
}];
// If we are sharing both a window and the screen, we show 'Screen'.
let stringId = "getUserMedia.sharing" + aState.screen;
- chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
- stringBundle.getString(stringId + ".message"),
- "webRTC-sharingScreen-notification-icon",
- mainAction, secondaryActions, options);
+ screenSharingNotif =
+ chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
+ stringBundle.getString(stringId + ".message"),
+ "webRTC-sharingScreen-notification-icon",
+ mainAction, secondaryActions, options);
}
function removeBrowserNotification(aBrowser, aNotificationId) {
let win = aBrowser.ownerDocument.defaultView;
let notification =
win.PopupNotifications.getNotification(aNotificationId, aBrowser);
if (notification)
win.PopupNotifications.remove(notification);
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -140,16 +140,17 @@ searchbar[oneoffui] .search-go-button:-m
.search-panel-input-value {
color: black;
}
.search-panel-one-offs {
margin: 0 0 !important;
border-top: 1px solid #ccc;
+ line-height: 0;
}
.searchbar-engine-one-off-item {
-moz-appearance: none;
display: inline-block;
border: none;
min-width: 48px;
height: 32px;
--- a/dom/bindings/test/test_exceptions_from_jsimplemented.html
+++ b/dom/bindings/test/test_exceptions_from_jsimplemented.html
@@ -7,30 +7,44 @@ https://bugzilla.mozilla.org/show_bug.cg
<meta charset="utf-8">
<title>Test for Bug 923010</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/** Test for Bug 923010 **/
try {
var conn = new mozRTCPeerConnection();
- var candidate = new mozRTCIceCandidate({candidate: null });
try {
- conn.addIceCandidate(candidate, function() {
- ok(false, "The call to addIceCandidate succeeded when it should have thrown");
+ conn.updateIce(candidate, function() {
+ ok(false, "The call to updateIce succeeded when it should have thrown");
}, function() {
- ok(false, "The call to addIceCandidate failed when it should have thrown");
+ ok(false, "The call to updateIce failed when it should have thrown");
})
- ok(false, "That call to addIceCandidate should have thrown");
+ ok(false, "That call to updateIce should have thrown");
} catch (e) {
- is(e.lineNumber, 17, "Exception should have been on line 17");
+ is(e.lineNumber, 16, "Exception should have been on line 16");
is(e.message,
- "Invalid candidate passed to addIceCandidate!",
+ "updateIce not yet implemented",
"Should have the exception we expect");
}
+
+ var candidate = new mozRTCIceCandidate({candidate: null });
+
+ conn.addIceCandidate(candidate)
+ .then(function() {
+ ok(false, "addIceCandidate succeeded when it should have failed");
+ }, function(reason) {
+ is(reason.lineNumber, 31, "Rejection should have been on line 31");
+ is(reason.message,
+ "Invalid candidate passed to addIceCandidate!",
+ "Should have the rejection we expect");
+ })
+ .catch(function(reason) {
+ ok(false, "unexpected error: " + reason);
+ });
} catch (e) {
// b2g has no WebRTC, apparently
todo(false, "No WebRTC on b2g yet");
}
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=923010">Mozilla Bug 923010</a>
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -337,17 +337,17 @@ RTCPeerConnection.prototype = {
if (!rtcConfig.iceServers ||
!Services.prefs.getBoolPref("media.peerconnection.use_document_iceservers")) {
rtcConfig.iceServers =
JSON.parse(Services.prefs.getCharPref("media.peerconnection.default_iceservers"));
}
this._mustValidateRTCConfiguration(rtcConfig,
"RTCPeerConnection constructor passed invalid RTCConfiguration");
if (_globalPCList._networkdown || !this._win.navigator.onLine) {
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidStateError",
"Can't create RTCPeerConnections when the network is down");
}
this.makeGetterSetterEH("onaddstream");
this.makeGetterSetterEH("onaddtrack");
this.makeGetterSetterEH("onicecandidate");
this.makeGetterSetterEH("onnegotiationneeded");
this.makeGetterSetterEH("onsignalingstatechange");
@@ -380,37 +380,22 @@ RTCPeerConnection.prototype = {
this._impl.initialize(this._observer, this._win, rtcConfig,
Services.tm.currentThread);
this._initIdp();
_globalPCList.notifyLifecycleObservers(this, "initialized");
},
get _impl() {
if (!this._pc) {
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidStateError",
"RTCPeerConnection is gone (did you enter Offline mode?)");
}
return this._pc;
},
- callCB: function(callback, arg) {
- if (callback) {
- this._win.setTimeout(() => {
- try {
- callback(arg);
- } catch(e) {
- // A content script (user-provided) callback threw an error. We don't
- // want this to take down peerconnection, but we still want the user
- // to see it, so we catch it, report it, and move on.
- this.logErrorAndCallOnError(e.message, e.fileName, e.lineNumber);
- }
- }, 0);
- }
- },
-
_initIdp: function() {
let prefName = "media.peerconnection.identity.timeout";
let idpTimeout = Services.prefs.getIntPref(prefName);
let warningFunc = this.logWarning.bind(this);
this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
this.dispatchEvent.bind(this));
this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
this.dispatchEvent.bind(this));
@@ -496,36 +481,36 @@ RTCPeerConnection.prototype = {
},
// Ideally, this should be of the form _checkState(state),
// where the state is taken from an enumeration containing
// the valid peer connection states defined in the WebRTC
// spec. See Bug 831756.
_checkClosed: function() {
if (this._closed) {
- throw new this._win.DOMError("", "Peer connection is closed");
+ throw new this._win.DOMError("InvalidStateError", "Peer connection is closed");
}
},
dispatchEvent: function(event) {
// PC can close while events are firing if there is an async dispatch
// in c++ land
if (!this._closed) {
this.__DOM_IMPL__.dispatchEvent(event);
}
},
// Log error message to web console and window.onerror, if present.
- logErrorAndCallOnError: function(msg, file, line) {
- this.logMsg(msg, file, line, Ci.nsIScriptError.exceptionFlag);
+ logErrorAndCallOnError: function(e) {
+ this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.exceptionFlag);
// Safely call onerror directly if present (necessary for testing)
try {
if (typeof this._win.onerror === "function") {
- this._win.onerror(msg, file, line);
+ this._win.onerror(e.message, e.fileName, e.lineNumber);
}
} catch(e) {
// If onerror itself throws, service it.
try {
this.logError(e.message, e.fileName, e.lineNumber);
} catch(e) {}
}
},
@@ -559,23 +544,40 @@ RTCPeerConnection.prototype = {
makeGetterSetterEH: function(name) {
Object.defineProperty(this, name,
{
get:function() { return this.getEH(name); },
set:function(h) { return this.setEH(name, h); }
});
},
- createOffer: function(onSuccess, onError, options) {
+ // Helper for legacy callbacks
+ thenCB: function(p, onSuccess, onError) {
+ var errorFunc = this.logErrorAndCallOnError.bind(this);
+
+ function callCB(func, arg) {
+ try {
+ func(arg);
+ } catch (e) {
+ errorFunc(e);
+ }
+ }
+ return onSuccess? p.then(result => callCB(onSuccess, result),
+ reason => (onError? callCB(onError, reason) : null)) : p;
+ },
+
+ createOffer: function(optionsOrOnSuccess, onError, options) {
// TODO: Remove old constraint-like RTCOptions support soon (Bug 1064223).
// Note that webidl bindings make o.mandatory implicit but not o.optional.
function convertLegacyOptions(o) {
- if (!(Object.keys(o.mandatory).length || o.optional) ||
- Object.keys(o).length != (o.optional? 2 : 1)) {
+ // Detect (mandatory OR optional) AND no other top-level members.
+ let lcy = ((o.mandatory && Object.keys(o.mandatory).length) || o.optional) &&
+ Object.keys(o).length == (o.mandatory? 1 : 0) + (o.optional? 1 : 0);
+ if (!lcy) {
return false;
}
let old = o.mandatory || {};
if (o.mandatory) {
delete o.mandatory;
}
if (o.optional) {
o.optional.forEach(one => {
@@ -595,27 +597,35 @@ RTCPeerConnection.prototype = {
Object.keys(o).forEach(k => {
if (o[k] === undefined) {
delete o[k];
}
});
return true;
}
+ let onSuccess;
+ if (optionsOrOnSuccess && typeof optionsOrOnSuccess === "function") {
+ onSuccess = optionsOrOnSuccess;
+ } else {
+ options = optionsOrOnSuccess;
+ onError = undefined;
+ }
if (options && convertLegacyOptions(options)) {
this.logWarning(
"Mandatory/optional in createOffer options is deprecated! Use " +
JSON.stringify(options) + " instead (note the case difference)!",
null, 0);
}
- this._queueOrRun({
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._createOffer,
- args: [onSuccess, onError, options],
+ args: [resolve, reject, options],
wait: true
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
_createOffer: function(onSuccess, onError, options) {
this._onCreateOfferSuccess = onSuccess;
this._onCreateOfferFailure = onError;
this._impl.createOffer(options);
},
@@ -635,88 +645,83 @@ RTCPeerConnection.prototype = {
this._observer.onCreateAnswerError(Ci.IPeerConnection.kInvalidState,
"No outstanding offer");
return;
}
this._impl.createAnswer();
},
createAnswer: function(onSuccess, onError) {
- this._queueOrRun({
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._createAnswer,
- args: [onSuccess, onError],
+ args: [resolve, reject],
wait: true
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
setLocalDescription: function(desc, onSuccess, onError) {
- if (!onSuccess || !onError) {
- this.logWarning(
- "setLocalDescription called without success/failure callbacks. This is deprecated, and will be an error in the future.",
- null, 0);
- }
-
this._localType = desc.type;
let type;
switch (desc.type) {
case "offer":
type = Ci.IPeerConnection.kActionOffer;
break;
case "answer":
type = Ci.IPeerConnection.kActionAnswer;
break;
case "pranswer":
- throw new this._win.DOMError("", "pranswer not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "pranswer not yet implemented");
default:
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidParameterError",
"Invalid type " + desc.type + " provided to setLocalDescription");
}
- this._queueOrRun({
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._setLocalDescription,
- args: [type, desc.sdp, onSuccess, onError],
+ args: [type, desc.sdp, resolve, reject],
wait: true
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
_setLocalDescription: function(type, sdp, onSuccess, onError) {
this._onSetLocalDescriptionSuccess = onSuccess;
this._onSetLocalDescriptionFailure = onError;
this._impl.setLocalDescription(type, sdp);
},
setRemoteDescription: function(desc, onSuccess, onError) {
- if (!onSuccess || !onError) {
- this.logWarning(
- "setRemoteDescription called without success/failure callbacks. This is deprecated, and will be an error in the future.",
- null, 0);
- }
this._remoteType = desc.type;
let type;
switch (desc.type) {
case "offer":
type = Ci.IPeerConnection.kActionOffer;
break;
case "answer":
type = Ci.IPeerConnection.kActionAnswer;
break;
case "pranswer":
- throw new this._win.DOMError("", "pranswer not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "pranswer not yet implemented");
default:
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidParameterError",
"Invalid type " + desc.type + " provided to setRemoteDescription");
}
- this._queueOrRun({
+ // Have to get caller's origin outside of Promise constructor and pass it in
+ let origin = Cu.getWebIDLCallerPrincipal().origin;
+
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._setRemoteDescription,
- args: [type, desc.sdp, onSuccess, onError],
+ args: [type, desc.sdp, origin, resolve, reject],
wait: true
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
/**
* Takes a result from the IdP and checks it against expectations.
* If OK, generates events.
* Returns true if it is either present and valid, or if there is no
* need for identity.
*/
@@ -731,32 +736,32 @@ RTCPeerConnection.prototype = {
this._impl.peerIdentity = message.identity;
this._peerIdentity = new this._win.RTCIdentityAssertion(
this._remoteIdp.provider, message.identity);
this.dispatchEvent(new this._win.Event("peeridentity"));
}
return good;
},
- _setRemoteDescription: function(type, sdp, onSuccess, onError) {
+ _setRemoteDescription: function(type, sdp, origin, onSuccess, onError) {
let idpComplete = false;
let setRemoteComplete = false;
let idpError = null;
let isDone = false;
// we can run the IdP validation in parallel with setRemoteDescription this
// complicates much more than would be ideal, but it ensures that the IdP
// doesn't hold things up too much when it's not on the critical path
let allDone = () => {
if (!setRemoteComplete || !idpComplete || isDone) {
return;
}
// May be null if the user didn't supply success/failure callbacks.
// Violation of spec, but we allow it for now
- this.callCB(onSuccess);
+ onSuccess();
isDone = true;
this._executeNext();
};
let setRemoteDone = () => {
setRemoteComplete = true;
allDone();
};
@@ -770,27 +775,27 @@ RTCPeerConnection.prototype = {
} else {
idpDone = message => {
let idpGood = this._processIdpResult(message);
if (!idpGood) {
// iff we are waiting for a very specific peerIdentity
// call the error callback directly and then close
idpError = "Peer Identity mismatch, expected: " +
this._impl.peerIdentity;
- this.callCB(onError, idpError);
+ onError(idpError);
this.close();
} else {
idpComplete = true;
allDone();
}
};
}
try {
- this._remoteIdp.verifyIdentityFromSDP(sdp, idpDone);
+ this._remoteIdp.verifyIdentityFromSDP(sdp, origin, idpDone);
} catch (e) {
// if processing the SDP for identity doesn't work
this.logWarning(e.message, e.fileName, e.lineNumber);
idpDone(null);
}
this._onSetRemoteDescriptionSuccess = setRemoteDone;
this._onSetRemoteDescriptionFailure = onError;
@@ -817,35 +822,31 @@ RTCPeerConnection.prototype = {
}
};
this._localIdp.getIdentityAssertion(this._impl.fingerprint,
gotAssertion);
},
updateIce: function(config) {
- throw new this._win.DOMError("", "updateIce not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "updateIce not yet implemented");
},
addIceCandidate: function(cand, onSuccess, onError) {
- if (!onSuccess || !onError) {
- this.logWarning(
- "addIceCandidate called without success/failure callbacks. This is deprecated, and will be an error in the future.",
- null, 0);
- }
if (!cand.candidate && !cand.sdpMLineIndex) {
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidParameterError",
"Invalid candidate passed to addIceCandidate!");
}
- this._queueOrRun({
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._addIceCandidate,
- args: [cand, onSuccess, onError],
+ args: [cand, resolve, reject],
wait: false
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
_addIceCandidate: function(cand, onSuccess, onError) {
this._onAddIceCandidateSuccess = onSuccess || null;
this._onAddIceCandidateError = onError || null;
this._impl.addIceCandidate(cand.candidate, cand.sdpMid || "",
(cand.sdpMLineIndex === null) ? 0 :
@@ -853,42 +854,42 @@ RTCPeerConnection.prototype = {
},
addStream: function(stream) {
stream.getTracks().forEach(track => this.addTrack(track, stream));
},
removeStream: function(stream) {
// Bug 844295: Not implementing this functionality.
- throw new this._win.DOMError("", "removeStream not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "removeStream not yet implemented");
},
getStreamById: function(id) {
- throw new this._win.DOMError("", "getStreamById not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "getStreamById not yet implemented");
},
addTrack: function(track, stream) {
if (stream.currentTime === undefined) {
- throw new this._win.DOMError("", "invalid stream.");
+ throw new this._win.DOMError("InvalidParameterError", "invalid stream.");
}
if (stream.getTracks().indexOf(track) == -1) {
- throw new this._win.DOMError("", "track is not in stream.");
+ throw new this._win.DOMError("InvalidParameterError", "track is not in stream.");
}
this._checkClosed();
this._impl.addTrack(track, stream);
let sender = this._win.RTCRtpSender._create(this._win,
new RTCRtpSender(this, track,
stream));
this._senders.push({ sender: sender, stream: stream });
return sender;
},
removeTrack: function(sender) {
// Bug 844295: Not implementing this functionality.
- throw new this._win.DOMError("", "removeTrack not yet implemented");
+ throw new this._win.DOMError("NotSupportedError", "removeTrack not yet implemented");
},
_replaceTrack: function(sender, withTrack, onSuccess, onError) {
// TODO: Do a (sender._stream.getTracks().indexOf(track) == -1) check
// on both track args someday.
//
// The proposed API will be that both tracks must already be in the same
// stream. However, since our MediaStreams currently are limited to one
@@ -1012,21 +1013,22 @@ RTCPeerConnection.prototype = {
changeIceConnectionState: function(state) {
this._iceConnectionState = state;
_globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange");
this.dispatchEvent(new this._win.Event("iceconnectionstatechange"));
},
getStats: function(selector, onSuccess, onError) {
- this._queueOrRun({
+ let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
func: this._getStats,
- args: [selector, onSuccess, onError],
+ args: [selector, resolve, reject],
wait: false
- });
+ }));
+ return this.thenCB(p, onSuccess, onError);
},
_getStats: function(selector, onSuccess, onError) {
this._onGetStatsSuccess = onSuccess;
this._onGetStatsFailure = onError;
this._impl.getStats(selector);
},
@@ -1051,17 +1053,17 @@ RTCPeerConnection.prototype = {
}
if (dict.stream != undefined) {
dict.id = dict.stream;
this.logWarning("Deprecated RTCDataChannelInit dictionary entry stream used!", null, 0);
}
if (dict.maxRetransmitTime != undefined &&
dict.maxRetransmits != undefined) {
- throw new this._win.DOMError("",
+ throw new this._win.DOMError("InvalidParameterError",
"Both maxRetransmitTime and maxRetransmits cannot be provided");
}
let protocol;
if (dict.protocol == undefined) {
protocol = "";
} else {
protocol = dict.protocol;
}
@@ -1137,84 +1139,80 @@ PeerConnectionObserver.prototype = {
dispatchEvent: function(event) {
this._dompc.dispatchEvent(event);
},
onCreateOfferSuccess: function(sdp) {
let pc = this._dompc;
let fp = pc._impl.fingerprint;
- pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+ let origin = Cu.getWebIDLCallerPrincipal().origin;
+ pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
if (assertion) {
pc._gotIdentityAssertion(assertion);
}
- pc.callCB(pc._onCreateOfferSuccess,
- new pc._win.mozRTCSessionDescription({ type: "offer",
- sdp: sdp }));
+ pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
+ sdp: sdp }));
pc._executeNext();
}.bind(this));
},
onCreateOfferError: function(code, message) {
- this._dompc.callCB(this._dompc._onCreateOfferFailure, this.newError(code, message));
+ this._dompc._onCreateOfferFailure(this.newError(code, message));
this._dompc._executeNext();
},
onCreateAnswerSuccess: function(sdp) {
let pc = this._dompc;
let fp = pc._impl.fingerprint;
- pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+ let origin = Cu.getWebIDLCallerPrincipal().origin;
+ pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
if (assertion) {
pc._gotIdentityAssertion(assertion);
}
- pc.callCB(pc._onCreateAnswerSuccess,
- new pc._win.mozRTCSessionDescription({ type: "answer",
- sdp: sdp }));
+ pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
+ sdp: sdp }));
pc._executeNext();
}.bind(this));
},
onCreateAnswerError: function(code, message) {
- this._dompc.callCB(this._dompc._onCreateAnswerFailure,
- this.newError(code, message));
+ this._dompc._onCreateAnswerFailure(this.newError(code, message));
this._dompc._executeNext();
},
onSetLocalDescriptionSuccess: function() {
- this._dompc.callCB(this._dompc._onSetLocalDescriptionSuccess);
+ this._dompc._onSetLocalDescriptionSuccess();
this._dompc._executeNext();
},
onSetRemoteDescriptionSuccess: function() {
// This function calls _executeNext() for us
this._dompc._onSetRemoteDescriptionSuccess();
},
onSetLocalDescriptionError: function(code, message) {
this._localType = null;
- this._dompc.callCB(this._dompc._onSetLocalDescriptionFailure,
- this.newError(code, message));
+ this._dompc._onSetLocalDescriptionFailure(this.newError(code, message));
this._dompc._executeNext();
},
onSetRemoteDescriptionError: function(code, message) {
this._remoteType = null;
- this._dompc.callCB(this._dompc._onSetRemoteDescriptionFailure,
- this.newError(code, message));
+ this._dompc._onSetRemoteDescriptionFailure(this.newError(code, message));
this._dompc._executeNext();
},
onAddIceCandidateSuccess: function() {
- this._dompc.callCB(this._dompc._onAddIceCandidateSuccess);
+ this._dompc._onAddIceCandidateSuccess();
this._dompc._executeNext();
},
onAddIceCandidateError: function(code, message) {
- this._dompc.callCB(this._dompc._onAddIceCandidateError,
- this.newError(code, message));
+ this._dompc._onAddIceCandidateError(this.newError(code, message));
this._dompc._executeNext();
},
onIceCandidate: function(level, mid, candidate) {
if (candidate == "") {
this.foundIceCandidate(null);
} else {
this.foundIceCandidate(new this._dompc._win.mozRTCIceCandidate(
@@ -1329,61 +1327,60 @@ PeerConnectionObserver.prototype = {
}
},
onGetStatsSuccess: function(dict) {
let chromeobj = new RTCStatsReport(this._dompc._win, dict);
let webidlobj = this._dompc._win.RTCStatsReport._create(this._dompc._win,
chromeobj);
chromeobj.makeStatsPublic();
- this._dompc.callCB(this._dompc._onGetStatsSuccess, webidlobj);
+ this._dompc._onGetStatsSuccess(webidlobj);
this._dompc._executeNext();
},
onGetStatsError: function(code, message) {
- this._dompc.callCB(this._dompc._onGetStatsFailure,
- this.newError(code, message));
+ this._dompc._onGetStatsFailure(this.newError(code, message));
this._dompc._executeNext();
},
onAddStream: function(stream) {
let ev = new this._dompc._win.MediaStreamEvent("addstream",
{ stream: stream });
- this._dompc.dispatchEvent(ev);
+ this.dispatchEvent(ev);
},
onRemoveStream: function(stream, type) {
this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
{ stream: stream }));
},
onAddTrack: function(track) {
let ev = new this._dompc._win.MediaStreamTrackEvent("addtrack",
{ track: track });
- this._dompc.dispatchEvent(ev);
+ this.dispatchEvent(ev);
},
onRemoveTrack: function(track, type) {
this.dispatchEvent(new this._dompc._win.MediaStreamTrackEvent("removetrack",
{ track: track }));
},
onReplaceTrackSuccess: function() {
var pc = this._dompc;
pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
pc._onReplaceTrackWithTrack = null;
pc._onReplaceTrackSender = null;
- pc.callCB(pc._onReplaceTrackSuccess);
+ pc._onReplaceTrackSuccess();
},
onReplaceTrackError: function(code, message) {
var pc = this._dompc;
pc._onReplaceTrackWithTrack = null;
pc._onReplaceTrackSender = null;
- pc.callCB(pc._onReplaceTrackError, this.newError(code, message));
+ pc._onReplaceTrackError(this.newError(code, message));
},
foundIceCandidate: function(cand) {
this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate",
{ candidate: cand } ));
},
notifyDataChannel: function(channel) {
@@ -1418,23 +1415,24 @@ function RTCRtpSender(pc, track, stream)
this._stream = stream;
}
RTCRtpSender.prototype = {
classDescription: "RTCRtpSender",
classID: PC_SENDER_CID,
contractID: PC_SENDER_CONTRACT,
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
- replaceTrack: function(withTrack, onSuccess, onError) {
+ replaceTrack: function(withTrack) {
this._pc._checkClosed();
- this._pc._queueOrRun({
+
+ return new this._pc._win.Promise((resolve, reject) => this._pc._queueOrRun({
func: this._pc._replaceTrack,
- args: [this, withTrack, onSuccess, onError],
+ args: [this, withTrack, resolve, reject],
wait: false
- });
+ }));
}
};
function RTCRtpReceiver(pc, track) {
this.pc = pc;
this.track = track;
}
RTCRtpReceiver.prototype = {
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -119,28 +119,28 @@ PeerConnectionIdp.prototype = {
},
/**
* Queues a task to verify the a=identity line the given SDP contains, if any.
* If the verification succeeds callback is called with the message from the
* IdP proxy as parameter, else (verification failed OR no a=identity line in
* SDP at all) null is passed to callback.
*/
- verifyIdentityFromSDP: function(sdp, callback) {
+ verifyIdentityFromSDP: function(sdp, origin, callback) {
let identity = this._getIdentityFromSdp(sdp);
let fingerprints = this._getFingerprintsFromSdp(sdp);
// it's safe to use the fingerprint we got from the SDP here,
// only because we ensure that there is only one
if (!identity || fingerprints.length <= 0) {
callback(null);
return;
}
this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
- this._verifyIdentity(identity.assertion, fingerprints, callback);
+ this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
},
/**
* Checks that the name in the identity provided by the IdP is OK.
*
* @param name (string) the name to validate
* @returns (string) an error message, iff the name isn't good
*/
@@ -207,50 +207,51 @@ PeerConnectionIdp.prototype = {
warn("invalid JSON in content");
}
return false;
},
/**
* Asks the IdP proxy to verify an identity.
*/
- _verifyIdentity: function(assertion, fingerprints, callback) {
+ _verifyIdentity: function(assertion, fingerprints, origin, callback) {
function onVerification(message) {
if (message && this._checkVerifyResponse(message, fingerprints)) {
callback(message);
} else {
this._warning("RTC identity: assertion validation failure", null, 0);
callback(null);
}
}
let request = {
type: "VERIFY",
- message: assertion
+ message: assertion,
+ origin: origin
};
this._sendToIdp(request, "validation", onVerification.bind(this));
},
/**
* Asks the IdP proxy for an identity assertion and, on success, enriches the
* given SDP with an a=identity line and calls callback with the new SDP as
* parameter. If no IdP is configured the original SDP (without a=identity
* line) is passed to the callback.
*/
- appendIdentityToSDP: function(sdp, fingerprint, callback) {
+ appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
let onAssertion = function() {
callback(this.wrapSdp(sdp), this.assertion);
}.bind(this);
if (!this._idpchannel || this.assertion) {
onAssertion();
return;
}
- this._getIdentityAssertion(fingerprint, onAssertion);
+ this._getIdentityAssertion(fingerprint, origin, onAssertion);
},
/**
* Inserts an identity assertion into the given SDP.
*/
wrapSdp: function(sdp) {
if (!this.assertion) {
return sdp;
@@ -265,31 +266,33 @@ PeerConnectionIdp.prototype = {
getIdentityAssertion: function(fingerprint, callback) {
if (!this._idpchannel) {
this.reportError("assertion", "IdP not set");
callback(null);
return;
}
- this._getIdentityAssertion(fingerprint, callback);
+ let origin = Cu.getWebIDLCallerPrincipal().origin;
+ this._getIdentityAssertion(fingerprint, origin, callback);
},
- _getIdentityAssertion: function(fingerprint, callback) {
+ _getIdentityAssertion: function(fingerprint, origin, callback) {
let [algorithm, digest] = fingerprint.split(" ", 2);
let message = {
fingerprint: [{
algorithm: algorithm,
digest: digest
}]
};
let request = {
type: "SIGN",
message: JSON.stringify(message),
- username: this.username
+ username: this.username,
+ origin: origin
};
// catch the assertion, clean it up, warn if absent
function trapAssertion(assertion) {
if (!assertion) {
this._warning("RTC identity: assertion generation failure", null, 0);
this.assertion = null;
} else {
@@ -303,17 +306,16 @@ PeerConnectionIdp.prototype = {
/**
* Packages a message and sends it to the IdP.
* @param request (dictionary) the message to send
* @param type (DOMString) the type of message (assertion/validation)
* @param callback (function) the function to call with the results
*/
_sendToIdp: function(request, type, callback) {
- request.origin = Cu.getWebIDLCallerPrincipal().origin;
this._idpchannel.send(request, this._wrapCallback(type, callback));
},
_reportIdpError: function(type, message) {
let args = {};
let msg = "";
if (message.type === "ERROR") {
msg = message.error;
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -78,18 +78,16 @@ skip-if = buildapp == 'b2g' || os == 'an
[test_peerConnection_bug822674.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
[test_peerConnection_bug825703.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
[test_peerConnection_bug827843.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_bug834153.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
-[test_peerConnection_bug835370.html]
-skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
[test_peerConnection_bug1013809.html]
skip-if = toolkit == 'gonk' # b2g emulator seems to be too slow (Bug 1016498 and 1008080)
[test_peerConnection_bug1042791.html]
skip-if = buildapp == 'b2g' || os == 'android' # bug 1043403
[test_peerConnection_capturedVideo.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_close.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
@@ -102,16 +100,18 @@ skip-if = toolkit == 'gonk' # b2g (Bug 1
[test_peerConnection_noTrickleOfferAnswer.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
[test_peerConnection_offerRequiresReceiveAudio.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_offerRequiresReceiveVideo.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_offerRequiresReceiveVideoAudio.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
+[test_peerConnection_promiseSendOnly.html]
+skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_replaceTrack.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_syncSetDescription.html]
skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
[test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
[test_peerConnection_setLocalAnswerInStable.html]
skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
--- a/dom/media/tests/mochitest/test_dataChannel_noOffer.html
+++ b/dom/media/tests/mochitest/test_dataChannel_noOffer.html
@@ -15,20 +15,21 @@
});
runNetworkTest(function () {
var pc = new mozRTCPeerConnection();
// necessary to circumvent bug 864109
var options = { offerToReceiveAudio: true };
- pc.createOffer(function (offer) {
+ pc.createOffer(options).then(offer => {
ok(!offer.sdp.contains("m=application"),
"m=application is not contained in the SDP");
networkTestFinished();
- }, generateErrorCallback(), options);
+ })
+ .catch(generateErrorCallback());
});
</script>
</pre>
</body>
</html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug834153.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug834153.html
@@ -9,41 +9,32 @@
<body>
<pre id="test">
<script type="application/javascript">
createHTML({
bug: "834153",
title: "Queue CreateAnswer in PeerConnection.js"
});
- function croak(msg) {
- ok(0, msg);
- pc1.close();
- pc2.close();
- networkTestFinished();
- }
-
runNetworkTest(function () {
var pc1 = new mozRTCPeerConnection();
+ var pc2 = new mozRTCPeerConnection();
- pc1.createOffer(function (d) {
- var pc2 = new mozRTCPeerConnection();
-
+ pc1.createOffer({ offerToReceiveAudio: true }).then(offer => {
// The whole point of this test is not to wait for the
// setRemoteDescription call to succesfully complete, so we
- // don't do anything in its callbacks.
- pc2.setRemoteDescription(d, function (x) {}, function (x) {});
- pc2.createAnswer(function (d) {
- is(d.type,"answer","CreateAnswer created an answer");
- pc1.close();
- pc2.close();
- networkTestFinished();
- }, function (err) {
- croak("createAnswer failed: " + err);
- });
- }, function (err) {
- croak("createOffer failed: " + err);
- }, { offerToReceiveAudio: true });
+ // don't wait for it to succeed.
+ pc2.setRemoteDescription(offer);
+ return pc2.createAnswer();
+ })
+ .then(answer => is(answer.type, "answer", "CreateAnswer created an answer"))
+ .catch(reason => ok(false, reason.message))
+ .then(() => {
+ pc1.close();
+ pc2.close();
+ networkTestFinished();
+ })
+ .catch(reason => ok(false, reason.message));
});
</script>
</pre>
</body>
</html>
deleted file mode 100644
--- a/dom/media/tests/mochitest/test_peerConnection_bug835370.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
- <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
- <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
- <script type="application/javascript" src="head.js"></script>
- <script type="application/javascript" src="pc.js"></script>
-</head>
-<body>
-<pre id="test">
-<script type="application/javascript">
- createHTML({
- bug: "835370",
- title: "PeerConnection.createOffer valid/invalid constraints permutations"
- });
-
- runNetworkTest(function () {
- var pconnect = new mozRTCPeerConnection();
- var pconnects = new mozRTCPeerConnection();
-
- function step1(offer) {}
- function failed(code) {}
-
- var exception = null;
- try { pconnects.createOffer(step1, failed); } catch (e) { exception = e; }
- ok(!exception, "createOffer(step1, failed) succeeds");
- exception = null;
- try { pconnect.createOffer(step1, failed, 1); } catch (e) { exception = e; }
- ok(exception, "createOffer(step1, failed, 1) throws");
- exception = null;
- try { pconnects.createOffer(step1, failed, {}); } catch (e) { exception = e; }
- ok(!exception, "createOffer(step1, failed, {}) succeeds");
- exception = null;
- try {
- pconnect.updateIce();
- } catch (e) {
- ok(e.message.indexOf("updateIce") >= 0, "PeerConnection.js has readable exceptions");
- exception = e;
- }
- ok(exception, "updateIce not yet implemented and throws");
- exception = null;
- try { pconnects.createOffer(step1, failed, { offerToReceiveVideo: false, offerToReceiveAudio: true, MozDontOfferDataChannel: true }); } catch (e) { exception = e; }
- ok(!exception, "createOffer(step1, failed, { offerToReceiveVideo: false, offerToReceiveAudio: true, MozDontOfferDataChannel: true }) succeeds");
- pconnect.close();
- pconnects.close();
- pconnect = null;
- pconnects = null;
- networkTestFinished();
- });
-</script>
-</pre>
-</body>
-</html>
--- a/dom/media/tests/mochitest/test_peerConnection_close.html
+++ b/dom/media/tests/mochitest/test_peerConnection_close.html
@@ -20,49 +20,82 @@
var eTimeout = null;
// everything should be in initial state
is(pc.signalingState, "stable", "Initial signalingState is 'stable'");
is(pc.iceConnectionState, "new", "Initial iceConnectionState is 'new'");
is(pc.iceGatheringState, "new", "Initial iceGatheringState is 'new'");
var finish;
- var finished = new Promise(function(resolve) {
- finish = resolve;
- });
+ var finished = new Promise(resolve => finish = resolve);
pc.onsignalingstatechange = function(e) {
clearTimeout(eTimeout);
is(pc.signalingState, "closed", "signalingState is 'closed'");
is(pc.iceConnectionState, "closed", "iceConnectionState is 'closed'");
try {
pc.close();
} catch (e) {
exception = e;
}
is(exception, null, "A second close() should not raise an exception");
is(pc.signalingState, "closed", "Final signalingState stays at 'closed'");
is(pc.iceConnectionState, "closed", "Final iceConnectionState stays at 'closed'");
- // TODO: according to the webrtc spec all of these should throw InvalidStateError's
- // instead they seem to throw simple Error's
- SimpleTest.doesThrow(function() {
- pc.setLocalDescription(
- "Invalid Session Description",
- function() {},
- function() {})},
- "setLocalDescription() on closed PC raised expected exception");
+ // Due to a limitation in our WebIDL compiler that prevents overloads with
+ // both Promise and non-Promise return types, legacy APIs with callbacks
+ // are unable to continue to throw exceptions. Luckily the spec uses
+ // exceptions solely for "programming errors" so this should not hinder
+ // working code from working, which is the point of the legacy API. All
+ // new code should use the promise API.
+ //
+ // The legacy methods that no longer throw on programming errors like
+ // "invalid-on-close" are:
+ // - createOffer
+ // - createAnswer
+ // - setLocalDescription
+ // - setRemoteDescription
+ // - addIceCandidate
+ // - getStats
+ //
+ // These legacy methods fire the error callback instead. This is not
+ // entirely to spec but is better than ignoring programming errors.
+
+ var offer = new mozRTCSessionDescription({ sdp: "sdp", type: "offer" });
+ var answer = new mozRTCSessionDescription({ sdp: "sdp", type: "answer" });
+ var candidate = new mozRTCIceCandidate({ candidate: "dummy",
+ sdpMid: "test",
+ sdpMLineIndex: 3 });
- SimpleTest.doesThrow(function() {
- pc.setRemoteDescription(
- "Invalid Session Description",
- function() {},
- function() {})},
- "setRemoteDescription() on closed PC raised expected exception");
+ var doesFail = (p, msg) => p.then(generateErrorCallback(),
+ r => is(r.name, "InvalidStateError", msg));
+
+ doesFail(pc.createOffer(), "createOffer fails on close")
+ .then(() => doesFail(pc.createAnswer(), "createAnswer fails on close"))
+ .then(() => doesFail(pc.setLocalDescription(offer),
+ "setLocalDescription fails on close"))
+ .then(() => doesFail(pc.setRemoteDescription(answer),
+ "setRemoteDescription fails on close"))
+ .then(() => doesFail(pc.addIceCandidate(candidate),
+ "addIceCandidate fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.createOffer(y, n)),
+ "Legacy createOffer fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.createAnswer(y, n)),
+ "Legacy createAnswer fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.setLocalDescription(offer, y, n)),
+ "Legacy setLocalDescription fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.setRemoteDescription(answer, y, n)),
+ "Legacy setRemoteDescription fails on close"))
+ .then(() => doesFail(new Promise((y, n) => pc.addIceCandidate(candidate, y, n)),
+ "Legacy addIceCandidate fails on close"))
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(finish);
+
+ // Other methods are unaffected.
SimpleTest.doesThrow(function() {
pc.updateIce("Invalid RTC Configuration")},
"updateIce() on closed PC raised expected exception");
SimpleTest.doesThrow(function() {
pc.addStream("Invalid Media Stream")},
"addStream() on closed PC raised expected exception");
@@ -70,27 +103,20 @@
SimpleTest.doesThrow(function() {
pc.removeStream("Invalid Media Stream")},
"removeStream() on closed PC raised expected exception");
SimpleTest.doesThrow(function() {
pc.createDataChannel({})},
"createDataChannel() on closed PC raised expected exception");
- // The spec says it has to throw, but it seems questionable why...
- SimpleTest.doesThrow(function() {
- pc.getStats()},
- "getStats() on closed PC raised expected exception");
-
SimpleTest.doesThrow(function() {
pc.setIdentityProvider("Invalid Provider")},
"setIdentityProvider() on closed PC raised expected exception");
-
- finish();
- }
+ };
// This prevents a mochitest timeout in case the event does not fire
eTimeout = setTimeout(function() {
ok(false, "Failed to receive expected onsignalingstatechange event in 60s");
finish();
}, 60000);
try {
--- a/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
+++ b/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
@@ -9,62 +9,65 @@
<body>
<pre id="test">
<script type="application/javascript">
createHTML({
bug: "834270",
title: "Align PeerConnection error handling with WebRTC specification"
});
- function errorCallback(nextStep) {
- return function (err) {
- ok(err, "Error is set");
- ok(err.name && err.name.length, "Error name = " + err.name);
- ok(err.message && err.message.length, "Error message = " + err.message);
- nextStep();
- }
+ function validateReason(reason) {
+ ok(reason.name.length, "Reason name = " + reason.name);
+ ok(reason.message.length, "Reason message = " + reason.message);
};
function testCreateAnswerError() {
var pc = new mozRTCPeerConnection();
- info ("Testing createAnswer error callback");
- pc.createAnswer(generateErrorCallback("createAnswer before offer should fail"),
- errorCallback(testSetLocalDescriptionError));
+ info ("Testing createAnswer error");
+ return pc.createAnswer()
+ .then(generateErrorCallback("createAnswer before offer should fail"),
+ validateReason);
};
function testSetLocalDescriptionError() {
var pc = new mozRTCPeerConnection();
- info ("Testing setLocalDescription error callback");
- pc.setLocalDescription(new mozRTCSessionDescription({ sdp: "Picklechips!",
- type: "offer" }),
- generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
- errorCallback(testSetRemoteDescriptionError));
+ info ("Testing setLocalDescription error");
+ return pc.setLocalDescription(new mozRTCSessionDescription({ sdp: "Picklechips!",
+ type: "offer" }))
+ .then(generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
+ validateReason);
};
function testSetRemoteDescriptionError() {
var pc = new mozRTCPeerConnection();
- info ("Testing setRemoteDescription error callback");
- pc.setRemoteDescription(new mozRTCSessionDescription({ sdp: "Who?",
- type: "offer" }),
- generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
- errorCallback(testAddIceCandidateError));
+ info ("Testing setRemoteDescription error");
+ return pc.setRemoteDescription(new mozRTCSessionDescription({ sdp: "Who?",
+ type: "offer" }))
+ .then(generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
+ validateReason);
};
function testAddIceCandidateError() {
var pc = new mozRTCPeerConnection();
- info ("Testing addIceCandidate error callback");
- pc.addIceCandidate(new mozRTCIceCandidate({ candidate: "Pony Lords, jump!",
- sdpMid: "whee",
- sdpMLineIndex: 1 }),
- generateErrorCallback("addIceCandidate with nonsense candidate should fail"),
- errorCallback(networkTestFinished));
+ info ("Testing addIceCandidate error");
+ return pc.addIceCandidate(new mozRTCIceCandidate({ candidate: "Pony Lords, jump!",
+ sdpMid: "whee",
+ sdpMLineIndex: 1 }))
+ .then(generateErrorCallback("addIceCandidate with nonsense candidate should fail"),
+ validateReason);
};
// No test for createOffer errors -- there's nothing we can do at this
// level to evoke an error in createOffer.
runNetworkTest(function () {
- testCreateAnswerError();
+ testCreateAnswerError()
+ .then(testSetLocalDescriptionError)
+ .then(testSetRemoteDescriptionError)
+ .then(testAddIceCandidateError)
+ .catch(reason => ok(false, "unexpected error: " + reason))
+ .then(networkTestFinished);
});
+
</script>
</pre>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="head.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<video id="v1" controls="controls" height="120" width="160" autoplay></video>
+<video id="v2" controls="controls" height="120" width="160" autoplay></video><br>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+ createHTML({
+ bug: "1091898",
+ title: "PeerConnection with promises (sendonly)",
+ visible: true
+ });
+
+ var waituntil = func => new Promise(resolve => {
+ var inter = setInterval(() => func() && resolve(clearInterval(inter)), 200);
+ });
+
+ var pc1 = new mozRTCPeerConnection();
+ var pc2 = new mozRTCPeerConnection();
+
+ var pc2_haveRemoteOffer = new Promise(resolve => pc2.onsignalingstatechange =
+ e => (e.target.signalingState == "have-remote-offer") && resolve());
+ var pc1_stable = new Promise(resolve => pc1.onsignalingstatechange =
+ e => (e.target.signalingState == "stable") && resolve());
+
+ pc1.onicecandidate = e => pc2_haveRemoteOffer.then(() => !e.candidate ||
+ pc2.addIceCandidate(e.candidate)).catch(generateErrorCallback());
+ pc2.onicecandidate = e => pc1_stable.then(() => !e.candidate ||
+ pc1.addIceCandidate(e.candidate)).catch(generateErrorCallback());
+
+ var delivered = new Promise(resolve =>
+ pc2.onaddstream = e => resolve(v2.mozSrcObject = e.stream));
+ var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve());
+
+ runNetworkTest(function() {
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ navigator.mediaDevices.getUserMedia({ fake: true, video: true, audio: true })
+ .then(stream => pc1.addStream(v1.mozSrcObject = stream))
+ .then(() => pc1.createOffer())
+ .then(offer => pc1.setLocalDescription(offer))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer())
+ .then(answer => pc2.setLocalDescription(answer))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .then(() => delivered)
+// .then(() => canPlayThrough) // why doesn't this fire?
+ .then(() => waituntil(() => v2.currentTime > 0 && v2.mozSrcObject.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."))
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(networkTestFinished);
+ });
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -32,27 +32,23 @@
var flowtest = test.chain.remove("PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT");
test.chain.append(flowtest);
test.chain.append([["PC_LOCAL_REPLACE_VIDEOTRACK",
function (test) {
var stream = test.pcLocal._pc.getLocalStreams()[0];
var track = stream.getVideoTracks()[0];
var sender = test.pcLocal._pc.getSenders().find(isSenderOfTrack, track);
ok(sender, "track has a sender");
+ var newtrack;
navigator.mediaDevices.getUserMedia({video:true, fake: true})
.then(function(newStream) {
- var newtrack = newStream.getVideoTracks()[0];
- return new Promise(function(resolve, reject) {
- sender.replaceTrack(newtrack, function() {
- resolve(newtrack);
- }, reject);
- });
+ newtrack = newStream.getVideoTracks()[0];
+ return sender.replaceTrack(newtrack);
})
- .then(function(newtrack) {
- ok(true, "replaceTrack success callback is called");
+ .then(function() {
is(sender.track, newtrack, "sender.track has been replaced");
})
.catch(function(reason) {
ok(false, "unexpected error = " + reason.message);
})
.then(test.next.bind(test));
}
]]);
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -79,34 +79,25 @@ interface RTCDataChannel;
// moz-prefixed until sufficiently standardized.
interface mozRTCPeerConnection : EventTarget {
[Pref="media.peerconnection.identity.enabled"]
void setIdentityProvider (DOMString provider,
optional DOMString protocol,
optional DOMString username);
[Pref="media.peerconnection.identity.enabled"]
void getIdentityAssertion();
- void createOffer (RTCSessionDescriptionCallback successCallback,
- RTCPeerConnectionErrorCallback failureCallback,
- optional RTCOfferOptions options);
- void createAnswer (RTCSessionDescriptionCallback successCallback,
- RTCPeerConnectionErrorCallback failureCallback);
- void setLocalDescription (mozRTCSessionDescription description,
- optional VoidFunction successCallback,
- optional RTCPeerConnectionErrorCallback failureCallback);
- void setRemoteDescription (mozRTCSessionDescription description,
- optional VoidFunction successCallback,
- optional RTCPeerConnectionErrorCallback failureCallback);
+ Promise<mozRTCSessionDescription> createOffer (optional RTCOfferOptions options);
+ Promise<mozRTCSessionDescription> createAnswer ();
+ Promise<void> setLocalDescription (mozRTCSessionDescription description);
+ Promise<void> setRemoteDescription (mozRTCSessionDescription description);
readonly attribute mozRTCSessionDescription? localDescription;
readonly attribute mozRTCSessionDescription? remoteDescription;
readonly attribute RTCSignalingState signalingState;
void updateIce (optional RTCConfiguration configuration);
- void addIceCandidate (mozRTCIceCandidate candidate,
- optional VoidFunction successCallback,
- optional RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> addIceCandidate (mozRTCIceCandidate candidate);
readonly attribute RTCIceGatheringState iceGatheringState;
readonly attribute RTCIceConnectionState iceConnectionState;
[Pref="media.peerconnection.identity.enabled"]
readonly attribute RTCIdentityAssertion? peerIdentity;
[ChromeOnly]
attribute DOMString id;
@@ -133,26 +124,49 @@ interface mozRTCPeerConnection : EventTa
attribute EventHandler onnegotiationneeded;
attribute EventHandler onicecandidate;
attribute EventHandler onsignalingstatechange;
attribute EventHandler onaddstream;
attribute EventHandler onaddtrack; // replaces onaddstream; see AddTrackEvent
attribute EventHandler onremovestream;
attribute EventHandler oniceconnectionstatechange;
- void getStats (MediaStreamTrack? selector,
- RTCStatsCallback successCallback,
- RTCPeerConnectionErrorCallback failureCallback);
+ Promise<RTCStatsReport> getStats (MediaStreamTrack? selector);
// Data channel.
RTCDataChannel createDataChannel (DOMString label,
optional RTCDataChannelInit dataChannelDict);
attribute EventHandler ondatachannel;
[Pref="media.peerconnection.identity.enabled"]
attribute EventHandler onidentityresult;
[Pref="media.peerconnection.identity.enabled"]
attribute EventHandler onpeeridentity;
[Pref="media.peerconnection.identity.enabled"]
attribute EventHandler onidpassertionerror;
[Pref="media.peerconnection.identity.enabled"]
attribute EventHandler onidpvalidationerror;
};
+// Legacy callback API
+
+partial interface mozRTCPeerConnection {
+
+ // Dummy Promise<void> return values avoid "WebIDL.WebIDLError: error:
+ // We have overloads with both Promise and non-Promise return types"
+
+ Promise<void> createOffer (RTCSessionDescriptionCallback successCallback,
+ RTCPeerConnectionErrorCallback failureCallback,
+ optional RTCOfferOptions options);
+ Promise<void> createAnswer (RTCSessionDescriptionCallback successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> setLocalDescription (mozRTCSessionDescription description,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> setRemoteDescription (mozRTCSessionDescription description,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> addIceCandidate (mozRTCIceCandidate candidate,
+ VoidFunction successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> getStats (MediaStreamTrack? selector,
+ RTCStatsCallback successCallback,
+ RTCPeerConnectionErrorCallback failureCallback);
+};
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -7,12 +7,10 @@
* http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
*/
[Pref="media.peerconnection.enabled",
JSImplementation="@mozilla.org/dom/rtpsender;1"]
interface RTCRtpSender {
readonly attribute MediaStreamTrack track;
- void replaceTrack(MediaStreamTrack track,
- VoidFunction successCallback,
- RTCPeerConnectionErrorCallback failureCallback);
+ Promise<void> replaceTrack(MediaStreamTrack track);
};
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -38,17 +38,16 @@
#include "jsproxy.h"
#include "jsscript.h"
#include "jsstr.h"
#include "jstypes.h"
#include "jsutil.h"
#include "jswatchpoint.h"
#include "jsweakmap.h"
#include "jswrapper.h"
-#include "prmjtime.h"
#include "asmjs/AsmJSLink.h"
#include "builtin/AtomicsObject.h"
#include "builtin/Eval.h"
#include "builtin/Intl.h"
#include "builtin/MapObject.h"
#include "builtin/RegExp.h"
#include "builtin/SymbolObject.h"
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -46,16 +46,17 @@ JSCompartment::JSCompartment(Zone *zone,
isSelfHosting(false),
marked(true),
addonId(options.addonIdOrNull()),
#ifdef DEBUG
firedOnNewGlobalObject(false),
#endif
global_(nullptr),
enterCompartmentDepth(0),
+ totalTime(0),
data(nullptr),
objectMetadataCallback(nullptr),
lastAnimationTime(0),
regExps(runtime_),
globalWriteBarriered(false),
neuteredTypedObjects(0),
propertyTree(thisForCtor()),
selfHostingScriptSource(nullptr),
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -4,16 +4,17 @@
* 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/. */
#ifndef jscompartment_h
#define jscompartment_h
#include "mozilla/MemoryReporting.h"
+#include "prmjtime.h"
#include "builtin/RegExp.h"
#include "gc/Zone.h"
#include "vm/GlobalObject.h"
#include "vm/PIC.h"
#include "vm/SavedStacks.h"
namespace js {
@@ -161,20 +162,32 @@ struct JSCompartment
private:
friend struct JSRuntime;
friend struct JSContext;
friend class js::ExclusiveContext;
js::ReadBarrieredGlobalObject global_;
unsigned enterCompartmentDepth;
+ int64_t startInterval;
public:
- void enter() { enterCompartmentDepth++; }
- void leave() { enterCompartmentDepth--; }
+ int64_t totalTime;
+ void enter() {
+ if (addonId && !enterCompartmentDepth) {
+ startInterval = PRMJ_Now();
+ }
+ enterCompartmentDepth++;
+ }
+ void leave() {
+ enterCompartmentDepth--;
+ if (addonId && !enterCompartmentDepth) {
+ totalTime += (PRMJ_Now() - startInterval);
+ }
+ }
bool hasBeenEntered() { return !!enterCompartmentDepth; }
JS::Zone *zone() { return zone_; }
const JS::Zone *zone() const { return zone_; }
JS::CompartmentOptions &options() { return options_; }
const JS::CompartmentOptions &options() const { return options_; }
JSRuntime *runtimeFromMainThread() {
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -1,13 +1,14 @@
[testGeckoProfile]
# [test_bug720538] # disabled on fig - bug 897072
[testAboutPage]
# disabled on Android 2.3; bug 975187
skip-if = android_version == "10"
+[testAboutPasswords]
[testAddonManager]
# disabled on x86; bug 936216
# disabled on 2.3; bug 941624, bug 1063509, bug 1073374, bug 1087221, bug 1088023, bug 1088027, bug 1090206
skip-if = android_version == "10" || processor == "x86"
[testAddSearchEngine]
# disabled on Android 2.3; bug 979552
skip-if = android_version == "10"
[testAdobeFlash]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testAboutPasswords.java
@@ -0,0 +1,11 @@
+/* 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/. */
+
+package org.mozilla.gecko.tests;
+
+public class testAboutPasswords extends JavascriptTest {
+ public testAboutPasswords() {
+ super("testAboutPasswords.js");
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testAboutPasswords.js
@@ -0,0 +1,101 @@
+/* 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 { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AndroidLog.jsm");
+
+function ok(passed, text) {
+ do_report_result(passed, text, Components.stack.caller, false);
+}
+
+const LOGIN_FIELDS = {
+ hostname: "http://example.org/tests/robocop/robocop_blank_01.html",
+ formSubmitUrl: "",
+ realmAny: null,
+ username: "username1",
+ password: "password1",
+ usernameField: "",
+ passwordField: ""
+};
+
+const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+
+let BrowserApp;
+let browser;
+
+function add_login(login) {
+ let newLogin = new LoginInfo(login.hostname,
+ login.formSubmitUrl,
+ login.realmAny,
+ login.username,
+ login.password,
+ login.usernameField,
+ login.passwordField);
+
+ Services.logins.addLogin(newLogin);
+}
+
+add_test(function password_setup() {
+ add_login(LOGIN_FIELDS);
+
+ // Load about:passwords.
+ BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ browser = BrowserApp.addTab("about:passwords", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+ browser.addEventListener("load", () => {
+ browser.removeEventListener("load", this, true);
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+});
+
+add_test(function test_passwords_list() {
+ // Test that the (single) entry added in setup is correct.
+ let logins_list = browser.contentDocument.getElementById("logins-list");
+
+ let hostname = logins_list.querySelector(".hostname");
+ do_check_eq(hostname.textContent, LOGIN_FIELDS.hostname);
+
+ let username = logins_list.querySelector(".username");
+ do_check_eq(username.textContent, LOGIN_FIELDS.username);
+
+ let login_item = browser.contentDocument.querySelector("#logins-list > .login-item");
+ browser.addEventListener("PasswordsDetailsLoad", function() {
+ browser.removeEventListener("PasswordsDetailsLoad", this, false);
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, false);
+
+ // Expand item details.
+ login_item.click();
+});
+
+add_test(function test_passwords_details() {
+ let login_details = browser.contentDocument.getElementById("login-details");
+
+ let hostname = login_details.querySelector(".hostname");
+ do_check_eq(hostname.textContent, LOGIN_FIELDS.hostname);
+ let username = login_details.querySelector(".username");
+ do_check_eq(username.textContent, LOGIN_FIELDS.username);
+
+ // Check that details page opens link to host.
+ BrowserApp.deck.addEventListener("TabOpen", (tabevent) => {
+ // Wait for tab to finish loading.
+ let browser_target = tabevent.target;
+ browser_target.addEventListener("load", () => {
+ browser_target.removeEventListener("load", this, true);
+
+ do_check_eq(BrowserApp.selectedTab.browser.currentURI.spec, LOGIN_FIELDS.hostname);
+ Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+ }, true);
+
+ BrowserApp.deck.removeEventListener("TabOpen", this, false);
+ }, false);
+
+ browser.contentDocument.getElementById("details-header").click();
+});
+
+run_next_test();
--- a/mobile/android/base/tests/testDeviceSearchEngine.java
+++ b/mobile/android/base/tests/testDeviceSearchEngine.java
@@ -1,9 +1,11 @@
-package org.mozilla.gecko.tests;
+/* 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/. */
-
+package org.mozilla.gecko.tests;
public class testDeviceSearchEngine extends JavascriptTest {
public testDeviceSearchEngine() {
super("testDeviceSearchEngine.js");
}
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPasswords.js
@@ -0,0 +1,224 @@
+/* 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/. */
+
+let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow));
+
+let debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutPasswords");
+
+let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutPasswords.properties");
+
+function copyStringAndToast(string, notifyString) {
+ try {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(string);
+ gChromeWin.NativeWindow.toast.show(notifyString, "short");
+ } catch (e) {
+ debug("Error copying from about:passwords");
+ gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("passwordsDetails.copyFailed"), "short");
+ }
+}
+
+let Passwords = {
+ init: function () {
+ window.addEventListener("popstate", this , false);
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
+
+ this._loadList();
+
+ document.getElementById("copyusername-btn").addEventListener("click", this._copyUsername.bind(this), false);
+ document.getElementById("copypassword-btn").addEventListener("click", this._copyPassword.bind(this), false);
+ document.getElementById("details-header").addEventListener("click", this._openLink.bind(this), false);
+
+ this._showList();
+ },
+
+ uninit: function () {
+ Services.obs.removeObserver(this, "passwordmgr-storage-changed");
+ window.removeEventListener("popstate", this, false);
+ },
+
+ _loadList: function () {
+ let logins;
+ try {
+ logins = Services.logins.getAllLogins();
+ } catch(e) {
+ // Master password was not entered
+ debug("Master password permissions error: " + e);
+ return;
+ }
+
+ logins.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
+
+ logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
+
+ // Clear all content before filling the logins
+ let list = document.getElementById("logins-list");
+ list.innerHTML = "";
+ logins.forEach(login => {
+ let item = this._createItemForLogin(login);
+ list.appendChild(item);
+ });
+ },
+
+ _showList: function () {
+ // Hide the detail page and show the list
+ let details = document.getElementById("login-details");
+ details.setAttribute("hidden", "true");
+ let list = document.getElementById("logins-list");
+ list.removeAttribute("hidden");
+ },
+
+ _onPopState: function (event) {
+ // Called when back/forward is used to change the state of the page
+ if (event.state) {
+ // Show the detail page for an addon
+ this._showDetails(this._getElementForLogin(event.state.id));
+ } else {
+ // Clear any previous detail addon
+ let detailItem = document.querySelector("#login-details > .login-item");
+ detailItem.login = null;
+ this._showList();
+ }
+ },
+
+ _createItemForLogin: function (login) {
+ let loginItem = document.createElement("div");
+
+ loginItem.setAttribute("loginID", login.guid);
+ loginItem.className = "login-item list-item";
+ loginItem.addEventListener("click", () => {
+ this._showDetails(loginItem);
+ history.pushState({ id: login.guid }, document.title);
+ }, true);
+
+ // Create item icon.
+ let img = document.createElement("img");
+ img.className = "icon";
+ img.setAttribute("src", login.hostname + "/favicon.ico");
+ loginItem.appendChild(img);
+
+ // Create item details.
+ let inner = document.createElement("div");
+ inner.className = "inner";
+
+ let details = document.createElement("div");
+ details.className = "details";
+ inner.appendChild(details);
+
+ let titlePart = document.createElement("div");
+ titlePart.className = "hostname";
+ titlePart.textContent = login.hostname;
+ details.appendChild(titlePart);
+
+ let versionPart = document.createElement("div");
+ versionPart.textContent = login.httpRealm;
+ versionPart.className = "realm";
+ details.appendChild(versionPart);
+
+ let descPart = document.createElement("div");
+ descPart.textContent = login.username;
+ descPart.className = "username";
+ inner.appendChild(descPart);
+
+ loginItem.appendChild(inner);
+ loginItem.login = login;
+ return loginItem;
+ },
+
+ _getElementForLogin: function (login) {
+ let list = document.getElementById("logins-list");
+ let element = list.querySelector("div[loginID=" + login.quote() + "]");
+ return element;
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "popstate": {
+ this._onPopState(event);
+ break;
+ }
+ }
+ },
+
+ observe: function (subject, topic, data) {
+ switch(topic) {
+ case "passwordmgr-storage-changed": {
+ // Reload passwords content.
+ this._loadList();
+ break;
+ }
+ }
+ },
+
+ _showDetails: function (listItem) {
+ let detailItem = document.querySelector("#login-details > .login-item");
+ let login = detailItem.login = listItem.login;
+ let favicon = detailItem.querySelector(".icon");
+ favicon.setAttribute("src", login.hostname + "/favicon.ico");
+
+ document.getElementById("details-header").setAttribute("link", login.hostname);
+
+ document.getElementById("detail-hostname").textContent = login.hostname;
+ document.getElementById("detail-realm").textContent = login.httpRealm;
+ document.getElementById("detail-username").textContent = login.username;
+
+ // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#204
+ let matchedURL = login.hostname.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+
+ let userInputs = [];
+ if (matchedURL) {
+ let [, , domain] = matchedURL;
+ userInputs = domain.split(".").filter(part => part.length > 3);
+ }
+
+ let lastChanged = new Date(login.timePasswordChanged);
+ let days = Math.round((Date.now() - lastChanged) / 1000 / 60 / 60/ 24);
+ document.getElementById("detail-age").textContent = gStringBundle.formatStringFromName("passwordsDetails.age", [days], 1);
+
+ let list = document.getElementById("logins-list");
+ list.setAttribute("hidden", "true");
+
+ let loginDetails = document.getElementById("login-details");
+ loginDetails.removeAttribute("hidden");
+
+ // Password details page is loaded.
+ let loadEvent = document.createEvent("Events");
+ loadEvent.initEvent("PasswordsDetailsLoad", true, false);
+ window.dispatchEvent(loadEvent);
+ },
+
+ _copyUsername: function() {
+ let detailItem = document.querySelector("#login-details > .login-item");
+ let login = detailItem.login;
+ copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
+ },
+
+ _copyPassword: function() {
+ let detailItem = document.querySelector("#login-details > .login-item");
+ let login = detailItem.login;
+ copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
+ },
+
+ _openLink: function (clickEvent) {
+ let url = clickEvent.currentTarget.getAttribute("link");
+ let BrowserApp = gChromeWin.BrowserApp;
+ BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
+ }
+};
+
+window.addEventListener("load", Passwords.init.bind(Passwords), false);
+window.addEventListener("unload", Passwords.uninit.bind(Passwords), false);
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPasswords.xhtml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutPasswords.dtd" >
+%aboutDTD;
+]>
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&aboutPasswords.title;</title>
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+ <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+ <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutPasswords.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPasswords.js"></script>
+ </head>
+ <body dir="&locale.dir;">
+ <div id="passwords-header" class="header">
+ <div>&aboutPasswords.title;</div>
+ </div>
+ <div id="logins-list" class="list" hidden="true">
+ </div>
+ <div id="login-details" class="list" hidden="true">
+ <div class="login-item list-item">
+ <img class="icon"/>
+ <div id="details-header" class="inner">
+ <div class="details">
+ <div id="detail-hostname" class="hostname"></div>
+ <div id="detail-realm" class="realm"></div>
+ </div>
+ <div id="detail-username" class="username"></div>
+ <div id="detail-age"></div>
+ </div>
+ <div class="buttons">
+ <button id="copyusername-btn">&aboutPasswords.copyUsername;</button>
+ <button id="copypassword-btn">&aboutPasswords.copyPassword;</button>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7350,18 +7350,20 @@ var RemoteDebugger = {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");
}
let pathOrPort = this._getPath();
if (!pathOrPort)
pathOrPort = this._getPort();
- let listener = DebuggerServer.openListener(pathOrPort);
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = pathOrPort;
listener.allowConnection = this._showConnectionPrompt.bind(this);
+ listener.open();
dump("Remote debugger listening at path " + pathOrPort);
} catch(e) {
dump("Remote debugger didn't start: " + e);
}
},
_stop: function rd_start() {
DebuggerServer.closeAllListeners();
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -59,16 +59,18 @@ chrome.jar:
content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
* content/aboutHealthReport.js (content/aboutHealthReport.js)
#endif
#ifdef MOZ_DEVICES
content/aboutDevices.xhtml (content/aboutDevices.xhtml)
content/aboutDevices.js (content/aboutDevices.js)
#endif
#ifdef NIGHTLY_BUILD
+ content/aboutPasswords.xhtml (content/aboutPasswords.xhtml)
+ content/aboutPasswords.js (content/aboutPasswords.js)
content/WebcompatReporter.js (content/WebcompatReporter.js)
#endif
% content branding %content/branding/
% override chrome://global/content/config.xul chrome://browser/content/config.xhtml
% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
--- a/mobile/android/components/AboutRedirector.js
+++ b/mobile/android/components/AboutRedirector.js
@@ -79,16 +79,22 @@ let modules = {
},
#endif
#ifdef MOZ_DEVICES
devices: {
uri: "chrome://browser/content/aboutDevices.xhtml",
privileged: true
},
#endif
+#ifdef NIGHTLY_BUILD
+ passwords: {
+ uri: "chrome://browser/content/aboutPasswords.xhtml",
+ privileged: true
+ }
+#endif
}
function AboutRedirector() {}
AboutRedirector.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
classID: Components.ID("{322ba47e-7047-4f71-aebf-cb7d69325cd9}"),
_getModuleInfo: function (aURI) {
@@ -108,17 +114,17 @@ AboutRedirector.prototype = {
newChannel: function(aURI) {
let moduleInfo = this._getModuleInfo(aURI);
var ios = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
var channel = ios.newChannel(moduleInfo.uri, null, null);
-
+
if (!moduleInfo.privileged) {
// Setting the owner to null means that we'll go through the normal
// path in GetChannelPrincipal and create a codebase principal based
// on the channel's originalURI
channel.owner = null;
}
channel.originalURI = aURI;
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -16,16 +16,19 @@ contract @mozilla.org/network/protocol/a
contract @mozilla.org/network/protocol/about;1?what=healthreport {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
#ifdef MOZ_SAFE_BROWSING
contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
#ifdef MOZ_DEVICES
contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
+#ifdef NIGHTLY_BUILD
+contract @mozilla.org/network/protocol/about;1?what=passwords {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#endif
# DirectoryProvider.js
component {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b} DirectoryProvider.js
contract @mozilla.org/browser/directory-provider;1 {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}
category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1
# Sidebar.js
component {22117140-9c6e-11d3-aaf1-00805f8a4905} Sidebar.js
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutPasswords.dtd
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+
+<!ENTITY aboutPasswords.title "Passwords">
+
+<!ENTITY aboutPasswords.copyUsername "Copy Username">
+<!ENTITY aboutPasswords.copyPassword "Copy Password">
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutPasswords.properties
@@ -0,0 +1,9 @@
+# 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/.
+
+passwordsDetails.age=Age: %S days
+
+passwordsDetails.copyFailed=Copy failed
+passwordsDetails.passwordCopied=Password copied
+passwordsDetails.usernameCopied=Username copied
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -35,16 +35,18 @@
locale/@AB_CD@/browser/sync.properties (%chrome/sync.properties)
locale/@AB_CD@/browser/prompt.dtd (%chrome/prompt.dtd)
locale/@AB_CD@/browser/feedback.dtd (%chrome/feedback.dtd)
locale/@AB_CD@/browser/phishing.dtd (%chrome/phishing.dtd)
locale/@AB_CD@/browser/payments.properties (%chrome/payments.properties)
locale/@AB_CD@/browser/handling.properties (%chrome/handling.properties)
locale/@AB_CD@/browser/webapp.properties (%chrome/webapp.properties)
#ifdef NIGHTLY_BUILD
+ locale/@AB_CD@/browser/aboutPasswords.dtd (%chrome/aboutPasswords.dtd)
+ locale/@AB_CD@/browser/aboutPasswords.properties (%chrome/aboutPasswords.properties)
locale/@AB_CD@/browser/webcompatReporter.properties (%chrome/webcompatReporter.properties)
#endif
# overrides for toolkit l10n, also for en-US
relativesrcdir toolkit/locales:
locale/@AB_CD@/browser/overrides/about.dtd (%chrome/global/about.dtd)
locale/@AB_CD@/browser/overrides/aboutAbout.dtd (%chrome/global/aboutAbout.dtd)
locale/@AB_CD@/browser/overrides/aboutRights.dtd (%chrome/global/aboutRights.dtd)
new file mode 100644
--- /dev/null
+++ b/mobile/android/themes/core/aboutPasswords.css
@@ -0,0 +1,32 @@
+/* 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/. */
+.hidden {
+ display: none;
+}
+
+.details {
+ width: 100%;
+}
+
+.details > div {
+ display: inline;
+}
+
+.username {
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.hostname {
+ font-weight: bold;
+ overflow: hidden;
+ flex: 1;
+}
+
+.realm {
+ /* hostname is not localized, so keep the margin on the left side */
+ margin-left: .67em;
+}
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -29,16 +29,20 @@ chrome.jar:
skin/touchcontrols.css (touchcontrols.css)
skin/netError.css (netError.css)
% override chrome://global/skin/about.css chrome://browser/skin/about.css
% override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
% override chrome://global/skin/aboutSupport.css chrome://browser/skin/aboutSupport.css
% override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
% override chrome://global/skin/netError.css chrome://browser/skin/netError.css
+#ifdef NIGHTLY_BUILD
+ skin/aboutPasswords.css (aboutPasswords.css)
+#endif
+
skin/images/search.png (images/search.png)
skin/images/lock.png (images/lock.png)
skin/images/textfield.png (images/textfield.png)
skin/images/5stars.png (images/5stars.png)
skin/images/addons-32.png (images/addons-32.png)
skin/images/amo-logo.png (images/amo-logo.png)
skin/images/arrowleft-16.png (images/arrowleft-16.png)
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -794,16 +794,18 @@ pref("devtools.dump.emit", false);
// Disable device discovery logging
pref("devtools.discovery.log", false);
// Disable scanning for DevTools devices via WiFi
pref("devtools.remote.wifi.scan", false);
// Hide UI options for controlling device visibility over WiFi
// N.B.: This does not set whether the device can be discovered via WiFi, only
// whether the UI control to make such a choice is shown to the user
pref("devtools.remote.wifi.visible", false);
+// Client must complete TLS handshake within this window (ms)
+pref("devtools.remote.tls-handshake-timeout", 10000);
// view source
pref("view_source.syntax_highlight", true);
pref("view_source.wrap_long_lines", false);
pref("view_source.editor.external", false);
pref("view_source.editor.path", "");
// allows to add further arguments to the editor; use the %LINE% placeholder
// for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%")
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -331,17 +331,19 @@ function _register_modules_protocol_hand
_TESTING_MODULES_DIR);
}
let modulesURI = ios.newFileURI(modulesFile);
protocolHandler.setSubstitution("testing-common", modulesURI);
}
-function _initDebugging(port) {
+/* Debugging support */
+// Used locally and by our self-tests.
+function _setupDebuggerServer(breakpointFiles, callback) {
let prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
// Always allow remote debugging.
prefs.setBoolPref("devtools.debugger.remote-enabled", true);
// for debugging-the-debugging, let an env var cause log spew.
let env = Components.classes["@mozilla.org/process/environment;1"]
@@ -357,62 +359,69 @@ function _initDebugging(port) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
// An observer notification that tells us when we can "resume" script
// execution.
let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
- let initialized = false;
const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
let observe = function(subject, topic, data) {
switch (topic) {
case "devtools-thread-resumed":
// Exceptions in here aren't reported and block the debugger from
// resuming, so...
try {
// Add a breakpoint for the first line in our test files.
let threadActor = subject.wrappedJSObject;
let location = { line: 1 };
- for (let file of _TEST_FILE) {
+ for (let file of breakpointFiles) {
let sourceActor = threadActor.sources.source({originalUrl: file});
- sourceActor.createAndStoreBreakpoint(location);
+ sourceActor.setBreakpoint(location);
}
} catch (ex) {
do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
}
break;
case "xpcshell-test-devtools-shutdown":
// the debugger has shutdown before we got a resume event - nothing
// special to do here.
break;
}
- initialized = true;
for (let topicToRemove of TOPICS) {
obsSvc.removeObserver(observe, topicToRemove);
}
+ callback();
};
for (let topic of TOPICS) {
obsSvc.addObserver(observe, topic, false);
}
+ return DebuggerServer;
+}
+
+function _initDebugging(port) {
+ let initialized = false;
+ let DebuggerServer = _setupDebuggerServer(_TEST_FILE, () => {initialized = true;});
do_print("");
do_print("*******************************************************************");
do_print("Waiting for the debugger to connect on port " + port)
do_print("")
do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
do_print("from the Developer menu and specify the port as " + port);
do_print("*******************************************************************");
do_print("")
- let listener = DebuggerServer.openListener(port);
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = port;
listener.allowConnection = () => true;
+ listener.open();
// spin an event loop until the debugger connects.
let thr = Components.classes["@mozilla.org/thread-manager;1"]
.getService().currentThread;
while (!initialized) {
do_print("Still waiting for debugger to connect...");
thr.processNextEvent(true);
}
--- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -199,17 +199,17 @@ AboutProtocolChannel.prototype = {
// Ask the parent to synchronously read all the data from the channel.
let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
.getService(Ci.nsISyncMessageSender);
let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:OpenChannel", {
uri: this.URI.spec,
contractID: this._contractID
}, {
notificationCallbacks: this.notificationCallbacks,
- loadGroupNotificationCallbacks: this.loadGroup.notificationCallbacks
+ loadGroupNotificationCallbacks: this.loadGroup ? this.loadGroup.notificationCallbacks : null,
});
if (rval.length != 1) {
throw Cr.NS_ERROR_FAILURE;
}
let {data, contentType} = rval[0];
this.contentType = contentType;
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -236,17 +236,21 @@ let AboutProtocolParent = {
// return it to the child.
openChannel: function(msg) {
let uri = BrowserUtils.makeURI(msg.data.uri);
let contractID = msg.data.contractID;
let module = Cc[contractID].getService(Ci.nsIAboutModule);
try {
let channel = module.newChannel(uri, null);
channel.notificationCallbacks = msg.objects.notificationCallbacks;
- channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+ if (msg.objects.loadGroupNotificationCallbacks) {
+ channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+ } else {
+ channel.loadGroup = null;
+ }
let stream = channel.open();
let data = NetUtil.readInputStreamToString(stream, stream.available(), {});
return {
data: data,
contentType: channel.contentType
};
} catch (e) {
Cu.reportError(e);
--- a/toolkit/components/addoncompat/tests/addon/bootstrap.js
+++ b/toolkit/components/addoncompat/tests/addon/bootstrap.js
@@ -1,14 +1,15 @@
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const baseURL = "http://mochi.test:8888/browser/" +
"toolkit/components/addoncompat/tests/browser/";
function forEachWindow(f)
{
let wins = Services.ww.getWindowEnumerator("navigator:browser");
while (wins.hasMoreElements()) {
@@ -253,31 +254,191 @@ function testAddonContent()
gBrowser.removeTab(tab);
res.setSubstitution("addonshim1", null);
resolve();
});
});
}
+
+// Test for bug 1102410. We check that multiple nsIAboutModule's can be
+// registered in the parent, and that the child can browse to each of
+// the registered about: pages.
+function testAboutModuleRegistration()
+{
+ let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+ let modulesToUnregister = new Map();
+
+ /**
+ * This function creates a new nsIAboutModule and registers it. Callers
+ * should also call unregisterModules after using this function to clean
+ * up the nsIAboutModules at the end of this test.
+ *
+ * @param aboutName
+ * This will be the string after about: used to refer to this module.
+ * For example, if aboutName is foo, you can refer to this module by
+ * browsing to about:foo.
+ *
+ * @param uuid
+ * A unique identifer string for this module. For example,
+ * "5f3a921b-250f-4ac5-a61c-8f79372e6063"
+ */
+ let createAndRegisterAboutModule = function(aboutName, uuid) {
+
+ let AboutModule = function() {};
+
+ AboutModule.prototype = {
+ classID: Components.ID(uuid),
+ classDescription: `Testing About Module for about:${aboutName}`,
+ contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ newChannel: (aURI) => {
+ let uri = Services.io.newURI(`data:,<html><h1>${aboutName}</h1></html>`, null, null);
+ let chan = Services.io.newChannelFromURI(uri);
+ chan.originalURI = aURI;
+ return chan;
+ },
+
+ getURIFlags: (aURI) => {
+ return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+ };
+
+ let factory = {
+ createInstance: function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new AboutModule();
+ },
+ };
+
+ Registrar.registerFactory(AboutModule.prototype.classID,
+ AboutModule.prototype.classDescription,
+ AboutModule.prototype.contractID,
+ factory);
+
+ modulesToUnregister.set(AboutModule.prototype.classID,
+ factory);
+ };
+
+ /**
+ * Unregisters any nsIAboutModules registered with
+ * createAndRegisterAboutModule.
+ */
+ let unregisterModules = () => {
+ for (let [classID, factory] of modulesToUnregister) {
+ Registrar.unregisterFactory(classID, factory);
+ }
+ };
+
+ /**
+ * Takes a browser, and sends it a framescript to attempt to
+ * load some about: pages. The frame script will send a test:result
+ * message on completion, passing back a data object with:
+ *
+ * {
+ * pass: true
+ * }
+ *
+ * on success, and:
+ *
+ * {
+ * pass: false,
+ * errorMsg: message,
+ * }
+ *
+ * on failure.
+ *
+ * @param browser
+ * The browser to send the framescript to.
+ */
+ let testAboutModulesWork = (browser) => {
+ let testConnection = () => {
+ const XMLHttpRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+ "nsIXMLHttpRequest");
+ let request = new XMLHttpRequest();
+ try {
+ request.open("GET", "about:test1", false);
+ request.send(null);
+ if (request.status != 200) {
+ throw(`about:test1 response had status ${request.status} - expected 200`);
+ }
+
+ request = new XMLHttpRequest();
+ request.open("GET", "about:test2", false);
+ request.send(null);
+
+ if (request.status != 200) {
+ throw(`about:test2 response had status ${request.status} - expected 200`);
+ }
+
+ sendAsyncMessage("test:result", {
+ pass: true,
+ });
+ } catch(e) {
+ sendAsyncMessage("test:result", {
+ pass: false,
+ errorMsg: e.toString(),
+ });
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("test:result", function onTestResult(message) {
+ mm.removeMessageListener("test:result", onTestResult);
+ if (message.data.pass) {
+ ok(true, "Connections to about: pages were successful");
+ } else {
+ ok(false, message.data.errorMsg);
+ }
+ resolve();
+ });
+ mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false);
+ });
+ }
+
+ // Here's where the actual test is performed.
+ return new Promise((resolve, reject) => {
+ createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063");
+ createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f");
+
+ let newTab = gBrowser.addTab();
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+
+ testAboutModulesWork(browser).then(() => {
+ gBrowser.removeTab(newTab);
+ unregisterModules();
+ resolve();
+ });
+ });
+}
+
function runTests(win, funcs)
{
ok = funcs.ok;
is = funcs.is;
info = funcs.info;
gWin = win;
gBrowser = win.gBrowser;
return testContentWindow().
then(testListeners).
then(testCapturing).
then(testObserver).
then(testSandbox).
- then(testAddonContent);
+ then(testAddonContent).
+ then(testAboutModuleRegistration);
}
/*
bootstrap.js API
*/
function startup(aData, aReason)
{
--- a/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
+++ b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
@@ -560,24 +560,24 @@ NativeFileWatcherIOTask::Run()
* otherwise NS_OK.
*/
nsresult
NativeFileWatcherIOTask::AddPathRunnableMethod(
PathRunnablesParametersWrapper* aWrappedParameters)
{
MOZ_ASSERT(!NS_IsMainThread());
+ nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
// We return immediately if |mShuttingDown| is true (see below for
// details about the shutdown protocol being followed).
if (mShuttingDown) {
return NS_OK;
}
- nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
-
if (!wrappedParameters ||
!wrappedParameters->mChangeCallbackHandle) {
FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod - Invalid arguments.");
return NS_ERROR_NULL_POINTER;
}
// Is aPathToWatch already being watched?
WatchedResourceDescriptor* watchedResource =
@@ -730,24 +730,24 @@ NativeFileWatcherIOTask::AddPathRunnable
* handles.
*/
nsresult
NativeFileWatcherIOTask::RemovePathRunnableMethod(
PathRunnablesParametersWrapper* aWrappedParameters)
{
MOZ_ASSERT(!NS_IsMainThread());
+ nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
// We return immediately if |mShuttingDown| is true (see below for
// details about the shutdown protocol being followed).
if (mShuttingDown) {
return NS_OK;
}
- nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
-
if (!wrappedParameters ||
!wrappedParameters->mChangeCallbackHandle) {
return NS_ERROR_NULL_POINTER;
}
WatchedResourceDescriptor* toRemove =
mWatchedResourcesByPath.Get(wrappedParameters->mPath);
if (!toRemove) {
--- a/toolkit/components/jsdownloads/moz.build
+++ b/toolkit/components/jsdownloads/moz.build
@@ -2,8 +2,9 @@
# vim: set filetype=python:
# 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/.
DIRS += ['public', 'src']
XPCSHELL_TESTS_MANIFESTS += ['test/data/xpcshell.ini', 'test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -25,28 +25,34 @@
* DownloadSaver
* Template for an object that actually transfers the data for the download.
*
* DownloadCopySaver
* Saver object that simply copies the entire source file to the target.
*
* DownloadLegacySaver
* Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * DownloadPDFSaver
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"Download",
"DownloadSource",
"DownloadTarget",
"DownloadError",
"DownloadSaver",
"DownloadCopySaver",
"DownloadLegacySaver",
+ "DownloadPDFSaver",
];
////////////////////////////////////////////////////////////////////////////////
//// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
@@ -63,26 +69,31 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm")
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
"@mozilla.org/browser/download-history;1",
Ci.nsIDownloadHistory);
XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
"@mozilla.org/uriloader/external-helper-app-service;1",
Ci.nsPIExternalAppLauncher);
XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
"@mozilla.org/uriloader/external-helper-app-service;1",
Ci.nsIExternalHelperAppService);
+XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
+ "@mozilla.org/gfx/printsettings-service;1",
+ Ci.nsIPrintSettingsService);
const BackgroundFileSaverStreamListener = Components.Constructor(
"@mozilla.org/network/background-file-saver;1?mode=streamlistener",
"nsIBackgroundFileSaver");
/**
* Returns true if the given value is a primitive string or a String object.
*/
@@ -539,17 +550,17 @@ this.Download.prototype = {
* @return {Promise}
* @resolves When the instruction to launch the file has been
* successfully given to the operating system. Note that
* the OS might still take a while until the file is actually
* launched.
* @rejects JavaScript exception if there was an error trying to launch
* the file.
*/
- launch: function() {
+ launch: function () {
if (!this.succeeded) {
return Promise.reject(
new Error("launch can only be called if the download succeeded")
);
}
return DownloadIntegration.launchDownload(this);
},
@@ -906,21 +917,26 @@ this.Download.prototype = {
*/
toSerializable: function ()
{
let serializable = {
source: this.source.toSerializable(),
target: this.target.toSerializable(),
};
+ let saver = this.saver.toSerializable();
+ if (!saver) {
+ // If we are unable to serialize the saver, we won't persist the download.
+ return null;
+ }
+
// Simplify the representation for the most common saver type. If the saver
// is an object instead of a simple string, we can't simplify it because we
// need to persist all its properties, not only "type". This may happen for
// savers of type "copy" as well as other types.
- let saver = this.saver.toSerializable();
if (saver !== "copy") {
serializable.saver = saver;
}
if (this.error) {
serializable.errorObj = this.error.toSerializable();
}
@@ -1121,16 +1137,20 @@ this.DownloadSource.prototype = {
*/
this.DownloadSource.fromSerializable = function (aSerializable) {
let source = new DownloadSource();
if (isString(aSerializable)) {
// Convert String objects to primitive strings at this point.
source.url = aSerializable.toString();
} else if (aSerializable instanceof Ci.nsIURI) {
source.url = aSerializable.spec;
+ } else if (aSerializable instanceof Ci.nsIDOMWindow) {
+ source.url = aSerializable.location.href;
+ source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable);
+ source.windowRef = Cu.getWeakReference(aSerializable);
} else {
// Convert String objects to primitive strings at this point.
source.url = aSerializable.url.toString();
if ("isPrivate" in aSerializable) {
source.isPrivate = aSerializable.isPrivate;
}
if ("referrer" in aSerializable) {
source.referrer = aSerializable.referrer;
@@ -1521,16 +1541,19 @@ this.DownloadSaver.fromSerializable = fu
let saver;
switch (serializable.type) {
case "copy":
saver = DownloadCopySaver.fromSerializable(serializable);
break;
case "legacy":
saver = DownloadLegacySaver.fromSerializable(serializable);
break;
+ case "pdf":
+ saver = DownloadPDFSaver.fromSerializable(serializable);
+ break;
default:
throw new Error("Unrecoginzed download saver type.");
}
return saver;
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadCopySaver
@@ -1944,17 +1967,17 @@ this.DownloadCopySaver.fromSerializable
////////////////////////////////////////////////////////////////////////////////
//// DownloadLegacySaver
/**
* Saver object that integrates with the legacy nsITransfer interface.
*
* For more background on the process, see the DownloadLegacyTransfer object.
*/
-this.DownloadLegacySaver = function()
+this.DownloadLegacySaver = function ()
{
this.deferExecuted = Promise.defer();
this.deferCanceled = Promise.defer();
}
this.DownloadLegacySaver.prototype = {
__proto__: DownloadSaver.prototype,
@@ -2297,8 +2320,164 @@ this.DownloadLegacySaver.prototype = {
/**
* Returns a new DownloadLegacySaver object. This saver type has a
* deserializable form only when creating a new object in memory, because it
* cannot be serialized to disk.
*/
this.DownloadLegacySaver.fromSerializable = function () {
return new DownloadLegacySaver();
};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadPDFSaver
+
+/**
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ *
+ * In order to prevent the download from saving a different document than the one
+ * originally loaded in the window, any attempt to restart the download will fail.
+ *
+ * Since this DownloadSaver type requires a live document as a source, it cannot
+ * be persisted across sessions, unless the download already succeeded.
+ */
+this.DownloadPDFSaver = function () {
+}
+
+this.DownloadPDFSaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * An nsIWebBrowserPrint instance for printing this page.
+ * This is null when saving has not started or has completed,
+ * or while the operation is being canceled.
+ */
+ _webBrowserPrint: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function (aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ return Task.spawn(function task_DCS_execute() {
+ if (!this.download.source.windowRef) {
+ throw new DownloadError({
+ message: "PDF saver must be passed an open window, and cannot be restarted.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ let win = this.download.source.windowRef.get();
+
+ // Set windowRef to null to avoid re-trying.
+ this.download.source.windowRef = null;
+
+ if (!win) {
+ throw new DownloadError({
+ message: "PDF saver can't save a window that has been closed.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ this.addToHistory();
+
+ let targetPath = this.download.target.path;
+
+ // An empty target file must exist for the PDF printer to work correctly.
+ let file = yield OS.File.open(targetPath, { truncate: true });
+ yield file.close();
+
+ let printSettings = gPrintSettingsService.newPrintSettings;
+
+ printSettings.printToFile = true;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ printSettings.toFileName = targetPath;
+
+ printSettings.printSilent = true;
+ printSettings.showPrintProgress = false;
+
+ printSettings.printBGImages = true;
+ printSettings.printBGColors = true;
+ printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+ printSettings.headerStrCenter = "";
+ printSettings.headerStrLeft = "";
+ printSettings.headerStrRight = "";
+ printSettings.footerStrCenter = "";
+ printSettings.footerStrLeft = "";
+ printSettings.footerStrRight = "";
+
+ this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserPrint);
+
+ try {
+ yield new Promise((resolve, reject) => {
+ this._webBrowserPrint.print(printSettings, {
+ onStateChange: function (webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (!Components.isSuccessCode(status)) {
+ reject(new DownloadError({ result: status,
+ inferCause: true }));
+ } else {
+ resolve();
+ }
+ }
+ },
+ onProgressChange: function (webProgress, request, curSelfProgress,
+ maxSelfProgress, curTotalProgress,
+ maxTotalProgress) {
+ aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false);
+ },
+ onLocationChange: function () {},
+ onStatusChange: function () {},
+ onSecurityChange: function () {},
+ });
+ });
+ } finally {
+ // Remove the print object to avoid leaks
+ this._webBrowserPrint = null;
+ }
+
+ let fileInfo = yield OS.File.stat(targetPath);
+ aSetProgressBytesFn(fileInfo.size, fileInfo.size, false);
+ }.bind(this));
+ },
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DCS_cancel()
+ {
+ if (this._webBrowserPrint) {
+ this._webBrowserPrint.cancel();
+ this._webBrowserPrint = null;
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ if (this.download.succeeded) {
+ return DownloadCopySaver.prototype.toSerializable.call(this);
+ }
+
+ // This object needs a window to recreate itself. If it didn't succeded
+ // it will not be possible to restart. Returning null here will
+ // prevent us from serializing it at all.
+ return null;
+ },
+};
+
+/**
+ * Creates a new DownloadPDFSaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadPDFSaver object.
+ *
+ * @return The newly created DownloadPDFSaver object.
+ */
+this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
+ return new DownloadPDFSaver();
+};
+
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -159,17 +159,23 @@ this.DownloadStore.prototype = {
// Take a static snapshot of the current state of all the downloads.
let storeData = { list: [] };
let atLeastOneDownload = false;
for (let download of downloads) {
try {
if (!this.onsaveitem(download)) {
continue;
}
- storeData.list.push(download.toSerializable());
+
+ let serializable = download.toSerializable();
+ if (!serializable) {
+ // This item cannot be persisted across sessions.
+ continue;
+ }
+ storeData.list.push(serializable);
atLeastOneDownload = true;
} catch (ex) {
// If an item cannot be converted to a serializable form, don't
// prevent others from being saved.
Cu.reportError(ex);
}
}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+ testFile.html
+
+[browser_DownloadPDFSaver.js]
+skip-if = e10s || os != "win"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the PDF download saver, and tests using a window as a
+ * source for the copy download saver.
+ */
+
+"use strict";
+
+/**
+ * Helper function to make sure a window reference exists on the download source.
+ */
+function* test_download_windowRef(aTab, aDownload) {
+ ok(aDownload.source.windowRef, "Download source had a window reference");
+ ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
+ is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
+}
+
+/**
+ * Helper function to check the state of a completed download.
+ */
+function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
+ ok(aDownload.source, "Download has a source");
+ is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
+ is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
+ ok(aDownload.stopped, "Download is stopped");
+ is(aCanceled, aDownload.canceled, "Download has correct canceled state");
+ is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
+ is(aDownload.error, null, "Download error is not defined");
+}
+
+function* test_createDownload_common(aPrivate, aType) {
+ let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ if (aPrivate) {
+ tab.linkedBrowser.docShell.QueryInterface(Ci.nsILoadContext)
+ .usePrivateBrowsing = true;
+ }
+
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: { type: aType },
+ });
+
+ yield test_download_windowRef(tab, download);
+ yield download.start();
+
+ yield test_download_state_complete(tab, download, aPrivate, false);
+ if (aType == "pdf") {
+ let signature = yield OS.File.read(download.target.path,
+ { bytes: 4, encoding: "us-ascii" });
+ is(signature, "%PDF", "File exists and signature matches");
+ } else {
+ ok((yield OS.File.exists(download.target.path)), "File exists");
+ }
+
+ gBrowser.removeTab(tab);
+}
+
+add_task(function* test_createDownload_pdf_private() {
+ yield test_createDownload_common(true, "pdf");
+});
+add_task(function* test_createDownload_pdf_not_private() {
+ yield test_createDownload_common(false, "pdf");
+});
+
+// Even for the copy saver, using a window should produce valid results
+add_task(function* test_createDownload_copy_private() {
+ yield test_createDownload_common(true, "copy");
+});
+add_task(function* test_createDownload_copy_not_private() {
+ yield test_createDownload_common(false, "copy");
+});
+
+add_task(function* test_cancel_pdf_download() {
+ let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: "pdf",
+ });
+
+ yield test_download_windowRef(tab, download);
+ download.start();
+
+ // Immediately cancel the download to test that it is erased correctly.
+ yield download.cancel();
+ yield test_download_state_complete(tab, download, false, true);
+
+ let exists = yield OS.File.exists(download.target.path)
+ ok(!exists, "Target file does not exist");
+
+ gBrowser.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/head.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Support functions
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ ok(!file.exists(), "Temp file does not exist");
+
+ registerCleanupFunction(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+function promiseBrowserLoaded(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener("load", function onLoad(event) {
+ if (event.target == browser.contentDocument) {
+ browser.removeEventListener("load", onLoad, true);
+ resolve();
+ }
+ }, true);
+ });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/testFile.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Save as PDF</title>
+ </head>
+ <body>
+ <p>Save me as a PDF!</p>
+ </body>
+</html>
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -52,23 +52,35 @@ add_task(function test_save_reload()
listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
listForSave.add(yield Downloads.createDownload({
source: { url: httpUrl("empty.txt"),
referrer: TEST_REFERRER_URL },
target: getTempFile(TEST_TARGET_FILE_NAME),
}));
+ // This PDF download should not be serialized because it never succeeds.
+ let pdfDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ saver: "pdf",
+ });
+ listForSave.add(pdfDownload);
+
let legacyDownload = yield promiseStartLegacyDownload();
yield legacyDownload.cancel();
listForSave.add(legacyDownload);
yield storeForSave.save();
yield storeForLoad.load();
+ // Remove the PDF download because it should not appear in this list.
+ listForSave.remove(pdfDownload);
+
let itemsForSave = yield listForSave.getAll();
let itemsForLoad = yield listForLoad.getAll();
do_check_eq(itemsForSave.length, itemsForLoad.length);
// Downloads should be reloaded in the same order.
for (let i = 0; i < itemsForSave.length; i++) {
// The reloaded downloads are different objects.
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -56,16 +56,41 @@ add_task(function test_createDownload_pu
source: { url: "about:blank" },
target: { path: tempPath },
saver: { type: "copy" }
});
do_check_false(download.source.isPrivate);
});
/**
+ * Tests createDownload for a pdf saver throws if only given a url.
+ */
+add_task(function test_createDownload_pdf()
+{
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "pdf" },
+ });
+
+ try {
+ yield download.start();
+ do_throw("The download should have failed.");
+ } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) { }
+
+ do_check_false(download.succeeded);
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
* Tests "fetch" with nsIURI and nsIFile as arguments.
*/
add_task(function test_fetch_uri_file_arguments()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
});
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -264,17 +264,17 @@ CreateRoot(nsCOMPtr<mozIStorageConnectio
// The position of the new item in its folder.
static int32_t itemPosition = 0;
// A single creation timestamp for all roots so that the root folder's
// last modification time isn't earlier than its childrens' creation time.
static PRTime timestamp = 0;
if (!timestamp)
- timestamp = PR_Now();
+ timestamp = RoundedPRNow();
// Create a new bookmark folder for the root.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
"INSERT INTO moz_bookmarks "
"(type, position, title, dateAdded, lastModified, guid, parent) "
"VALUES (:item_type, :item_position, :item_title,"
":date_added, :last_modified, :guid,"
@@ -723,16 +723,23 @@ Database::InitSchema(bool* aDatabaseMigr
if (currentSchemaVersion < 25) {
rv = MigrateV25Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 36 uses schema version 25.
+ if (currentSchemaVersion < 26) {
+ rv = MigrateV26Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 37 uses schema version 26.
+
// Schema Upgrades must add migration code here.
rv = UpdateBookmarkRootTitles();
// We don't want a broken localization to cause us to think
// the database is corrupt and needs to be replaced.
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
@@ -1462,16 +1469,29 @@ Database::MigrateV25Up()
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
+nsresult
+Database::MigrateV26Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Round down dateAdded and lastModified values to milliseconds precision.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET dateAdded = dateAdded - dateAdded % 1000, "
+ " lastModified = lastModified - lastModified % 1000"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
void
Database::Shutdown()
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mShuttingDown);
MOZ_ASSERT(!mClosed);
mShuttingDown = true;
@@ -1575,16 +1595,32 @@ Database::Observe(nsISupports *aSubject,
"FROM moz_favicons "
"WHERE guid IS NULL "
), getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->ExecuteStep(&haveNullGuids);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(!haveNullGuids && "Found a favicon without a GUID!");
}
+
+ { // Sanity check for unrounded dateAdded and lastModified values (bug
+ // 1107308).
+ bool hasUnroundedDates = false;
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 "
+ "FROM moz_bookmarks "
+ "WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->ExecuteStep(&hasUnroundedDates);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(!hasUnroundedDates && "Found unrounded dates!");
+ }
#endif
// As the last step in the shutdown path, finalize the database handle.
Shutdown();
}
return NS_OK;
}
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -11,17 +11,17 @@
#include "nsIObserver.h"
#include "mozilla/storage.h"
#include "mozilla/storage/StatementCache.h"
#include "mozilla/Attributes.h"
#include "nsIEventTarget.h"
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 25
+#define DATABASE_SCHEMA_VERSION 26
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
// Fired when initialization fails due to a locked database.
#define TOPIC_DATABASE_LOCKED "places-database-locked"
// This topic is received when the profile is about to be lost. Places does
// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
// Any shutdown work that requires the Places APIs should happen here.
@@ -268,16 +268,17 @@ protected:
nsresult MigrateV18Up();
nsresult MigrateV19Up();
nsresult MigrateV20Up();
nsresult MigrateV21Up();
nsresult MigrateV22Up();
nsresult MigrateV23Up();
nsresult MigrateV24Up();
nsresult MigrateV25Up();
+ nsresult MigrateV26Up();
nsresult UpdateBookmarkRootTitles();
private:
~Database();
/**
* Singleton getter, invoked by class instantiation.
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -316,16 +316,26 @@ void
TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed)
{
aTrimmed = aTitle;
if (aTitle.Length() > TITLE_LENGTH_MAX) {
aTrimmed = StringHead(aTitle, TITLE_LENGTH_MAX);
}
}
+PRTime
+RoundToMilliseconds(PRTime aTime) {
+ return aTime - (aTime % PR_USEC_PER_MSEC);
+}
+
+PRTime
+RoundedPRNow() {
+ return RoundToMilliseconds(PR_Now());
+}
+
void
ForceWALCheckpoint()
{
nsRefPtr<Database> DB = Database::GetDatabase();
if (DB) {
nsCOMPtr<mozIStorageAsyncStatement> stmt = DB->GetAsyncStatement(
"pragma wal_checkpoint "
);
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -9,16 +9,17 @@
/**
* This file contains helper classes used by various bits of Places code.
*/
#include "mozilla/storage.h"
#include "nsIURI.h"
#include "nsThreadUtils.h"
#include "nsProxyRelease.h"
+#include "prtime.h"
#include "mozilla/Telemetry.h"
namespace mozilla {
namespace places {
////////////////////////////////////////////////////////////////////////////////
//// Asynchronous Statement Callback Helper
@@ -144,16 +145,32 @@ bool IsValidGUID(const nsACString& aGUID
* @param aTitle
* The title to truncate (if necessary)
* @param aTrimmed
* Output parameter to return the trimmed string
*/
void TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed);
/**
+ * Round down a PRTime value to milliseconds precision (...000).
+ *
+ * @param aTime
+ * a PRTime value.
+ * @return aTime rounded down to milliseconds precision.
+ */
+PRTime RoundToMilliseconds(PRTime aTime);
+
+/**
+ * Round down PR_Now() to milliseconds precision.
+ *
+ * @return @see PR_Now, RoundToMilliseconds.
+ */
+PRTime RoundedPRNow();
+
+/**
* Used to finalize a statementCache on a specified thread.
*/
template<typename StatementType>
class FinalizeStatementCacheProxy : public nsRunnable
{
public:
/**
* Constructor.
--- a/toolkit/components/places/nsAnnotationService.cpp
+++ b/toolkit/components/places/nsAnnotationService.cpp
@@ -1912,32 +1912,32 @@ nsAnnotationService::StartSetAnnotation(
rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), oldAnnoId);
NS_ENSURE_SUCCESS(rv, rv);
rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), oldAnnoDate);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
rv = aStatement->BindNullByName(NS_LITERAL_CSTRING("id"));
NS_ENSURE_SUCCESS(rv, rv);
- rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), PR_Now());
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), RoundedPRNow());
NS_ENSURE_SUCCESS(rv, rv);
}
rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("fk"), fkId);
NS_ENSURE_SUCCESS(rv, rv);
rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), nameID);
NS_ENSURE_SUCCESS(rv, rv);
rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("flags"), aFlags);
NS_ENSURE_SUCCESS(rv, rv);
rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("expiration"), aExpiration);
NS_ENSURE_SUCCESS(rv, rv);
rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("type"), aType);
NS_ENSURE_SUCCESS(rv, rv);
- rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), PR_Now());
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), RoundedPRNow());
NS_ENSURE_SUCCESS(rv, rv);
// On success, leave the statement open, the caller will set the value
// and execute the statement.
setAnnoScoper.Abandon();
return NS_OK;
}
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -403,37 +403,61 @@ interface nsINavBookmarksService : nsISu
* @param aItemId
* The id of the item whose title should be retrieved
* @return The title of the item.
*/
AUTF8String getItemTitle(in long long aItemId);
/**
* Set the date added time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose date added time should be updated.
+ * @param aDateAdded
+ * the new date added value in microseconds. Note that it is rounded
+ * down to milliseconds precision.
*/
void setItemDateAdded(in long long aItemId, in PRTime aDateAdded);
+
/**
* Get the date added time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose date added time should be retrieved.
+ *
+ * @return the date added value in microseconds.
*/
PRTime getItemDateAdded(in long long aItemId);
/**
* Set the last modified time for an item.
*
- * @note This is the only method that will send an itemChanged notification
- * for the property. lastModified will still be updated in
- * any other method that changes an item property, but we will send
- * the corresponding itemChanged notification instead.
+ * @param aItemId
+ * the id of the item whose last modified time should be updated.
+ * @param aLastModified
+ * the new last modified value in microseconds. Note that it is
+ * rounded down to milliseconds precision.
+ *
+ * @note This is the only method that will send an itemChanged notification
+ * for the property. lastModified will still be updated in
+ * any other method that changes an item property, but we will send
+ * the corresponding itemChanged notification instead.
*/
void setItemLastModified(in long long aItemId, in PRTime aLastModified);
+
/**
* Get the last modified time for an item.
*
- * @note When an item is added lastModified is set to the same value as
- * dateAdded.
+ * @param aItemId
+ * the id of the item whose last modified time should be retrieved.
+ *
+ * @return the date added value in microseconds.
+ *
+ * @note When an item is added lastModified is set to the same value as
+ * dateAdded.
*/
PRTime getItemLastModified(in long long aItemId);
/**
* Get the URI for a bookmark item.
*/
nsIURI getBookmarkURI(in long long aItemId);
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -521,17 +521,17 @@ nsNavBookmarks::InsertBookmark(int64_t a
else {
index = aIndex;
// Create space for the insertion.
rv = AdjustIndices(aFolder, index, INT32_MAX, 1);
NS_ENSURE_SUCCESS(rv, rv);
}
*aNewBookmarkId = -1;
- PRTime dateAdded = PR_Now();
+ PRTime dateAdded = RoundedPRNow();
nsAutoCString guid(aGUID);
nsCString title;
TruncateTitle(aTitle, title);
rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
0, folderGuid, grandParentId, aURI,
aNewBookmarkId, guid);
NS_ENSURE_SUCCESS(rv, rv);
@@ -623,17 +623,17 @@ nsNavBookmarks::RemoveItem(int64_t aItem
// Fix indices in the parent.
if (bookmark.position != DEFAULT_INDEX) {
rv = AdjustIndices(bookmark.parentId,
bookmark.position + 1, INT32_MAX, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
- bookmark.lastModified = PR_Now();
+ bookmark.lastModified = RoundedPRNow();
rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId,
bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> uri;
@@ -751,17 +751,17 @@ nsNavBookmarks::CreateContainerWithID(in
} else {
index = *aIndex;
// Create space for the insertion.
rv = AdjustIndices(aParent, index, INT32_MAX, 1);
NS_ENSURE_SUCCESS(rv, rv);
}
*aNewFolder = aItemId;
- PRTime dateAdded = PR_Now();
+ PRTime dateAdded = RoundedPRNow();
nsAutoCString guid(aGUID);
nsCString title;
TruncateTitle(aTitle, title);
rv = InsertBookmarkInDB(-1, FOLDER, aParent, index,
title, dateAdded, 0, folderGuid, grandParentId,
nullptr, aNewFolder, guid);
NS_ENSURE_SUCCESS(rv, rv);
@@ -812,17 +812,17 @@ nsNavBookmarks::InsertSeparator(int64_t
NS_ENSURE_SUCCESS(rv, rv);
}
*aNewItemId = -1;
// Set a NULL title rather than an empty string.
nsCString voidString;
voidString.SetIsVoid(true);
nsAutoCString guid(aGUID);
- PRTime dateAdded = PR_Now();
+ PRTime dateAdded = RoundedPRNow();
rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, voidString, dateAdded,
0, folderGuid, grandParentId, nullptr,
aNewItemId, guid);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
@@ -1100,17 +1100,17 @@ nsNavBookmarks::RemoveFolderChildren(int
"DELETE FROM moz_items_annos "
"WHERE id IN ("
"SELECT a.id from moz_items_annos a "
"LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
"WHERE b.id ISNULL)"));
NS_ENSURE_SUCCESS(rv, rv);
// Set the lastModified date.
- rv = SetItemDateInternal(LAST_MODIFIED, folder.id, PR_Now());
+ rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow());
NS_ENSURE_SUCCESS(rv, rv);
for (uint32_t i = 0; i < folderChildrenArray.Length(); i++) {
BookmarkData& child = folderChildrenArray[i];
if (child.type == TYPE_BOOKMARK) {
// If not a tag, recalculate frecency for this entry, since it changed.
if (child.grandParentId != mTagsRoot) {
nsNavHistory* history = nsNavHistory::GetHistoryService();
@@ -1284,17 +1284,17 @@ nsNavBookmarks::MoveItem(int64_t aItemId
rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), newIndex);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
- PRTime now = PR_Now();
+ PRTime now = RoundedPRNow();
rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now);
NS_ENSURE_SUCCESS(rv, rv);
rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
@@ -1381,16 +1381,18 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
return NS_OK;
}
nsresult
nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
int64_t aItemId,
PRTime aValue)
{
+ aValue = RoundToMilliseconds(aValue);
+
nsCOMPtr<mozIStorageStatement> stmt;
if (aDateType == DATE_ADDED) {
// lastModified is set to the same value as dateAdded. We do this for
// performance reasons, since it will allow us to use an index to sort items
// by date.
stmt = mDB->GetStatement(
"UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date "
"WHERE id = :item_id"
@@ -1422,17 +1424,19 @@ nsNavBookmarks::SetItemDateInternal(enum
NS_IMETHODIMP
nsNavBookmarks::SetItemDateAdded(int64_t aItemId, PRTime aDateAdded)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.dateAdded = aDateAdded;
+
+ // Round here so that we notify with the right value.
+ bookmark.dateAdded = RoundToMilliseconds(aDateAdded);
rv = SetItemDateInternal(DATE_ADDED, bookmark.id, bookmark.dateAdded);
NS_ENSURE_SUCCESS(rv, rv);
// Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
@@ -1466,17 +1470,19 @@ nsNavBookmarks::GetItemDateAdded(int64_t
NS_IMETHODIMP
nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified)
{
NS_ENSURE_ARG_MIN(aItemId, 1);
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = aLastModified;
+
+ // Round here so that we notify with the right value.
+ bookmark.lastModified = RoundToMilliseconds(aLastModified);
rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
// Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
@@ -1530,17 +1536,17 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
if (title.IsVoid()) {
rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
}
else {
rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
title);
}
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = PR_Now();
+ bookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
@@ -1999,17 +2005,17 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
"UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
"WHERE id = :item_id "
);
NS_ENSURE_STATE(statement);
mozStorageStatementScoper scoper(statement);
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId);
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = PR_Now();
+ bookmark.lastModified = RoundedPRNow();
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
rv = statement->Execute();
NS_ENSURE_SUCCESS(rv, rv);
@@ -2355,17 +2361,17 @@ nsNavBookmarks::SetKeywordForBookmark(in
// Add new keyword association to the hash, removing the old one if needed.
if (!oldKeyword.IsEmpty())
mBookmarkToKeywordHash.Remove(bookmark.id);
mBookmarkToKeywordHash.Put(bookmark.id, keyword);
rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
}
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = PR_Now();
+ bookmark.lastModified = RoundedPRNow();
rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"),
bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
bookmark.id);
NS_ENSURE_SUCCESS(rv, rv);
rv = updateBookmarkStmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
@@ -2830,17 +2836,17 @@ nsNavBookmarks::OnPageAnnotationSet(nsIU
NS_IMETHODIMP
nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName)
{
BookmarkData bookmark;
nsresult rv = FetchItemInfo(aItemId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
- bookmark.lastModified = PR_Now();
+ bookmark.lastModified = RoundedPRNow();
rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
NS_ENSURE_SUCCESS(rv, rv);
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavBookmarkObserver,
OnItemChanged(bookmark.id,
aName,
true,
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -3915,17 +3915,17 @@ nsNavHistoryFolderResultNode::OnItemMove
NS_ENSURE_SUCCESS(rv, rv);
}
if (aOldParent == mTargetFolderItemId) {
OnItemRemoved(aItemId, aOldParent, aOldIndex, aItemType, itemURI,
aGUID, aOldParentGUID);
}
if (aNewParent == mTargetFolderItemId) {
OnItemAdded(aItemId, aNewParent, aNewIndex, aItemType, itemURI, itemTitle,
- PR_Now(), // This is a dummy dateAdded, not the real value.
+ RoundedPRNow(), // This is a dummy dateAdded, not the real value.
aGUID, aNewParentGUID);
}
}
return NS_OK;
}
/**
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
@@ -144,18 +144,20 @@ add_task(function test_bookmarks() {
do_check_eq(lastModified, dateAdded);
// The time before we set the title, in microseconds.
let beforeSetTitle = Date.now() * 1000;
do_check_true(beforeSetTitle >= beforeInsert);
// Workaround possible VM timers issues moving lastModified and dateAdded
// to the past.
- bs.setItemLastModified(newId, --lastModified);
- bs.setItemDateAdded(newId, --dateAdded);
+ lastModified -= 1000;
+ bs.setItemLastModified(newId, lastModified);
+ dateAdded -= 1000;
+ bs.setItemDateAdded(newId, dateAdded);
// set bookmark title
bs.setItemTitle(newId, "Google");
do_check_eq(bookmarksObserver._itemChangedId, newId);
do_check_eq(bookmarksObserver._itemChangedProperty, "title");
do_check_eq(bookmarksObserver._itemChangedValue, "Google");
// check that dateAdded hasn't changed
@@ -322,18 +324,20 @@ add_task(function test_bookmarks() {
try {
let dateAdded = bs.getItemDateAdded(kwTestItemId);
// after just inserting, modified should not be set
let lastModified = bs.getItemLastModified(kwTestItemId);
do_check_eq(lastModified, dateAdded);
// Workaround possible VM timers issues moving lastModified and dateAdded
// to the past.
+ lastModified -= 1000;
bs.setItemLastModified(kwTestItemId, --lastModified);
- bs.setItemDateAdded(kwTestItemId, --dateAdded);
+ dateAdded -= 1000;
+ bs.setItemDateAdded(kwTestItemId, dateAdded);
bs.setKeywordForBookmark(kwTestItemId, "bar");
let lastModified2 = bs.getItemLastModified(kwTestItemId);
LOG("test setKeywordForBookmark");
LOG("dateAdded = " + dateAdded);
LOG("lastModified = " + lastModified);
LOG("lastModified2 = " + lastModified2);
@@ -449,18 +453,20 @@ add_task(function test_bookmarks() {
bs.DEFAULT_INDEX, "");
dateAdded = bs.getItemDateAdded(newId10);
// after just inserting, modified should not be set
lastModified = bs.getItemLastModified(newId10);
do_check_eq(lastModified, dateAdded);
// Workaround possible VM timers issues moving lastModified and dateAdded
// to the past.
- bs.setItemLastModified(newId10, --lastModified);
- bs.setItemDateAdded(newId10, --dateAdded);
+ lastModified -= 1000;
+ bs.setItemLastModified(newId10, lastModified);
+ dateAdded -= 1000;
+ bs.setItemDateAdded(newId10, dateAdded);
bs.changeBookmarkURI(newId10, uri("http://foo11.com/"));
// check that lastModified is set after we change the bookmark uri
lastModified2 = bs.getItemLastModified(newId10);
LOG("test changeBookmarkURI");
LOG("dateAdded = " + dateAdded);
LOG("lastModified = " + lastModified);
@@ -591,22 +597,22 @@ add_task(function test_bookmarks() {
}
// check setItemLastModified() and setItemDateAdded()
let newId14 = bs.insertBookmark(testRoot, uri("http://bar.tld/"),
bs.DEFAULT_INDEX, "");
dateAdded = bs.getItemDateAdded(newId14);
lastModified = bs.getItemLastModified(newId14);
do_check_eq(lastModified, dateAdded);
- bs.setItemLastModified(newId14, 1234);
+ bs.setItemLastModified(newId14, 1234000000000000);
let fakeLastModified = bs.getItemLastModified(newId14);
- do_check_eq(fakeLastModified, 1234);
- bs.setItemDateAdded(newId14, 4321);
+ do_check_eq(fakeLastModified, 1234000000000000);
+ bs.setItemDateAdded(newId14, 4321000000000000);
let fakeDateAdded = bs.getItemDateAdded(newId14);
- do_check_eq(fakeDateAdded, 4321);
+ do_check_eq(fakeDateAdded, 4321000000000000);
// ensure that removing an item removes its annotations
do_check_true(anno.itemHasAnnotation(newId3, "test-annotation"));
bs.removeItem(newId3);
do_check_false(anno.itemHasAnnotation(newId3, "test-annotation"));
// bug 378820
let uri1 = uri("http://foo.tld/a");
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* 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/. */
-const CURRENT_SCHEMA_VERSION = 25;
+const CURRENT_SCHEMA_VERSION = 26;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
// Shortcuts to transitions type.
const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
index ef5e1def9b7f5a5c6495a5c3ac5eb76cb6a6038b..2afd1da1fd7ce40cda051975bb26d4a3223ff4b8
GIT binary patch
literal 1179648
zc%1Fsdz@TleK7F3?PWIEgp0U<76v3hN;Z2-NVr7YkPYO1T_C9MGTEKoOftK(nVHRQ
z7Lt=hR8aaBTWz(qg72${tyb&hZ50qHRJ@?&tyI)1-ms+tFJ7vEh47x4-A#6L0qm!r
z4_>}snDd<H_gv32&pC7UuWVSiGF!@|dh><;bSc#q*%OJyBCkoMB9TZu{G1Uv=#=o2
zn3Ru(pOYfbFCJ<9*P8lWpNPCFcY0*^zFmKJ&8K%2um1WqZyvpS=TAqk*?GavPmTQh
z@cxmH4xcgnw?khm4VQkucw2E};gQ15?N4v-8vMrKhWvN)>47f~T+#pC{@3Qd5OxCq
z0000000000000000000000000000000Kdlep7E-hhIQve_ioA#^khc*^P^qaQl`Jy
zl^z(#7c1xKQmK&LJXFdYsm2)>ukUEz*pcd7y=+})M{3#XB^_6!UbvUkn$?HaN}XNK
zbY*+yq{{iO0}aB^?7cHjuW7jC)ac%h1Cto*dN7yn&Ky3&O5v-HHn*cTotW16geo<T
zHlx^SH4V$oitat_z@VG+`EC8_!nR^p-%z%vYiJ<5eJFEy>FGzC*3sKf%<Djn)Y;`K
zvv;Iw8ZJI9y7z<wqg1AR<Oz%wop!X*kJ55t=&|DQaf_$cG_;4~9((AxLxtRthfTe}
zu!l4}bkwkT_TFPpscF~{uK4^Ttay3v<kH1bf4(Q%o6YnbUggyB860W7kJ$PMi{86!
zPO4mQ)p)(xyMC{x;i98#g28klGf+Cb;FRZ?$fPz$7=EmDPHHe;%$BnGf!W(%S<`Sq
zIDGw4R^;&T@4dh(Jl~a<b`EA{?~T7A+;dHnFL_J0Sjrc6?#PC-Dt7gxC*6lrie7ox
z*hgtOG5(a|)Y+A^QY)Or$<IHF-a@|r$VIPsfms}Ad6Zd{%eyL>*?SvKs%cmi&f=n@
z%%ZY=kG>!$zra+cHb2UI4nCwNt<UZg!^h=mlb2`wJ}d9uO1&ww8Y?{Mu<0DN(V_Qe
zxklHd@%tNU8Wx4)Cl4LJw~*=140P{2B!1%2hMrRY&`~D}XK#IZO~ZmC4Of{`cYbJ~
zbTHU(w6P9WK6Ie5E2|XE&YV!wFh3kCdgxeN^2O32v6mlhr~`Em9jTDnQ7N3=|FZDb
z4u_d`*b1h1WV`bNhlEc!>H<!xe%Shrm6d<aRvcg5Fl$!ywWajtT&B1^7d{KRilua^
zW%BvU#s}HhzIbIvYI4chrOmlH*`AR(<=pIjuRE@$;hb}#*XAqT9r8neNcQ+CogVg5
zkV<8H!mHZYaY@Je)Y|pSR<*C+lv>fTX-;bVb>Tqa>NOivt2<Y&Ji@Q_M4dUQ{%n7y
ztNb8KUDm$-;!E4tpWW6vJGG=^X?y3&iRR&wl)~q1YUP^MmyFk%lj_Z-`-%s~nrP5G
zCzTl)%oft+$MKO$D?=S2UcO7(dwMcGM=G5B6LWa^?0qYat!X&*)abRD1Iv40VJj!c
z9k!$|xR?`9k>9=5+<VN-nufNv=-x}FtmeThE*>Ft%n?_3QibPR>w$Ft)CC)VDVkXF
z*&WAJH*9K)Mk+V$_-&RRD&;Hrt|N@r)pCU3v3r~9YsxqEzS9m|+4vn_oP0jxuxptp
zdA^GX-^eC^rQ_F7`6|+>#Vgk=KFaQx@)egmn3F2MQl>U`T(NQXf*I8fD`q|4g-=Xv
z?20FHGuKY9E<bQKO?iA}!netR?nATnM|_Bl7oJ`IZsvr!+1J!n)^6_w2i9&|X6JCe
z&{LdzKJBozn<zQ@+NIh%H?CQ>I&8VBWA(;4sqq$-6&PQ}*$bw<&_Ya%Gqw;Dx#@dr
zYR7i-`~&ko_<~o+=S#&Sgp!9{#UT~S7gIjznzD6U&YgYWYwykHdNPGc4~N+u$rswh
zhYT`yC5Hs-_8nJK({R>V(KmD-enC?*wMV~*=iA4VA1UEwY&+};G4>Q2d-Xn8apD#H
zV5F2S<uX&Ba^v61lt<?HLRE$>f8$4ZcpdqXHTgc-JEyAh3$X931B+6AX(%2zsXlBm
zD&gl_h;oIoJvi|kI{4*|zh_Ol*l_xX|5i+X>JAn%z1fkm>9$l?H*}o(d|RP1yRi*W
zIjz}yZoH<Uu`#;u)Pt`O1A{}Q@t5C2^NGV>A|{oVFZ+3Z;3`$8Jj@Q34HYwE?|lca
zee$h5>FvAH%k0?~C8`@XG(O+8PM+D=B~Ol2MIw>J-b-RN4NXnaeTxo$zmwnk4-3Q(
z|GuaGm2mX${ovm-lXmwZi#7Rgi$h~mZ=hnPSe$fQ%-(l(w5H*-)1q%|KX4^uZ=VOx
zVu#Ir;{7!BqJMslc>7JfOHYkXdY3=a+rpM?cq<;^<zv#Gn7a9oRO8@_^`smBkeld0
z!^W0LEf4ua>Okd5cYpaAQpjZc299<S2VW%W!_749>W3r2`-4@xpV<A*-Ai{LyKDEZ
z_G=!vCLe|Y000000000000000000000000000000000000N}UI-r8yPk*k+ACTnY>
z(dt>XTS}$D;`vQY!^6XK`}3pOTrNE~U+8Pf3^aCbXzI-tGQIhcrh#nVmQrqKIbSRl
zh6b9JjuoU<j}@ermJ3qrhXzt>(|wt~LVj*Km&=ak`{$NBu6iy~mE5}jv0I)VX}$cC
z($?a(ONZAi?yjqk<RgvAWNqa6W^&Xy96CzR*dVcMyB4=+HgB9)>d237+m@`41TQ?u
zU?IOX(_Jba7_Du=+~#!43k~+@sxy|YS<$(0Rd(aLt&4{*sI8Bzi$rR}!D=J3l9hj$
z{`~U4|GwzlGmH5`X;I6(GyBsc>odinT&cLIrFr<g{L+r)E4vz(G;Z2^OwEkQ-kpaw
z@6G3WGKEEp*Q{Bws(t;64PC1`R(GB`9_?JcblJ*|C0z$fCki&MS+jC+`}#At4`m8F
zH<oA6GCqUWsWUh;Gt!+K>dCAND`qocq<g{go(&5(t?itD+2w83^^x{NCsgTX;nZH1
zbdSy-SUI}0b8y|JwN>?z&PZczxWQs!qvzjX>CO3}(qW66h}<yTd~x$|>!$9mm948f
z6ZMg<7u#cZexOvyZXPOS4(m1^xuJR0CEJ#4>AQ00;86N?@%l*ri|uw|Uvv)@OZoon
z=;6J`A~!6)WX;wcmoHe?H(DxO7ORhRS9(vr_};f<a)XC;9J!&ZeRxUh#?kin?C91_
z(fUZX(r@Hfm~|#xlyvS0U0*zZc<Ayg7GxJMSi7!1d_Kg3dn3XB3;rcc0000000000
z00000000000000000000000000002sH`#IVnn*NS9j%Q`i&Q1I?tko-r>l;Q)kKoz
zSlw7Cc5T%$(V9qYv^rUvtb`t2bw+);^`4h__5c6?0000000000000000000000000
z0000000000@Di&lANIV&lK=n!00000000000000000000000000000000000fR|W(
z_=yIOM#BFA00000000000000000000000000000000000007{3!ST`R$g5(tk^cN>
zHkV7!%@_J2H`Ij%(cmYM@P7aR000000000000000000000000000000000000QlW8
zBmSyne||KZ%cbY$3w`xrXy!G47YY73cz1AB(7gMnyZ>(YHM?KEJG$%hyZ(6BYj?f!
znjeP600000000000000000000000000000000000006+Rv2$ilk3=KUXn0y(Ju4oI
zMXHip_dmF3`9!+u+?Jl!EBlvU+}3g4{29|D$!IkBs!HYB*!oO&W}uYXnC>f%RX^|c
zvGl^ubg_MKYe#1E%GT9wi|Q-QYTGK!YQ~%8cBU5R^V|B<g>9Lhv1aGa8cP@6zj~x=
z{?<#^u5G<+pyS->W6ft(npekH<%^}%W!Yl3G|{fLFqY15?Oivtq+|Qi*3QjWcC<~a
zw2S^;rCn7`ynS<isFdm)pX<wKj-@AV-&kl}*txQ2aCtVfctPD*qp`sfHL*)FrPQ*4
z9r@gjiP4T-IF=s&{!N2zncmLs9ShbhSaCseY;Bsx7P98zp<*fDpB>GlmSzi?-u%c|
zqv?M?md>2N{@lw3ms~z_X{L4gk_&6cF0rN3I979MCO4Q$5A>uq4wdqSY&thS=fwEX
zwLfayux|5??D9fxOR;!<&DeK%&RFYMxI&eGmwxKVvB<=A@7S?&{_0F=)xf~QwuRN<
z$jNAOtZh8J+ER9)FSVhRE=(-%*Pi|ASj)&??r6Mp!@!Ex)=fh@mtR;_9yJ*qpL4u(
zpqMYWIq<EH&H27t-!az9;|qrddb7DqPgmL8#8~SS#^-!s4^7k$(^44g8h3uYQh$7T
zV+Y1aZOrF$o706UZNm*w8Vgt2E{Ijy*3Le(?W)Yc(A0*DmiLFTi9u&a%MEL5>&F@%
zI=@0bUz(^IR*6&v_eO%B2ag611rLO$00000000000000000000000000000000000
z0002+n`&BB{1wrmf!=H`(^FNOh@TKGrTdDBY4I>z%I9;N(}j3#EDZN&28LqEX#Aw)
zU@qOADb|IJtAhI?!IQz`!NbAV!&3kN00000000000000000000000000000000000
z0QikIE&ht=&_HiCm+7f0pO^Ca+~#y4UK>9lT1xj76Uq2V$-!K@J5wx&`ZEJVvHEhu
zJumUZ0RR910000000000000000000000000000000002sC018H?0JbN0RR9100000
z000000000000000000000000000000FR}XaVb4oE2><{900000000000000000000
z000000000000000c!|}ApQ`P5M0Q;<`q7cvp{v4E0000000000000000000000000
z000000001h-yUyHRV5>PPq<;^)aqGVN~OW#`Atpz`O$o#Z*F(bz})aOo6Du=4rEGA
z{h6L@x+&Y=*VLOWWP0->O}*(I+3x(n-0*NwYjaCmWAno6*RH;#zV76@u<QC|mo8Z!
zrpJ^&wbkMA)So?<jznV5E?d&Rap50)_^Z2C-nadgH9!8tf1Q2)qnE8(GyMADUBM5t
z(?9jj|NDkZ&wG9LM;|+X)$4D`eE5AAe)7aKkIigb^r8FaH2$xRSG;!KZQJH{e{jRw
z3&XE}^V{Yf-}}oup1r$&)Bd0Bf9~0D{@Wiu`Q}H0vpzVq_U#Y6di|e#egD9cX9wSY
z`L=tXx~Jxe`~K|Q;`S{c{>rmAb*%Wz<)xmdpMChdg>NU$*;%{u4`#Fve<$^!$K$ap
zKUDjnwwpiw@h3K{di)>mUNWzH`P0`g{_cghzHQ|x7nBBD=U;UEC*M)G<b+eMz4w!^
z9$E0t@2y=>yYBi^uHIH^KKZr(cjq}zKlHMj+yDJDPrv^ycm2^_ca8pi%`L|zAO2L|
z_trgd-KNe(AN|N9Z|ZKZ`IA$(-t*M0Gj3{p<43pt+w9z)bMN~2$ig?KM=q+_c1q1x
zXV3m0J&EtH-+l7C>pn5~g>7$mbngX&D?as>x7_mQpMB({=@<R4pPhU2f@PcMfAG9_
zes$pu4-bx9-TtOOp1pkVq@f?*mwVHf9(wecTff+{;HR$~_{7G&&t37B&-A|QpQ|r?
z>j}?ZbH`indf)f1y6y)zHvFIJFKupXx#%;opLJ&5`QfwgdH4O--1oK*{NUcFI)B`K
z$^EbU)O*f4{esg^|EF{A`Qv!Y^riFP{FSS^9=J93(dOp*_x#JpK052Xs?~q~vD2=2
zSKUYc`S-86`lNj~_0IUmZQnjK*D$Yo*{AD|o&JWunf{TD({I^0<0Ea)UiHNv4(9)`
z>1AI$yY|`d-}t8=pV4)}w_4siyyU^{55D{rgFFA=j7#p^``$HoHNS83iOa9L^`TEB
zT0XP?m=$+_@APla_~7s{qaXQv^JP!$-T&2x{_byHzvF@GGwKqnhc6qsBe?x13y!<@
zs&9Syy{C;PfAQg;Uy^?6_pg{yJhAb{yWaif```EDe?2uf^sR-NX&tZr{$G6ft!Lc6
zJN|~#_N7yAN}v6nyKe10@oUXDyszz@e{|CH<;Q;Kl+({&@a;cq*mCXPfBeB+@B7Xt
z3vc?sY17}-KfUSnlYTke{2ynZ^rIhce)X4MzjoP@ukOER<>^<>nRVN@=Jcf>%v}G-
zwm-h<op;``@t(^YfALh|O`n>6_8Vusf5|8Q<hsW`5Z`~#d#+ou<P&eXZr}B1zx}${
zb(bz(68q(&Z`&O`@ju^s^LO`l{h#vPx2$9JlD}Sj)xK<hx-Zi-IM5fVN<R6I54`ye
zJ%g9dUtK7!>E5<<^o_F;;Vl}!{$=C0Xn2PX4-X%>HN#uAskOOzep7$CC(}4I*f@Sy
zHnlaj%xhV&psl&Nt+{pH{1<U&MiPDPtJ))xk9>YX)m#2O%%4>1xT3Tn-&-0^7c!Cd
zp8V!aYFT-%>oe(|o!h^VxiAumeyw~vk9>3UGtWJDQG8Y;wdb8Tp7+Fe?(BQwvB#eI
z@P|J1%y-`Lwx8U2{q<i6QqMj3qksOZzx~c_!)spt=g~;?o=72Dd((Nv&wcspUw`V(
zY4N+CYkSS@b=N%++kWyfx4q^kf7$t0JMMbm?&jA-_Fn(4w!T+apZtr9UiO#AF8R~=
zPlE2+`tP4Jd-k&piR<5cQskJ|OuxVRshMq&&p&nND;}=vzApX4s~S#vZf1Rb)hl8<
zzVYqb>YC$`SnY>C^}ql34;tbR?6@)d#EHk8d-wFwnt!<My$|i%S(Dmx^DVWDpBb3m
za@Cm+q<ZeYto`olGe1-JqsBdveV_X7lPl-dKXKzzpS!B5Zr>fz<7;Y~o9~att~>VO
z*xFCDeCxJ*?~l%_e<W9Zd8+!;*S+_ZCvQ06HP^iSi|5X5>KnPUZ{{1m^o@^Czk1I#
zdm^#hB9Gkkc<L4B-+aqWd%yXu=s$dP&*OW3dFSo7KXcI=j=S+M{^S>@ruLlp&>dIb
zJL7*=wJ%BSzoc->%{PBHn6-9K>@Q~i)57CVeAVYZ`_Wj(qR;<45>5PJ)!JJ>{hI!r
zZ+`f=?pyyL_162p8mWyZj(K-<{loiuF8KV-@A}-0H-7H@CtVwj)xM`;&lg6Xy!z&U
zx$NSnZ~pA-cD;PgS0exMxiznQS2DGyd1Yef`{O6S<K{h8RWF<SFYPCWQ<(kbuSBBN
zk?Ci>{oc=R`@)=~@6%8J?c$cL%eSxUxMIipj*Y9{I4k~N+NZ5k_USzP)IR-h-lrdX
z*QMKf8}r+SbB$ZhTm8mavH#LOZJDx9+w4>O^uKwZ{`IWQJ9<mY&fC1A<;r~T8)rrT
zOZ&8W%06wePwmtH=6!nW1zoN4a_z%gE*<IFab<aDRqg(GB=~F)1h3xxY<LO)00000
z0000000000000000000000000000000KjjCWr@8fM7NYmgT?ckn)>sj`9j~^?w*0U
z;b}IPOV1t1l$!c8J=t_qw!g2bH(SW`=0}=((>t==`GL9N;iA^&mbS*`g;lRf?2T7?
z3OgDe9zM`R*ke;`bMySB{&Y{KacHn{ym3=oW6Qjj1q<4mo7<XO=gm+2${n;$>0n;`
zSMH!?N(XJRi^2|)ze)$qQ#xp=4;M5R+!G1D9UcGx0000000000000000000000000
z00000000000N^)RLo6Pdzc!cd&J>$A^rs7@#rgcUFx_@qG#**Dc|5bWkRQwxO4+bd
zPo~&i$PSjW`GLByeJuEgNbqmr0RR9100000000000000000000000000000000000
zev_RRUzXgQ&u{Bb7q+b}<Oef_QZ`d;>d6$l3)#U^Ha~D;e13FoF5MkQH-xoHi^prn
z>dWo-yu|Yd00000000000000000000000000000000000004lOSbg|O1a*<%?ZK8{
zes}@^000000000000000000000000000000000000Ko6K*@<b9Xf$$KZ8Tb4JtMY0
z)14V8ZA|wS6Y)q@a_jyFFHDT5)h+Q#)s{-t`tho{os0ANZT;!OwoK1NlZCs+)8O}G
zl_syMG?^Y-l`obq%NDbxgLUV9dp!N_*68#|G#S}DRyp3@)05gf-gPlm%BM<Ovc*(y
zKG%~el(WT5CJbfzQ$5+@U@pBg)04^%lq0Fd2U=~+=X0CWh4Hbz_T=A;rys8muRIa7
zM1uQ*w*^;)CjbBd00000000000000000000000000000000000@M1J35@%h|(4Qae
z+MLgC>rWT96{lpnvZYLacAzISGCvjm*A!2jb<y)xA50fA1Eui>ZK;;?Vu`ahyiotS
zbg|T*@5%ONGd%~#YK<n&T6X;NH0>M8_H+#mWVa7x##^=2g_krf_*^7-CU`RVY4CXP
zNbrN;q2TMmKLz)NVE_OC00000000000000000000000000000000000_zhAOk4CGe
zm5+(4@-dz$A7k-EJQ<B8;*n@w`H(ChYRiY3@}W8&k0k5D*3*LJk>EdrUj#o1z8!o%
z_;~Q1;QF8xbcJC600000000000000000000000000000000000008(Ey)51s?JMNx
zrgORMXuf~$@bK{5X-5jy9VwVRQn2bs!Nie*@goIe$HyC!hc%B!qP68iP5Dq=7tVZI
z@Wn{*Oz>pz)8O&ok>CfxL%}zK2ZDbL!vFvP000000000000000000000000000000
z00000@Eax(Pe!Me4|U~3G7+zh)|L-7<wJG(P*px8%7=LQ5R1oalXYRsX~7+l;FrPA
zgC7Tv20skG6MQ51r{GJ$Jz*FC00000000000000000000000000000000000004gd
zRmWq|$PHCvNn$LCk0r6Hcq|!~#iP-ly2>$`h}TAI%ZHlsp}HJ=ZdzT~&9vZSk>Hu&
z$>68K<G~}r4}yn+Zv^)RcZOjA00000000000000000000000000000000000008(k
zH9cM(jZ`JK?tko-r>7mv#ICJ7ka={~8OcOE60I#CYRZS|@}a7HNR$up@*x(FN0N16
zkMZDvNbu9(2VnvL00000000000000000000000000000000000006%=W+bB3(P@#Y
z<ktO<-STw2lC2xd#;%RkhuLJZl6`d58FgX(c<_@*@J#Szm;e9(000000000000000
z00000000000000000000!0(2tcr+S|C*sNQ7_Uv%g`s%x%}DSc!M}wG0000000000
z00000000000000000000000000002|mYE)}jz+4ITlYV9%hU0Lnb@_lY4Pf0CG+U2
zGwQ;cGlO?Uf?ozd3myx87(5g_7~CIxF}ORpJ@{ns*TDyZo5NxN000000000000000
z00000000000000000000004MtR41azXzh$jQd3_^s;5_ys%e!ZQCCUg$x0Hdtx6=r
zD%FWdG#Z~-Nn$l)N!3`A7)#<~NvtYNlCipQsF}f^MuPtgejYp#JQ92__-63c;7h^X
z!R^5(gTD?w5Zn|N0{{R30000000000000000000000000000000002MOQ9wajYezY
z)5g+RU3DUwjMgN>q&8YpTgk<%E4ldeN}X7|W-N`(tdz!PR7zv@)rn*@8n3D(u|!oO
z8MX|MVNoJcTbry42b&q39SMFG{4jVh_(E`7@ZsR5;BCRapcwQ7ox##zL0Aj`00000
z000000000000000000000000000000000#;64B~tZKNu>b^n7GcFw3|YsRt*@2{_9
ztH-kQTc=mDRb$x`w@<5N6Jy!q-(OeB#>cWV=O-)K*jTpqN452dWOZ~b7x~NT`b1<R
zw`lpyDY@9BTx%gdDfjZ3H3xHFdwgNl!Q6efz9TU`%q1ssb>X#5+jVm!crf^2FdQrn
zj@$jU-8b*vyz5_g-5Z7h00000000000000000000000000000000000004e%oEcvi
z9n7V>Gv}9f4rUgeduB0TC@pH4cV>TjWPPSMlq(e%wKUhB5^o=`Rw-XNwYd7~#NM6b
z<-Pe_Po}VF@tQR&R<*BRv7u{K$Lh{A$D^IAmo8h`v83xj=|sWCHEUKbZeM@q_MuE+
z=f?6(TE=J6I(4WsGb7!(p`OgTuwph7MrsztgXor0X|Q;HQ`7M9@ZA3VXf~Hi&&?P5
zng$E`t(oppv8gv($n@q%n%Wl3ZBDn8v&B+jXrL)MKb}8o>xo7K*}g5M+|Glo>Mn?1
zebiQ&fyT}aM{hf=Dc=1e+iuC^2Ag8@;{7kOe)mwZl<&`uW}4zH@oe%%Hp&c?3h7)^
zW4!AHhUw0SHM5(CN|~m_+40U7sGZ)NA1XCf)rEU(TCg+{JP|w;d?mOe_)zfY!CQiz
z!It3iFbn_y000000000000000000000000000000000000KbA`;<KV7t(RX?+FIOp
z>F}Dx)5?M2^YTkOmapt;T++Cywi0N*xOupBQ+L<O)>T#IK=*>>JsTEoTH87Qvde4A
zfv&~vnavyLl{)gH+v+NT_TeS18%NvQv!h#M<-oEvD>@gh%5Ge@b@6br99YslI)7m0
z=+4f;b(^YZ#%Cojo<BTv`4tPYix;e2SFW)5k~Ld*T)tpk-)N~2F9(`eU9xS-mcA=@
z4i2Rgb>Zx12C+!+MDS4X<>2<<uYx}dt`A0nzTom;X)r%HBRD=R1^@s60000000000
z000000000000000000000002sx85=FS<xLkR?c6YDXkh9SlCum2{c~1VPHjT>!zWd
z%d5+Qp@H6PF4NOhR#gs^^7-86bfFT6R|4t2V%NCD%=oNiA)hZz1j-fqGXp~t`B*v7
zyKZPn$M&VIotv-hm|hN~i|vD3J2InJwytiQQ4VYxY|Hd^cJElQX2FWOO5og<p4Kb-
zmtWl0ao)^wU}K?mVdu)8!R6V^;%Vi;`g1QETypuyrJ2^{OOoZl>XEMbTQ6O^w)L`s
zj{0(-al^XJJF?3Qxh=(FZC!Z@_PoSj0RR91000000000000000000000000000000
z00000yu|9lPa^n!B=}|U)8I$p2><{900000000000000000000000000000000000
d{8p+-M5ECeu|y@UkHstL^jNGa5ltrJ{~s!yL7M;o
new file mode 100644
index 0000000000000000000000000000000000000000..b43dc2389e4a17471bc01ae421f9521fbc2a0afa
GIT binary patch
literal 1179648
zc%1FsdwgA0ohb10%Ht$W%R>yH!YSB7rD>9ev;{;+X<AC(PYdb|r^!ir=t)j`&Pmgh
zw!5t_Q05Naab|SJ5xmzIBBL|vTon|#FoG|J8F55M@rBCBaPcuJDv!I*Nzx{53OJwp
z`QYRG{WN>+_4uu|_S$Rj?EaMv>sDq<nN)AS(4Q`)+9LZSu~_8dR4NjQ#KWJLMh<&P
z_>-8BkA^>|MxI$b()5FxhP@w+oS8c#vhU#DkMH^9-s05{?0MtvJ4b%Hd(X%PBcIsy
z@54{+`ta~ehyVA`zm|qeFDu?!+*o+HFtX!UJGutHHn<`G?R<LRO9NZ_zuo_;+~>k>
z000000000000000000000000000000007{3*nyXxS<|@goalkevjaVuUH$ppUD;Bm
zzu1)?7|0hZ=jl?Zkli{|%1qVbrI)Pl?AX|u+O&H4x=o#_<*S!;Zb?0RFR3-Fk8G7X
zyPWCD_RLL{^IeAm!lF3`W}H#exa{=kfzCsd80~s6m+sCSJ;O@nna7&jF+<0vH8!D2
zi(}0wHnXO2`B~8eGY<{AHJ{(!pDt`KcJ&Qqd%A`OvO9({N7tTltZ5xPetcesTBOb{
zH<`0DRnvIM%;<rW4vkWo^3)R;t(tkP(T@>1KJ;ky*to^hYZ^PkagRT8+@V5l>S0sQ
zGVBq7M~)g+&pB}XX*G=-!WD0yV#UjQCzmdk`tv>6-fX7l=q9I+&0wnao-%rhMep4{
zH&t%8YOLLyy)UV0yyzI4U@%?C43v(pIPDoGG9hM);YVxdrUvuHY$=-`n6u->HH{a9
z!#5mbMUF0h$+N7&GhKOUWH2-5K>S7Fo@<_X$=kBUQob;<GaJsT*wvGs@ElI6dht<X
zA0u*n{7KcRvny$(RXB@NpLrI&g?#_iRWEv$SsaQy#w^P9U6st71C6KDG_DF~anUhm
zQQ5x7UXW9tWh#@yk1?OaFR2OZv+v~abvbk5@{B!a<=tCpH)&R*m8Tpvonr<a`FxgJ
zbWIq)zp<wA!f^cLk>mFkGToVh?vW$PPd?VrliD9S>Uiaxt6o^sxNxfBDpTst4-J$K
z7d9Sitiz3u9BA~)DphkbC)G5zhhs&L9BW&?SURHYg~uA|P}?I%Dr9z6D(CdSAbhmL
zVd{@s!Sv2-cYffA;**ZKfD@V@wSJ>@<zHtjPONU6H7ok6QhIAHQ{0gY-vwR8Qo7VS
z@%#m2gKX?rys|SjvF7a3*4*4|&#t-U+?<22KB1=ZoO7bD%U8NP;url9*%K#qdelcj
zDwXXCuWDoGvd;CXwd<F!>R5kyYDMSeb5moV3x_IKui2Pdy=mpjDSlgzx0##j&-Q1!
z$}h6i=8pB3T-vez?6&!HQcF6Qc5GTX9v&`9DSXeSR<2pSY^>GXRBtZbS3ES<ctFeC
zRA$#;wvaBrj;E@v3^hf0`6=n>>B;m=RXOoj%+d984z4)9rt$RCqp!;xTHZqoTRA!5
zs3m>Y#T<W&{PC^kf#YV>G`6)x4=kIsnuo8rI7QKMQ?BrY2G6wC1L^+B3pVyqG`{3>
zI*+Svyu2+MsXVk}k6C)Cl&|EwrWmcOb&A5{4>UK_lppGYGY?(a*b`rzcs~88YZ<S3
zri%!_$R^&>v1_Q@igar6$~B9Ru{$Q+;&KObQ{_*})W*&&8|N&XUfsB2)-zrB_|!(P
zcsw^_?X>Fh3+M7ludht_H963IWVT_-m)Kb4+2wmPH_Xl1Q&(BL0~Z`xyX~2g;e4T|
zIPtvxsI?ofIriG6IyP-wvwU?JxvF#Z#<{7nh{_6#E#sVp_0P5t<Kv7j#CUGnftuRU
z-Q0d?-iJT%3i*7gI7LzNsH-@lLHT0JCtZ`aZtH?Mhi-dsKG%~eOn5oW=}bP`CO%@2
z(JMKku<qaqH8qWAofUm;_t6(LDN}pwi+HAeJn@whUdHyL-Vmd2vC&WO!wttj!4H>|
zvZY*R@>_1~u1tDmjxAJW*zz5p;^j5<D{JC&a$s&%<u|~=vkomv`J<tD=%o6n#i$fN
z(?XORjPAkl_t4>+JNBJ5;bOz-AN{*x;#+sHkm=3t8l7%yb#-Is>CdzkDzh8i0F~34
z0}J9cjZIC_gQp+<gcukcDvf>o9+^)Z{Sh&twtU&o@C&ZebkfW0aNSTbGy2_k_}V8v
z$`ii6E4|E_b5WwYaYNHHUF*b|jb8G^lB!4~k~pv|R@2zr96fm9;rl!BtN*Bi_|f-!
z^8XT!ecuoNe`dn&K4P&Z{@>!rvdIrnF;gr~cr4}|ygFLbICEz7^&N+<Wc2Iv@LBAr
zxsQLJrk?Y!pD7=|@o(wLr4zp8r}|phmJJ`pDLy_X?1{;nf2tOTKddJ__(wcMhXR{g
zCqy3cAE`r)Cp`V-cSs?V?Hf4OMI8Q+Xb3k`{nZaeg7*Zg_C2=mZTpt)JAUuJy&Ze*
z-;)oE0RR91000000000000000000000000000000007_*&VkzchRD^+o07G)(P;Io
z+HIxMVDbFs=HcPtdHwm_*<3C?FJI_u&I~kd+R)saEo6H0yP5~GecMX8k#fFRDhv%Y
zFCDE&tsbpNEiG51)(;J&)~5S1eTDqIbS{_Oo$sGl?zrmdNL8}$rYE+2;EI+VJJzng
zw7=N9xTU+UA(D?YC6l$0XPU_|=WygGJ)?sp?rmGJB75oL-3vGDYVF^iY={KUKFDAp
ze^sWtR6I0V+roJ*>DFf(>^C>xvTkwqs?A$=^=@B#Y10L@4Uu(`NNqS+ZDdxm@`vfq
zFaQ1b3m42T<_o0@ThE={pWd}TQyj{biWj!F6m~A@TsH8^-779#*mmH!n(2`PBS(h!
z=5sxn!i9_1tXZ+DWBrN^U8_1*Z<;+;x@q;&<tsavbRDW4uh_U|&C10c>u2v6$`nR6
zmS@m9HiP+-XD~ant2;N;lUWxw%x1!p&FgXtI@)txP2Jg*ZPg8tjw2^j>1NU7Ub5}!
z)hjQX-<Q9<ziDk%Lu6B=sW#kTu`uYFH&}XWeyDWR;wB>3wQnD3U6fruxTAl=(56H~
zr0co%*qt9J6|!50N|~d&jYqEAJ<{DVxU7H4s{SP%uZ}lF`k!mJ<NKm}s94JPXLldn
zdn|HY$J)V-UGsPE=-t+y-5hI(bXR&$KKI_YWpaZ@bsV`a-L-Z>=eof)T^${lT^?<S
zWGnqfev4UW!bM5vrs#U8qqK0_d0m$cHeH%+2;UF!;O<E9e}jJu6951J0000000000
z0000000000000000000000000_<eRlye1NjR!3`N^^vM%-%U?!`#{z4v6@J-Tvj()
zl(@I*xM)qJHd>vmO;(D2bMq|?<>>v-^Xvft000000000000000000000000000000
z000000N{C6S3c~2o+kkS00000000000000000000000000000000000007UkhVUmE
zJQ4~20RR910000000000000000000000000000000002MAA=L4)sZt}wUPe(?rbiX
zo|iB5MXsw0E26<qBH=#(00000000000000000000000000000000000008*oF+F}}
zvOm8&o6Du=<qLfcVbP2|ACClI3EmN08MN&C>AsKe+q3T#`=WdA+52aEU$yted%ho5
z0{{R30000000000000000000000000000000002L!_Jv8EfS4Hqv2_F^{jX-7O6`1
z-So)ik@2)^=k6_Qmld*0bBogL)2Bs}(P;9_O5@tt`b>9bpp@E}?kkQq-*obL*x-_u
zog1$j99*!u&~#x#C9Jls5>_)7mK#Ye&gZxHrwiLNJ)>dkTE@HSTDoCbI^R~<(Z6xS
zf@!1Svnt`$@m2X^DYZFU%$AOfTi3E=Wq$GMeEZ;4JJ&C3tFOdGUs8#ys)=`O%@37Q
zo5tqa{=jH@-e-od$aQWg?H<^&WMgJw-DuG0V2PU8vP>zpd|+ojw{v{7)*p{|uyA3z
zZ)<Mls?zdw>jlZtwP_w*$eK%riluyic6TPVG+W5@=68(-&8r>nq<=@t;PTwi&?OtT
zr(a$>dWo%-;8@M2ncQG1J<yZdI8@3PvgzE|oM$f@O<$I3>s)hQX3L^=%ezL}&aWB0
zhv$q&$HEn={9XFVBag?nKajqvZP8F^XXoba?LCXC!;zEG<Y;U>yxLNBpf9zdlr9`v
z-qW^?rf00p6!XKYO2sR-Eh=t#c~yDTWOQuK@l6B8d^zUOT^*hC58Cb<jh=qb&_HiC
zm+9#$o0k}kK51;uhxX8T`!FqqWnE*=k5}4{EpP157^#i<d~R#HFnO$XEv3=oO6<Z|
zCAN0Xk+G{X14EMoFOT$xW#fa+iIxLvYa2!bkDOm2pD&F!4Vy%&g1aNZ&x1#T2ZQ^=
zQvd(}00000000000000000000000000000000000_<dDh6@O85XrMQn%k)&$CgLYW
zOX<F1qCOrLm-6}C)^s6W8w-p3GXq1hWHf$Caxj<f&J^py;Huz@k>HoXqrpSL1K}wE
z00000000000000000000000000000000000008`6tB=1ZIyBIm&1HJ3%IBqgKDRYp
zh}XtXik8xS#Y8fGN^&rl?#>j;Mg5t9p;$vXaR2i>aR2}S00000000000000000000
z000000000000000c%Idj5Bs0zNdN!<00000000000000000000000000000000000
z!1Ju3eAxdyPXYh{000000000000000000000000000000000000G?+J;ZN0$Pet}_
z+5O>NwL@2irvLx|0000000000000000000000000000000Dm~%n5s%f4xDt|uG6b$
zZ7Y=qi|02t_vd%#3w`stdj{r(r`cRCJ#QdWYVObUWYf*r{=VkkY$4N|-__ik-kI&r
z56lY>7tL>JZEI>-bnV*J%NpuVtqZ$uSbph}^<jEk`KPuzJf8lur_+&0?8)UzIyNr)
zlUu&LcjXs%ytwAafBJ(t=RdM})tceg4DSuTpPlxJxBc^LFFo%y*&qG=`Kw;@-pnoU
ze)-2vo_%~~`-LC;;@qbHxpB*@&b@W}yzZMeytOd=nm4}X+!K3${i!GK?7#e}pFQ>T
zlVAVgpZ@ZV4+m%6G_>}u_rGHOUp(;Cz>+5i-}=h!cRzkt&0}Bus|Cd!+itn{$s0OX
zeEOB8o?kur(6<ZUOq?@PJMt&fJBGiN`rxDS*cBhF{b1XTpZv&U8&*B~k9RIPxBIeR
zUAy?(FMt1AR-Sf2X>fk~MJImj&+C?)blU6g{@5#aEqvQ|)-J4FckO9cZ!fi+`l^2(
zIp<dozTn1=fB*Ea-t(qA{>L46?EXJB?>!;;&?ox7v+n+DF5h(Fhd=c28@fAc{^Inj
z?t1+F({E^c{fDpm;hfz51$TU8*P_>_cU@Go{j{1d&zbYzdJ^AVzwgv{)O~dDbK770
z$bkz6SA614Z+h=vf9By+rd{+uf41Pph0C|L-*n#FzP#wVhX!|D-SLJ$n{(OVDMLU0
zV(txJc<_<q-v9a5g+G1qz(+S8czVm5KHdBFuT;PM%_lv%=TmRK<K5r6@|y2m-}n#J
zU)b8#deNt2Kiiaf+bw6`^^W`YeDN*s``+D;Z~Affvin~BiFclL#sz1b@h|7x^=I+c
zX-nJRc<+^6_rE{(;g*($cmCVoe|Xk;RjdE{?`Lj#d)<e=^0GZwpK|bq-s%6e{hPCM
zjptS`|764Q(_Z^`(>}Cu+Iu%n|4`eLSAPEcgZV#ge!=I@u6^>m*Z<{5rgvTNjn;P!
zFZt?@ufFg_gCl?P(q*?Fc-NXcTHd|&<jbyn|AQY*w0`=j<5t}Hoin~U{ifmLc7N!e
zmd%eHc<Rd!e*EuVv-AGym)0d#4{zS}so=JsEIi@vE5Grjcg@_L{KYLlUzUFSWm~2f
zPj0&Yj(2?NzIXrl2dC$TzOg7%-}#E~{>?3Ke(7!d;;)@~Fr9is`s{b!@&4YE|Fz}1
zcelOmf1EPyvg5yX+8O6B{N`UZZhPJT`N&uIzWZArE4<--GpD_we_Hbyr~G=j<v-6p
z<wxJ&`id{TX6^DNUw-PYm1kTrch;@nnA?~BYUbL9xBuA<Z@c|d8}E8$(=Q$`yx|km
z&VK#$_bmD7UtIGa?~6Zm*E_FSvgD(0y5``uXTSBD*fp0fT@w5CBX8LkJ^43pzVX`!
zy8fa3^eyjPz2t8fUwJUwpYF>v4-WK2s*?Y?``eGcad&6W^3E06((q+hEPef~MEHot
zuYJMTBN{%T!^6Xe9?kGkZJyuK(%#&k?#VO_4K|HEmCbEUt>?BbT-e&aaDGesdFMTc
zCo_`h>sZwhiG1jug;j6*_b`7-sdGzdL%z2(oGxS{9X<K2nbh*~T-RsPJtI3lmw9<4
z68+cm<GkzZTc3FP>5JmCBB}jvyZ*e#zIA)wWB>6VPu%js4?gj&KYz<lZol^0&jqQc
zzxt!E-2A`4b?fk&7yflL61^)@h}Pb4Uh%VEdf<V_Z?BKv`E=XGx7A(qSZv3s$K87I
zPd>2e=AC!ke`m|ZkptJhy{+#R)u;aAq8EJN_$7ZC|4GnY+wk3U=FEArF>&p?PKg|M
z@wEF|9-q+`x##iQU-VF2_ciJ7U)gxd(=!?xs$LY^`L%D}TGtYf#A-kIiU0M#zSkJP
zf9Lhl$4)+O!JX4~*ZkwH?|SgyNKI=0jqj~p{KUYt)+=Y<pX#}DbH|<4vp-$;qo)0l
zgP-{JFIS%1@YwZ_fA-4ex`UsJo>)`U(sExkcFpk*#nyhb^&7X|eP8t4hKF<2uS`{c
z@|t(O_|y$2UA*UopI<Pqxo_9)eKTJBg|B^N+SU8_?2p85jXZqAqp262f8%>^IPmpv
zME~)_`ybu^>)UU;?TL$Cd&2d9^B2E3J+=Sj2S0W7-P8YjRmYOlQ_BkPz469x2ea1h
zkNwSze_3?m$!C7{Gart1UU<*XBhkd4R;_*iCok?FdE-MTbie;kQg6QR%aPi6;<$H2
z*FSWy=Yo4~eEVmwzy7oDIpuZHSnWF-_kV8JFR#Aw-!@<Js~bP_>b)<VdvD}FKfC7D
zZ%?N7x2#NzyeEF@pWnE@s_F&v{;lKWa0+w2bZ;bD9hr94TkrnN_Rq~d_CCG$Yj4bt
z%wMpnebsqu&+Q#~{jB(ZX`jxYv`?SQZxH*`KK*U>=~sTZbU{mJY2*C1OP97TfBmf3
ze`%k#PTHpn?Nj^o|MNb*@3UR6T)ed9imtUgOP5^r`dQKc(mrjOv`-h<r}pXp=Y9Hx
z1)YVhL)$ZJF5j5h)?MCNRr@{?2|g19!7KJX8J+?F0000000000000000000000000
z00000000000Py=^dE&rH(QT#DVDbFs=KlQde4%e%chA7Q@HCstrRNP~O3nS5o@}}~
z+uzsRn=NE|^ShdR(>t@>`GI-i;iCC1t!+&$i>fY89EewX3OgDe9zN7V*kkkjmX`MB
z{&Y{KX=t!%EV#L?srB5}g$rBT7tU{KKkvN6Z{5NCNgXVT|JEI}PU>J`?4q!P<ZshK
z%cKq#G=vKp3+{>p-wY1`00000000000000000000000000000000000008iNtT7gk
zw6D#jyEDb+4gKjtX>mTkJxsUHjK(9&w~l4j7V?9cLMa<I>d6$l3)#U^Ha}1o#>awx
zj08Un4*&oF00000000000000000000000000000000000@cV3Le0g$fKEJ&`UD&?1
zkRQwxO4&@YxhGTXE@THw+5EuC@%HH2T)I0f-4M1aEgowfYbeL>f1c+L0000000000
z00000000000000000000000000002bvxe{|5!6M3w+7pS_V5G%000000000000000
z000000000000000000000DwPoa}xEDXf!gjHX5z2o*rAD>COz4Hm3WEiFl+c*>}?;
zTRuLPezG-QY1&$8+A!8MH?laN-`<}tY|r$JhipD`ES>eTSS94lO31X>s(i7uIa|z@
z4!7O#_ObM@uZm8KM3a#NqmAPoJw2(dV_g?hrF^QiEn7_W=5sxnLOEN^WWu6Mf2t>2
z9L%LhGCisMK)EEf_)yfwd_K1|T^JwhCCRb0z9GEw6ZbSnf=>r;3RVZF@B7KV&+L2a
zzU})i*?02Z@9q8Dy@kC?_RiSz<vs7(vm<N<000000000000000000000000000000
z000000010z@rjAH%TMgj@9sME7s<N{`FyEZ+>y(cGF|DRQa(G-liAfZMVqeH_EhV<
z6B1`#aLT0G$xXv<`YT<8t>>raw;Z21>!N3BKbS6L21?@*ZK>Atk4v1j;n_yWrHiHh
zd{4GFo9US_THB1oS<6p)hS<KLY){wFKz7GaW<0EQ?)1dzos~t|mM<P-DJnHxt*zno
z8WPQ`8Y^YlVkuu3*;)QO@WrDFx(0LU?o3a*G&)dtVX4-(X^G}bPJ9lry@h=LcvxHg
za}1g|)||S;%nK?DQJF^Z@L5-HdS|vfKQMavt*QC5lZlz_Q#UPN^?17lXVoTVE}Xhu
zWdhy#p@CAl;mc|gGcP>G*o91YW}tgyywkSk>cq@tQ@1VO;!4MduB#j{w<<BSW9opR
zLT=Kn4HvvAk(hbO)UA)&3FUhmPn@%^a&OCftk{(v7|0io$X3qNrBWfgb!co}=a%p7
z{8(bvipp&oTi~fT(s*U%VY)av{y3Jq?mE=#)S*YT>1v&yYOM<&>iXcbk>H8om%&ei
zM}voh?*$JA4+Q@bd@(Er00000000000000000000000000000000000004mBBUSNe
zw5q;*OjMPR@kIF;iznjAXe<$rMC;0jWcg5AKGc*C)$w>FSr<mv2bV>H-vqx1eiD2$
zxF`5X@Xp}cpcHh4#Q*>R00000000000000000000000000000000000@LPI8yeZmO
z$j?jXa@pPa{&~a0!}IE=Dy*BTFgaCW)l`LvsS4v$6~<1CHzkh>k4K`l<wH&RP+b?!
zygvARBzPkDW$@GB(ct0Wd%=Uj*Mj?le+r8M000000000000000000000000000000
z00000007|kOd_6))|U@;<wG(NuZ`B04>jdOb@@<LJ|xPAc=-^E$7_>yVPt*qsYvka
z;OD`QgGYkz2j2?57W_-_h2XBR7ytkO00000000000000000000000000000000000
ze)m<!W6{WURijB_G>MNUv8s418P>(4(Vn`>F`0<hMr+H5n)0E#T=;Z-UD!>1@b{76
ziQt#PPlHE;hlB404+dWgz8Ksd76SkP00000000000000000000000000000000000
z!0)JO@#<)#D%p3_6Wcyee>ju4x9(8pH#gssOvEG6+VY{Me5fuTs>+8%`4BH3V)1w+
zSr_&g5AKfyKMlSYCIA2c00000000000000000000000000000000000@CRdhB3d1-
zk5nc5ZhB(d2jZ1%-Doy(Z>%BACX<!yZ*IP&E^HqUei8|u2!0tR000000000000000
z000000000000000000000002+$Dt}7jmF}McrrZ3Ym;?hQ9Ss1B>2zZhhYK$00000
z000000000000000000000000000000004hrrp2qHk*Z|hO;2q5K>Tnfac`_XUY)FD
zesl9Jbz#dH!P_IjuY;cj{}FsYcrf^Ca9{BG;LhN-;A6qx2JZ`Q466YE0000000000
z00000000000000000000000000O0vioror*wbLs}O+zKAo>ob!>MKd2u9C!)l_XYM
zl}Lt7suPiDG(Mw}#A-&9s?j7dn#4zwSXG!LV|C$BGlIX21iuM>9y}I29DFDEdhq4o
z3&EYiZNbNazYX3O+z?g+00000000000000000000000000000000000006-Ap(YWH
zMr-2rqiL+JIuT7qYm#A78?C9W<l@zpTzp!kO)OqBn#N{SYGczYwXufkL^2wUS5=Z&
zqAHOLBg11@l}OaqChNk%W&~$Pf}aK7555|FF1R(gCAcAYOK>nK20g*1U}>;0tOfu8
z00000000000000000000000000000000000fQspfXmzwUQkCqx>5<DL(<|AU(d?#^
z8!FlA(d@dGX_ai%X!g9%)K{{J(QNwzb(L&<G~4>)WF;FL&Au#E+mJ|BM@MsKtgUWH
zM8<QMM`ldQ#U|v|wZtdn+8?Nykehx_)#2O^+U`qC3v<cwTwQo=^?PrO1YZqq3WkHl
z!3q2Rb>EHqw(kAG-n+wM00000000000000000000000000000000000004mB8MEW-
zqJz0~cjo-k$YAEe1+$C!Lg~WRb7%LbcdgG9hjOLjg{>{Mr^P$QnpNr-O|GuKI&olR
ztiCs&>&X-@T)bw@id7x!S8V87)wz1p?6J~KtCucc*}0_aQ0;id#x-kJF78-Ad&f|w
zFtV{clh(1B%%42e?98t2+)z(uUDz<22}^1&j0e$erP5&W{O0E2;o*7x`Q6!EE<G<_
z=xZJ<<gdzfmx|53*+Ql_zpJ@z;k=e~YdKpi6@~_ylkM^RF{8(W2C{wIO1Y84QFRx@
zuRdl}W}s=)hGWOpH^;l5W9+s}Zm>CaZoL0Fw(lM)mh%1C-I?ZiYdo8LjzO7$QX!pd
zZi;t3%P`&fuw{1ZP$|=#I6J=SSz4#J=7&nnRdwMWs}Gh&g2#dfgD(ZQ1vdwO6<iza
z3i^UqhQ$B?000000000000000000000000000000000000PtHlE<P)|d!)N#a9RJ7
zRsBmk5;Nknl0zM(h1<^Sx@@rN(rmdwVds+0WdpC=z2ee^ZMEfsY<qh3%FE{W<uC7V
zsxB9#yVfq~TsOF;tE1zxSh?VemK{6RuD-Ot*t@u;zFe?*U2Z`~d#<agJG-)`T(E9&
z_NvWWcJ*#wduda$T+p$0aAVi}-8*`>wP)kyg7)nrt&6hD2Y2*u7^*54v@KYXy>#*J
zg&TIY_Se;gv!4;fBEe(9gTa@A+k%^ezY4Amb_IRG=3r^i9=tR-F{}mv0000000000
z00000000000000000000000000N@Yaaq(Hv;VW{T8%nzewk+A0sV^4{4fJMnnVzn)
zs&YXopU-Vg7b*quN<q4>*fl0GBR(rx$mdJr1?2{VOImhrylQZ8!RA8K^m0M}j+VjY
zxuKy;Hf&EPD+QT>q49`Vxgb-_53ec}uh_PzxTU&WkiM#I(NJk;=jQG0JvHTmuARHL
ztX)>fF3l}U&!`kE-LNd3Z!7HR-?*WnT(D$ie(~yj``}eO*Dsq^E?BrQ-M2Nja#d-0
zy0xxc(AK%;yv&wG>y~$owAI#?mtg<%d<y^o000000000000000000000000000000
z000000N{C67ycxI??!@O2R{vd6rKP800000000000000000000000000000000000
f0KgxlnnW}jogPb6(uP>Pl1_`osuIy;GXDPnRw!H!
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v25.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v25.sqlite");
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_dates_rounded() {
+ let root = yield PlacesUtils.promiseBookmarksTree();
+ function ensureDates(node) {
+ // When/if promiseBookmarksTree returns these as Date objects, switch this
+ // test to use getItemDateAdded and getItemLastModified. And when these
+ // methods are removed, this test can be eliminated altogether.
+ Assert.strictEqual(typeof(node.dateAdded), "number");
+ Assert.strictEqual(typeof(node.lastModified), "number");
+ Assert.strictEqual(node.dateAdded % 1000, 0);
+ Assert.strictEqual(node.lastModified % 1000, 0);
+ if ("children" in node)
+ node.children.forEach(ensureDates);
+ }
+ ensureDates(root);
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -9,14 +9,16 @@ support-files =
places_v16.sqlite
places_v17.sqlite
places_v19.sqlite
places_v21.sqlite
places_v22.sqlite
places_v23.sqlite
places_v24.sqlite
places_v25.sqlite
+ places_v26.sqlite
[test_current_from_downgraded.js]
[test_current_from_v6.js]
[test_current_from_v16.js]
[test_current_from_v19.js]
[test_current_from_v24.js]
+[test_current_from_v25.js]
--- a/toolkit/components/places/tests/queries/test_sorting.js
+++ b/toolkit/components/places/tests/queries/test_sorting.js
@@ -275,34 +275,34 @@ tests.push({
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 2,
title: "z",
isInQuery: true },
// if URIs are equal, should fall back to date
{ isBookmark: true,
isDetails: true,
- lastVisit: timeInMicroseconds + 1,
+ lastVisit: timeInMicroseconds + 1000,
uri: "http://example.com/c",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 3,
title: "x",
isInQuery: true },
// if no URI (e.g., node is a folder), should fall back to title
{ isFolder: true,
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 4,
title: "a",
isInQuery: true },
// if URIs and dates are equal, should fall back to bookmark index
{ isBookmark: true,
isDetails: true,
- lastVisit: timeInMicroseconds + 1,
+ lastVisit: timeInMicroseconds + 1000,
uri: "http://example.com/c",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 5,
title: "x",
isInQuery: true },
// if no URI and titles are equal, should fall back to bookmark index
{ isFolder: true,
@@ -382,26 +382,26 @@ tests.push({
title: "y1",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 2,
isInQuery: true },
// if visitCounts are equal, should fall back to date
{ isBookmark: true,
uri: "http://example.com/b2",
- lastVisit: timeInMicroseconds + 1,
+ lastVisit: timeInMicroseconds + 1000,
title: "y2a",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 3,
isInQuery: true },
// if visitCounts and dates are equal, should fall back to bookmark index
{ isBookmark: true,
uri: "http://example.com/b2",
- lastVisit: timeInMicroseconds + 1,
+ lastVisit: timeInMicroseconds + 1000,
title: "y2b",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 4,
isInQuery: true },
];
this._sortedData = [
this._unsortedData[0],
@@ -413,18 +413,18 @@ tests.push({
// This function in head_queries.js creates our database with the above data
yield task_populateDB(this._unsortedData);
// add visits to increase visit count
yield promiseAddVisits([
{ uri: uri("http://example.com/a"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
{ uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
{ uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
- { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1 },
- { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1 },
+ { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
+ { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
{ uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
{ uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
{ uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
]);
},
check: function() {
// Query
@@ -560,51 +560,51 @@ tests.push({
var timeInMicroseconds = Date.now() * 1000;
this._unsortedData = [
{ isBookmark: true,
uri: "http://example.com/b1",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 0,
title: "y1",
- dateAdded: timeInMicroseconds -1,
+ dateAdded: timeInMicroseconds - 1000,
isInQuery: true },
{ isBookmark: true,
uri: "http://example.com/a",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 1,
title: "z",
- dateAdded: timeInMicroseconds - 2,
+ dateAdded: timeInMicroseconds - 2000,
isInQuery: true },
{ isBookmark: true,
uri: "http://example.com/c",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 2,
title: "x",
dateAdded: timeInMicroseconds,
isInQuery: true },
// if dateAddeds are equal, should fall back to title
{ isBookmark: true,
uri: "http://example.com/b2",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 3,
title: "y2",
- dateAdded: timeInMicroseconds - 1,
+ dateAdded: timeInMicroseconds - 1000,
isInQuery: true },
// if dateAddeds and titles are equal, should fall back to bookmark index
{ isBookmark: true,
uri: "http://example.com/b3",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 4,
title: "y3",
- dateAdded: timeInMicroseconds - 1,
+ dateAdded: timeInMicroseconds - 1000,
isInQuery: true },
];
this._sortedData = [
this._unsortedData[1],
this._unsortedData[0],
this._unsortedData[3],
this._unsortedData[4],
@@ -650,52 +650,52 @@ tests.push({
var timeInMicroseconds = Date.now() * 1000;
this._unsortedData = [
{ isBookmark: true,
uri: "http://example.com/b1",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 0,
title: "y1",
- lastModified: timeInMicroseconds -1,
+ lastModified: timeInMicroseconds - 1000,
isInQuery: true },
{ isBookmark: true,
uri: "http://example.com/a",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 1,
title: "z",
- lastModified: timeInMicroseconds - 2,
+ lastModified: timeInMicroseconds - 2000,
isInQuery: true },
{ isBookmark: true,
uri: "http://example.com/c",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 2,
title: "x",
lastModified: timeInMicroseconds,
isInQuery: true },
// if lastModifieds are equal, should fall back to title
{ isBookmark: true,
uri: "http://example.com/b2",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 3,
title: "y2",
- lastModified: timeInMicroseconds - 1,
+ lastModified: timeInMicroseconds - 1000,
isInQuery: true },
// if lastModifieds and titles are equal, should fall back to bookmark
// index
{ isBookmark: true,
uri: "http://example.com/b3",
parentFolder: PlacesUtils.bookmarks.toolbarFolder,
index: 4,
title: "y3",
- lastModified: timeInMicroseconds - 1,
+ lastModified: timeInMicroseconds - 1000,
isInQuery: true },
];
this._sortedData = [
this._unsortedData[1],
this._unsortedData[0],
this._unsortedData[3],
this._unsortedData[4],
@@ -1269,9 +1269,9 @@ add_task(function test_sorting()
yield promiseAsyncUpdates();
test.check();
// sorting reversed, usually SORT_BY have ASC and DESC
test.check_reverse();
// Execute cleanup tasks
remove_all_bookmarks();
yield promiseClearHistory();
}
-});
\ No newline at end of file
+});
--- a/toolkit/components/places/tests/unit/test_398914.js
+++ b/toolkit/components/places/tests/unit/test_398914.js
@@ -92,17 +92,17 @@ function run_test() {
// but could be equal if the test runs faster than our PRNow()
// granularity
do_check_true(bm1lm >= bm2lm);
// we need to ensure that bm1 last modified date is greater
// that the modified date of bm2, otherwise in case of a "tie"
// bm2 will win, as it has a bigger item id
if (bm1lm == bm2lm)
- bmsvc.setItemLastModified(bm1, bm2lm + 1);
+ bmsvc.setItemLastModified(bm1, bm2lm + 1000);
[url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
do_check_eq(testURI.spec, url);
do_check_eq(postdata, "pdata1");
// cleanup
bmsvc.removeItem(bm1);
bmsvc.removeItem(bm2);
--- a/toolkit/components/places/tests/unit/test_419731.js
+++ b/toolkit/components/places/tests/unit/test_419731.js
@@ -33,17 +33,17 @@ function run_test() {
let tagItemId = tagNode.itemId;
tagRoot.containerOpen = false;
// change bookmark 1 title
PlacesUtils.bookmarks.setItemTitle(bookmark1id, "new title 1");
// Workaround timers resolution and time skews.
let bookmark2LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark2id);
- PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1000);
// Query the tag.
options = PlacesUtils.history.getNewQueryOptions();
options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
options.resultType = options.RESULTS_AS_TAG_QUERY;
query = PlacesUtils.history.getNewQuery();
result = PlacesUtils.history.executeQuery(query, options);
@@ -71,17 +71,17 @@ function run_test() {
theTag.containerOpen = false;
root.containerOpen = false;
// Change bookmark 2 title.
PlacesUtils.bookmarks.setItemTitle(bookmark2id, "new title 2");
// Workaround timers resolution and time skews.
let bookmark1LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark1id);
- PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1000);
// Check that tag container contains new title
options = PlacesUtils.history.getNewQueryOptions();
options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
options.resultType = options.RESULTS_AS_TAG_CONTENTS;
query = PlacesUtils.history.getNewQuery();
query.setFolders([tagItemId], 1);
--- a/toolkit/components/places/tests/unit/test_lastModified.js
+++ b/toolkit/components/places/tests/unit/test_lastModified.js
@@ -18,17 +18,17 @@ function run_test() {
"itemTitle");
var dateAdded = bs.getItemDateAdded(itemId);
do_check_eq(dateAdded, bs.getItemLastModified(itemId));
// Change lastModified, then change dateAdded. LastModified should be set
// to the new dateAdded.
// This could randomly fail on virtual machines due to timing issues, so
// we manually increase the time value. See bug 500640 for details.
- bs.setItemLastModified(itemId, dateAdded + 1);
- do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1);
+ bs.setItemLastModified(itemId, dateAdded + 1000);
+ do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1000);
do_check_true(bs.getItemDateAdded(itemId) < bs.getItemLastModified(itemId));
- bs.setItemDateAdded(itemId, dateAdded + 2);
- do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2);
+ bs.setItemDateAdded(itemId, dateAdded + 2000);
+ do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2000);
do_check_eq(bs.getItemDateAdded(itemId), bs.getItemLastModified(itemId));
bs.removeItem(itemId);
}
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -612,17 +612,17 @@ add_test(function test_generic_item_anno
});
add_test(function test_editing_item_date_added() {
let testURI = NetUtil.newURI("http://test_editing_item_date_added.com");
let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
"Test editing item date added");
let oldAdded = bmsvc.getItemDateAdded(testBkmId);
- let newAdded = Date.now() + 1000;
+ let newAdded = Date.now() * 1000 + 1000;
let txn = new PlacesEditItemDateAddedTransaction(testBkmId, newAdded);
txn.doTransaction();
do_check_eq(newAdded, bmsvc.getItemDateAdded(testBkmId));
txn.undoTransaction();
do_check_eq(oldAdded, bmsvc.getItemDateAdded(testBkmId));
@@ -630,17 +630,17 @@ add_test(function test_editing_item_date
});
add_test(function test_edit_item_last_modified() {
let testURI = NetUtil.newURI("http://test_edit_item_last_modified.com");
let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
"Test editing item last modified");
let oldModified = bmsvc.getItemLastModified(testBkmId);
- let newModified = Date.now() + 1000;
+ let newModified = Date.now() * 1000 + 1000;
let txn = new PlacesEditItemLastModifiedTransaction(testBkmId, newModified);
txn.doTransaction();
do_check_eq(newModified, bmsvc.getItemLastModified(testBkmId));
txn.undoTransaction();
do_check_eq(oldModified, bmsvc.getItemLastModified(testBkmId));
@@ -859,17 +859,17 @@ add_test(function test_aggregate_removeI
});
add_test(function test_create_item_with_childTxn() {
let testFolder = bmsvc.createFolder(root, "Test creating an item with childTxns", bmsvc.DEFAULT_INDEX);
const BOOKMARK_TITLE = "parent item";
let testURI = NetUtil.newURI("http://test_create_item_with_childTxn.com");
let childTxns = [];
- let newDateAdded = Date.now() - 20000;
+ let newDateAdded = Date.now() * 1000 - 20000;
let editDateAdddedTxn = new PlacesEditItemDateAddedTransaction(null, newDateAdded);
childTxns.push(editDateAdddedTxn);
let itemChildAnnoObj = { name: "testAnno/testInt",
type: Ci.nsIAnnotationService.TYPE_INT32,
flags: 0,
value: 123,
expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
--- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js
+++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js
@@ -23,18 +23,22 @@ function connect(onDone) {
let settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
settingsService.createLock().set("devtools.debugger.remote-enabled", true, null);
// We can't use `set` callback as it is fired before shell.js code listening for this setting
// is actually called. Same thing applies to mozsettings-changed obs notification.
// So listen to a custom event until bug 942756 lands
let observer = {
observe: function (subject, topic, data) {
Services.obs.removeObserver(observer, "debugger-server-started");
- let transport = DebuggerClient.socketConnect("127.0.0.1", 6000);
- startClient(transport, onDone);
+ DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: 6000
+ }).then(transport => {
+ startClient(transport, onDone);
+ }, e => dump("Connection failed: " + e + "\n"));
}
};
Services.obs.addObserver(observer, "debugger-server-started", false);
} else {
// Initialize a loopback remote protocol connection
DebuggerServer.init();
// We need to register browser actors to have `listTabs` working
// and also have a root actor
--- a/toolkit/devtools/client/connection-manager.js
+++ b/toolkit/devtools/client/connection-manager.js
@@ -4,20 +4,23 @@
* 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 {Cc, Ci, Cu} = require("chrome");
const {setTimeout, clearTimeout} = require('sdk/timers');
const EventEmitter = require("devtools/toolkit/event-emitter");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
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");
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
/**
* Connection Manager.
*
* To use this module:
* const {ConnectionManager} = require("devtools/client/connection-manager");
*
* # ConnectionManager
@@ -43,17 +46,18 @@ Cu.import("resource://gre/modules/devtoo
* . connect(transport) Connect via transport. Expect a "connecting" event.
* . 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)
- * . keepConnecting Should the connection keep trying connecting
+ * . keepConnecting Should the connection keep trying to connect?
+ * . encryption Should the connection be encrypted?
* . status Connection status:
* Connection.Status.CONNECTED
* Connection.Status.DISCONNECTED
* Connection.Status.CONNECTING
* Connection.Status.DISCONNECTING
* Connection.Status.DESTROYED
*
* Events (as in event-emitter.js):
@@ -107,17 +111,17 @@ 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);
- this.keepConnecting = false;
+ this.resetOptions();
}
Connection.Status = {
CONNECTED: "connected",
DISCONNECTED: "disconnected",
CONNECTING: "connecting",
DISCONNECTING: "disconnecting",
DESTROYED: "destroyed",
@@ -170,16 +174,21 @@ Connection.prototype = {
set port(value) {
if (this._port && this._port == value)
return;
this._port = value;
this.emit(Connection.Events.PORT_CHANGED);
},
+ resetOptions() {
+ this.keepConnecting = false;
+ this.encryption = false;
+ },
+
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");
@@ -217,40 +226,48 @@ Connection.prototype = {
this.keepConnecting = false;
if (this._client) {
this._client.close();
this._client = null;
}
this._setStatus(Connection.Status.DESTROYED);
},
- _clientConnect: function () {
- let transport;
+ _getTransport: Task.async(function*() {
if (this._customTransport) {
- transport = this._customTransport;
- } else {
- if (!this.host) {
- transport = DebuggerServer.connectPipe();
- } else {
- try {
- transport = DebuggerClient.socketConnect(this.host, this.port);
- } catch (e) {
- // In some cases, especially on Mac, the openOutputStream call in
- // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
- // It occurs when we connect agressively to the simulator,
- // and keep trying to open a socket to the server being started in
- // the simulator.
- this._onDisconnected();
- return;
- }
+ return this._customTransport;
+ }
+ if (!this.host) {
+ return DebuggerServer.connectPipe();
+ }
+ let transport = yield DebuggerClient.socketConnect({
+ host: this.host,
+ port: this.port,
+ encryption: this.encryption
+ });
+ return transport;
+ }),
+
+ _clientConnect: function () {
+ this._getTransport().then(transport => {
+ if (!transport) {
+ return;
}
- }
- this._client = new DebuggerClient(transport);
- this._client.addOneTimeListener("closed", this._onDisconnected);
- this._client.connect(this._onConnected);
+ this._client = new DebuggerClient(transport);
+ this._client.addOneTimeListener("closed", this._onDisconnected);
+ this._client.connect(this._onConnected);
+ }, e => {
+ console.error(e);
+ // In some cases, especially on Mac, the openOutputStream call in
+ // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
+ // It occurs when we connect agressively to the simulator,
+ // and keep trying to open a socket to the server being started in
+ // the simulator.
+ this._onDisconnected();
+ });
},
get status() {
return this._status
},
_setStatus: function(value) {
if (this._status && this._status == value)
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -367,19 +367,19 @@ DebuggerClient.Argument = function (aPos
DebuggerClient.Argument.prototype.getArgument = function (aParams) {
if (!(this.position in aParams)) {
throw new Error("Bad index into params: " + this.position);
}
return aParams[this.position];
};
// Expose this to save callers the trouble of importing DebuggerSocket
-DebuggerClient.socketConnect = function(host, port) {
+DebuggerClient.socketConnect = function(options) {
// Defined here instead of just copying the function to allow lazy-load
- return DebuggerSocket.connect(host, port);
+ return DebuggerSocket.connect(options);
};
DebuggerClient.prototype = {
/**
* Connect to the server and start exchanging protocol messages.
*
* @param aOnConnected function
* If specified, will be called when the greeting packet is
--- a/toolkit/devtools/gcli/commands/listen.js
+++ b/toolkit/devtools/gcli/commands/listen.js
@@ -43,21 +43,24 @@ exports.items = [
type: "number",
get defaultValue() {
return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
},
description: gcli.lookup("listenPortDesc"),
}
],
exec: function(args, context) {
- var reply = debuggerServer.openListener(args.port);
- if (!reply) {
+ var listener = debuggerServer.createListener();
+ if (!listener) {
throw new Error(gcli.lookup("listenDisabledOutput"));
}
+ listener.portOrPath = args.port;
+ listener.open();
+
if (debuggerServer.initialized) {
return gcli.lookupFormat("listenInitOutput", [ "" + args.port ]);
}
return gcli.lookup("listenNoInitOutput");
},
}
];
--- a/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
@@ -14,16 +14,17 @@
* limitations under the License.
*/
'use strict';
var Cu = require('chrome').Cu;
var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
+var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
var Promise = require('../util/promise').Promise;
var Connection = require('./connectors').Connection;
/**
* What port should we use by default?
*/
Object.defineProperty(exports, 'defaultPort', {
@@ -56,36 +57,40 @@ exports.items = [
*/
function RdpConnection(url) {
throw new Error('Use RdpConnection.create');
}
/**
* Asynchronous construction
*/
-RdpConnection.create = function(url) {
+RdpConnection.create = Task.async(function*(url) {
this.host = url;
this.port = undefined; // TODO: Split out the port number
this.requests = {};
this.nextRequestId = 0;
this._emit = this._emit.bind(this);
+ let transport = yield DebuggerClient.socketConnect({
+ host: this.host,
+ port: this.port
+ });
+
return new Promise(function(resolve, reject) {
- this.transport = DebuggerClient.socketConnect(this.host, this.port);
- this.client = new DebuggerClient(this.transport);
+ this.client = new DebuggerClient(transport);
this.client.connect(function() {
this.client.listTabs(function(response) {
this.actor = response.gcliActor;
resolve();
}.bind(this));
}.bind(this));
}.bind(this));
-};
+});
RdpConnection.prototype = Object.create(Connection.prototype);
RdpConnection.prototype.call = function(command, data) {
return new Promise(function(resolve, reject) {
var request = { to: this.actor, type: command, data: data };
this.client.request(request, function(response) {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/cert.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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";
+
+let { Ci, Cc } = require("chrome");
+let promise = require("promise");
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+DevToolsUtils.defineLazyGetter(this, "localCertService", () => {
+ // Ensure PSM is initialized to support TLS sockets
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+ return Cc["@mozilla.org/security/local-cert-service;1"]
+ .getService(Ci.nsILocalCertService);
+});
+
+const localCertName = "devtools";
+
+exports.local = {
+
+ /**
+ * Get or create a new self-signed X.509 cert to represent this device for
+ * DevTools purposes over a secure transport, like TLS.
+ *
+ * The cert is stored permanently in the profile's key store after first use,
+ * and is valid for 1 year. If an expired or otherwise invalid cert is found,
+ * it is removed and a new one is made.
+ *
+ * @return promise
+ */
+ getOrCreate() {
+ let deferred = promise.defer();
+ localCertService.getOrCreateCert(localCertName, {
+ handleCert: function(cert, rv) {
+ if (rv) {
+ deferred.reject(rv);
+ return;
+ }
+ deferred.resolve(cert);
+ }
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Remove the DevTools self-signed X.509 cert for this device.
+ *
+ * @return promise
+ */
+ remove() {
+ let deferred = promise.defer();
+ localCertService.removeCert(localCertName, {
+ handleCert: function(rv) {
+ if (rv) {
+ deferred.reject(rv);
+ return;
+ }
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+ }
+
+};
--- a/toolkit/devtools/security/moz.build
+++ b/toolkit/devtools/security/moz.build
@@ -16,10 +16,11 @@ UNIFIED_SOURCES += [
'LocalCertService.cpp',
]
FAIL_ON_WARNINGS = True
FINAL_LIBRARY = 'xul'
EXTRA_JS_MODULES.devtools.security += [
+ 'cert.js',
'socket.js',
]
--- a/toolkit/devtools/security/socket.js
+++ b/toolkit/devtools/security/socket.js
@@ -2,74 +2,212 @@
/* 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";
let { Ci, Cc, CC, Cr } = require("chrome");
+
+// Ensure PSM is initialized to support TLS sockets
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
let Services = require("Services");
+let promise = require("promise");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
-let { dumpn } = DevToolsUtils;
+let { dumpn, dumpv } = DevToolsUtils;
loader.lazyRequireGetter(this, "DebuggerTransport",
"devtools/toolkit/transport/transport", true);
loader.lazyRequireGetter(this, "DebuggerServer",
"devtools/server/main", true);
-
-DevToolsUtils.defineLazyGetter(this, "ServerSocket", () => {
- return CC("@mozilla.org/network/server-socket;1",
- "nsIServerSocket",
- "initSpecialConnection");
-});
-
-DevToolsUtils.defineLazyGetter(this, "UnixDomainServerSocket", () => {
- return CC("@mozilla.org/network/server-socket;1",
- "nsIServerSocket",
- "initWithFilename");
-});
+loader.lazyRequireGetter(this, "discovery",
+ "devtools/toolkit/discovery/discovery");
+loader.lazyRequireGetter(this, "cert",
+ "devtools/toolkit/security/cert");
+loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
+loader.lazyRequireGetter(this, "clearTimeout", "Timer", true);
DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
});
DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
return Cc["@mozilla.org/network/socket-transport-service;1"]
.getService(Ci.nsISocketTransportService);
});
+DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => {
+ return Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+});
+
+DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
+ return Cc["@mozilla.org/nss_errors_service;1"]
+ .getService(Ci.nsINSSErrorsService);
+});
+
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
+let DebuggerSocket = {};
+
/**
- * Connects to a debugger server socket and returns a DebuggerTransport.
+ * Connects to a debugger server socket.
*
* @param host string
* The host name or IP address of the debugger server.
* @param port number
* The port number of the debugger server.
+ * @param encryption boolean (optional)
+ * Whether the server requires encryption. Defaults to false.
+ * @return promise
+ * Resolved to a DebuggerTransport instance.
*/
-function socketConnect(host, port) {
- let s = socketTransportService.createTransport(null, 0, host, port, null);
+DebuggerSocket.connect = Task.async(function*({ host, port, encryption }) {
+ let attempt = yield _attemptTransport({ host, port, encryption });
+ if (attempt.transport) {
+ return attempt.transport; // Success
+ }
+
+ // If the server cert failed validation, store a temporary override and make
+ // a second attempt.
+ if (encryption && attempt.certError) {
+ _storeCertOverride(attempt.s, host, port);
+ } else {
+ throw new Error("Connection failed");
+ }
+
+ attempt = yield _attemptTransport({ host, port, encryption });
+ if (attempt.transport) {
+ return attempt.transport; // Success
+ }
+
+ throw new Error("Connection failed even after cert override");
+});
+
+/**
+ * Try to connect and create a DevTools transport.
+ *
+ * @return transport DebuggerTransport
+ * A possible DevTools transport (if connection succeeded and streams
+ * are actually alive and working)
+ * @return certError boolean
+ * Flag noting if cert trouble caused the streams to fail
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ */
+let _attemptTransport = Task.async(function*({ host, port, encryption }){
+ // _attemptConnect only opens the streams. Any failures at that stage
+ // aborts the connection process immedidately.
+ let { s, input, output } = _attemptConnect({ host, port, encryption });
+
+ // Check if the input stream is alive. If encryption is enabled, we need to
+ // watch out for cert errors by testing the input stream.
+ let { alive, certError } = yield _isInputAlive(input);
+ dumpv("Server cert accepted? " + !certError);
+
+ let transport;
+ if (alive) {
+ transport = new DebuggerTransport(input, output);
+ } else {
+ // Something went wrong, close the streams.
+ input.close();
+ output.close();
+ }
+
+ return { transport, certError, s };
+});
+
+/**
+ * Try to connect to a remote server socket.
+ *
+ * If successsful, the socket transport and its opened streams are returned.
+ * Typically, this will only fail if the host / port is unreachable. Other
+ * problems, such as security errors, will allow this stage to succeed, but then
+ * fail later when the streams are actually used.
+ * @return s nsISocketTransport
+ * Underlying socket transport, in case more details are needed.
+ * @return input nsIAsyncInputStream
+ * The socket's input stream.
+ * @return output nsIAsyncOutputStream
+ * The socket's output stream.
+ */
+function _attemptConnect({ host, port, encryption }) {
+ let s;
+ if (encryption) {
+ s = socketTransportService.createTransport(["ssl"], 1, host, port, null);
+ } else {
+ s = socketTransportService.createTransport(null, 0, host, port, null);
+ }
// By default the CONNECT socket timeout is very long, 65535 seconds,
// so that if we race to be in CONNECT state while the server socket is still
// initializing, the connection is stuck in connecting state for 18.20 hours!
s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
// openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
// where the nsISocketTransport gets shutdown in between its instantiation and
// the call to this method.
- let transport;
+ let input;
+ let output;
try {
- transport = new DebuggerTransport(s.openInputStream(0, 0, 0),
- s.openOutputStream(0, 0, 0));
+ input = s.openInputStream(0, 0, 0);
+ output = s.openOutputStream(0, 0, 0);
} catch(e) {
- DevToolsUtils.reportException("socketConnect", e);
+ DevToolsUtils.reportException("_attemptConnect", e);
throw e;
}
- return transport;
+
+ return { s, input, output };
+}
+
+/**
+ * Check if the input stream is alive. For an encrypted connection, it may not
+ * be if the client refuses the server's cert. A cert error is expected on
+ * first connection to a new host because the cert is self-signed.
+ */
+function _isInputAlive(input) {
+ let deferred = promise.defer();
+ input.asyncWait({
+ onInputStreamReady(stream) {
+ try {
+ stream.available();
+ deferred.resolve({ alive: true });
+ } catch (e) {
+ try {
+ // getErrorClass may throw if you pass a non-NSS error
+ let errorClass = nssErrorsService.getErrorClass(e.result);
+ if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ deferred.resolve({ certError: true });
+ } else {
+ deferred.reject(e);
+ }
+ } catch (nssErr) {
+ deferred.reject(e);
+ }
+ }
+ }
+ }, 0, 0, Services.tm.currentThread);
+ return deferred.promise;
+}
+
+/**
+ * To allow the connection to proceed with self-signed cert, we store a cert
+ * override. This implies that we take on the burden of authentication for
+ * these connections.
+ */
+function _storeCertOverride(s, host, port) {
+ let cert = s.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+ .SSLStatus.serverCert;
+ let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH;
+ certOverrideService.rememberValidityOverride(host, port, cert, overrideBits,
+ true /* temporary */);
}
/**
* Creates a new socket listener for remote connections to the DebuggerServer.
* This helps contain and organize the parts of the server that may differ or
* are particular to one given listener mechanism vs. another.
*/
function SocketListener() {}
@@ -101,82 +239,161 @@ SocketListener.defaultAllowConnection =
DebuggerServer.closeAllListeners();
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
}
return false;
};
SocketListener.prototype = {
- /**
- * Listens on the given port or socket file for remote debugger connections.
- *
- * @param portOrPath int, string
- * If given an integer, the port to listen on.
- * Otherwise, the path to the unix socket domain file to listen on.
- */
- open: function(portOrPath) {
- let flags = Ci.nsIServerSocket.KeepWhenOffline;
- // A preference setting can force binding on the loopback interface.
- if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
- flags |= Ci.nsIServerSocket.LoopbackOnly;
- }
-
- try {
- let backlog = 4;
- let port = Number(portOrPath);
- if (port) {
- this._socket = new ServerSocket(port, flags, backlog);
- } else {
- let file = nsFile(portOrPath);
- if (file.exists())
- file.remove(false);
- this._socket = new UnixDomainServerSocket(file, parseInt("666", 8),
- backlog);
- }
- this._socket.asyncListen(this);
- } catch (e) {
- dumpn("Could not start debugging listener on '" + portOrPath + "': " + e);
- throw Cr.NS_ERROR_NOT_AVAILABLE;
- }
- },
+ /* Socket Options */
/**
- * Closes the SocketListener. Notifies the server to remove the listener from
- * the set of active SocketListeners.
+ * The port or path to listen on.
+ *
+ * If given an integer, the port to listen on. Use -1 to choose any available
+ * port. Otherwise, the path to the unix socket domain file to listen on.
*/
- close: function() {
- this._socket.close();
- DebuggerServer._removeListener(this);
- },
-
- /**
- * Gets the port that a TCP socket listener is listening on, or null if this
- * is not a TCP socket (so there is no port).
- */
- get port() {
- if (!this._socket) {
- return null;
- }
- return this._socket.port;
- },
+ portOrPath: null,
/**
* Prompt the user to accept or decline the incoming connection. The default
* implementation is used unless this is overridden on a particular socket
* listener instance.
*
* @return true if the connection should be permitted, false otherwise
*/
allowConnection: SocketListener.defaultAllowConnection,
+ /**
+ * Controls whether this listener is announced via the service discovery
+ * mechanism.
+ */
+ discoverable: false,
+
+ /**
+ * Controls whether this listener's transport uses encryption.
+ */
+ encryption: false,
+
+ /**
+ * Validate that all options have been set to a supported configuration.
+ */
+ _validateOptions: function() {
+ if (this.portOrPath === null) {
+ throw new Error("Must set a port / path to listen on.");
+ }
+ if (this.discoverable && !Number(this.portOrPath)) {
+ throw new Error("Discovery only supported for TCP sockets.");
+ }
+ },
+
+ /**
+ * Listens on the given port or socket file for remote debugger connections.
+ */
+ open: function() {
+ this._validateOptions();
+ DebuggerServer._addListener(this);
+
+ let flags = Ci.nsIServerSocket.KeepWhenOffline;
+ // A preference setting can force binding on the loopback interface.
+ if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+ flags |= Ci.nsIServerSocket.LoopbackOnly;
+ }
+
+ let self = this;
+ return Task.spawn(function*() {
+ let backlog = 4;
+ self._socket = self._createSocketInstance();
+ if (self.isPortBased) {
+ let port = Number(self.portOrPath);
+ self._socket.initSpecialConnection(port, flags, backlog);
+ } else {
+ let file = nsFile(self.portOrPath);
+ if (file.exists()) {
+ file.remove(false);
+ }
+ self._socket.initWithFilename(file, parseInt("666", 8), backlog);
+ }
+ yield self._setAdditionalSocketOptions();
+ self._socket.asyncListen(self);
+ dumpn("Socket listening on: " + (self.port || self.portOrPath));
+ }).then(() => {
+ if (this.discoverable && this.port) {
+ discovery.addService("devtools", {
+ port: this.port,
+ encryption: this.encryption
+ });
+ }
+ }).catch(e => {
+ dumpn("Could not start debugging listener on '" + this.portOrPath +
+ "': " + e);
+ this.close();
+ });
+ },
+
+ _createSocketInstance: function() {
+ if (this.encryption) {
+ return Cc["@mozilla.org/network/tls-server-socket;1"]
+ .createInstance(Ci.nsITLSServerSocket);
+ }
+ return Cc["@mozilla.org/network/server-socket;1"]
+ .createInstance(Ci.nsIServerSocket);
+ },
+
+ _setAdditionalSocketOptions: Task.async(function*() {
+ if (this.encryption) {
+ this._socket.serverCert = yield cert.local.getOrCreate();
+ this._socket.setSessionCache(false);
+ this._socket.setSessionTickets(false);
+ let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+ this._socket.setRequestClientCertificate(requestCert);
+ }
+ }),
+
+ /**
+ * Closes the SocketListener. Notifies the server to remove the listener from
+ * the set of active SocketListeners.
+ */
+ close: function() {
+ if (this.discoverable && this.port) {
+ discovery.removeService("devtools");
+ }
+ if (this._socket) {
+ this._socket.close();
+ this._socket = null;
+ }
+ DebuggerServer._removeListener(this);
+ },
+
+ /**
+ * Gets whether this listener uses a port number vs. a path.
+ */
+ get isPortBased() {
+ return !!Number(this.portOrPath);
+ },
+
+ /**
+ * Gets the port that a TCP socket listener is listening on, or null if this
+ * is not a TCP socket (so there is no port).
+ */
+ get port() {
+ if (!this.isPortBased || !this._socket) {
+ return null;
+ }
+ return this._socket.port;
+ },
+
// nsIServerSocketListener implementation
onSocketAccepted:
DevToolsUtils.makeInfallible(function(socket, socketTransport) {
+ if (this.encryption) {
+ new SecurityObserver(socketTransport);
+ }
if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") &&
!this.allowConnection()) {
return;
}
dumpn("New debugging connection on " +
socketTransport.host + ":" + socketTransport.port);
let input = socketTransport.openInputStream(0, 0, 0);
@@ -186,18 +403,68 @@ SocketListener.prototype = {
}, "SocketListener.onSocketAccepted"),
onStopListening: function(socket, status) {
dumpn("onStopListening, status: " + status);
}
};
-// TODO: These high-level entry points will branch based on TLS vs. bare TCP as
-// part of bug 1059001.
-exports.DebuggerSocket = {
- createListener() {
- return new SocketListener();
+// Client must complete TLS handshake within this window (ms)
+loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
+ return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
+});
+
+function SecurityObserver(socketTransport) {
+ this.socketTransport = socketTransport;
+ let connectionInfo = socketTransport.securityInfo
+ .QueryInterface(Ci.nsITLSServerConnectionInfo);
+ connectionInfo.setSecurityObserver(this);
+ this._handshakeTimeout = setTimeout(this._onHandshakeTimeout.bind(this),
+ HANDSHAKE_TIMEOUT);
+}
+
+SecurityObserver.prototype = {
+
+ _onHandshakeTimeout() {
+ dumpv("Client failed to complete handshake");
+ this.destroy(Cr.NS_ERROR_NET_TIMEOUT);
},
- connect(host, port) {
- return socketConnect(host, port);
+
+ // nsITLSServerSecurityObserver implementation
+ onHandshakeDone(socket, clientStatus) {
+ clearTimeout(this._handshakeTimeout);
+ dumpv("TLS version: " + clientStatus.tlsVersionUsed.toString(16));
+ dumpv("TLS cipher: " + clientStatus.cipherName);
+ dumpv("TLS key length: " + clientStatus.keyLength);
+ dumpv("TLS MAC length: " + clientStatus.macLength);
+ /*
+ * TODO: These rules should be really be set on the TLS socket directly, but
+ * this would need more platform work to expose it via XPCOM.
+ *
+ * Server *will* send hello packet when any rules below are not met, but the
+ * socket then closes after that.
+ *
+ * Enforcing cipher suites here would be a bad idea, as we want TLS
+ * cipher negotiation to work correctly. The server already allows only
+ * Gecko's normal set of cipher suites.
+ */
+ if (clientStatus.tlsVersionUsed != Ci.nsITLSClientStatus.TLS_VERSION_1_2) {
+ this.destroy(Cr.NS_ERROR_CONNECTION_REFUSED);
+ }
+ },
+
+ destroy(result) {
+ clearTimeout(this._handshakeTimeout);
+ let connectionInfo = this.socketTransport.securityInfo
+ .QueryInterface(Ci.nsITLSServerConnectionInfo);
+ connectionInfo.setSecurityObserver(null);
+ this.socketTransport.close(result);
+ this.socketTransport = null;
}
+
};
+
+DebuggerSocket.createListener = function() {
+ return new SocketListener();
+};
+
+exports.DebuggerSocket = DebuggerSocket;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/head_dbg.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+const CC = Components.Constructor;
+
+const { devtools } =
+ Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { Promise: promise } =
+ Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+
+const Services = devtools.require("Services");
+const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
+const xpcInspector = devtools.require("xpcInspector");
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+// Fast timeout for TLS tests
+Services.prefs.setIntPref("devtools.remote.tls-handshake-timeout", 1000);
+
+function tryImport(url) {
+ try {
+ Cu.import(url);
+ } catch (e) {
+ dump("Error importing " + url + "\n");
+ dump(DevToolsUtils.safeErrorString(e) + "\n");
+ throw e;
+ }
+}
+
+tryImport("resource://gre/modules/devtools/dbg-server.jsm");
+tryImport("resource://gre/modules/devtools/dbg-client.jsm");
+
+// Convert an nsIScriptError 'aFlags' value into an appropriate string.
+function scriptErrorFlagsToKind(aFlags) {
+ var kind;
+ if (aFlags & Ci.nsIScriptError.warningFlag)
+ kind = "warning";
+ if (aFlags & Ci.nsIScriptError.exceptionFlag)
+ kind = "exception";
+ else
+ kind = "error";
+
+ if (aFlags & Ci.nsIScriptError.strictFlag)
+ kind = "strict " + kind;
+
+ return kind;
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+let errorCount = 0;
+let listener = {
+ observe: function (aMessage) {
+ errorCount++;
+ try {
+ // If we've been given an nsIScriptError, then we can print out
+ // something nicely formatted, for tools like Emacs to pick up.
+ var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+ dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+ scriptErrorFlagsToKind(aMessage.flags) + ": " +
+ aMessage.errorMessage + "\n");
+ var string = aMessage.errorMessage;
+ } catch (x) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ var string = "" + aMessage.message;
+ } catch (x) {
+ var string = "<error converting error message to string>";
+ }
+ }
+
+ // Make sure we exit all nested event loops so that the test can finish.
+ while (xpcInspector.eventLoopNestLevel > 0) {
+ xpcInspector.exitNestedEventLoop();
+ }
+
+ // Print in most cases, but ignore the "strict" messages
+ if (!(aMessage.flags & Ci.nsIScriptError.strictFlag)) {
+ do_print("head_dbg.js got console message: " + string + "\n");
+ }
+ }
+};
+
+let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
+
+/**
+ * Initialize the testing debugger server.
+ */
+function initTestDebuggerServer() {
+ DebuggerServer.registerModule("xpcshell-test/testactors");
+ DebuggerServer.init();
+}
--- a/toolkit/devtools/security/tests/unit/test_cert.js
+++ b/toolkit/devtools/security/tests/unit/test_cert.js
@@ -1,17 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
-
-const { Promise: promise } =
- Cu.import("resource://gre/modules/Promise.jsm", {});
const certService = Cc["@mozilla.org/security/local-cert-service;1"]
.getService(Ci.nsILocalCertService);
const gNickname = "devtools";
function run_test() {
// Need profile dir to store the key / cert
do_get_profile();
@@ -44,17 +40,17 @@ function removeCert(nickname) {
}
deferred.resolve();
}
});
return deferred.promise;
}
add_task(function*() {
- // No master password, so prompt required here
+ // No master password, so no prompt required here
ok(!certService.loginPromptRequired);
let certA = yield getOrCreateCert(gNickname);
equal(certA.nickname, gNickname);
// Getting again should give the same cert
let certB = yield getOrCreateCert(gNickname);
equal(certB.nickname, gNickname);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/test_encryption.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test basic functionality of DevTools client and server TLS encryption mode
+function run_test() {
+ // Need profile dir to store the key / cert
+ do_get_profile();
+ // Ensure PSM is initialized
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+ run_next_test();
+}
+
+function connectClient(client) {
+ let deferred = promise.defer();
+ client.connect(() => {
+ client.listTabs(deferred.resolve);
+ });
+ return deferred.promise;
+}
+
+add_task(function*() {
+ initTestDebuggerServer();
+});
+
+// Client w/ encryption connects successfully to server w/ encryption
+add_task(function*() {
+ equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+ let listener = DebuggerServer.createListener();
+ ok(listener, "Socket listener created");
+ listener.portOrPath = -1 /* any available port */;
+ listener.allowConnection = () => true;
+ listener.encryption = true;
+ yield listener.open();
+ equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+ let transport = yield DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port,
+ encryption: true
+ });
+ ok(transport, "Client transport created");
+
+ let client = new DebuggerClient(transport);
+ let onUnexpectedClose = () => {
+ do_throw("Closed unexpectedly");
+ };
+ client.addListener("closed", onUnexpectedClose);
+ yield connectClient(client);
+
+ // Send a message the server will echo back
+ let message = "secrets";
+ let reply = yield client.request({
+ to: "root",
+ type: "echo",
+ message
+ });
+ equal(reply.message, message, "Encrypted echo matches");
+
+ client.removeListener("closed", onUnexpectedClose);
+ transport.close();
+ listener.close();
+ equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o encryption fails to connect to server w/ encryption
+add_task(function*() {
+ equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+ let listener = DebuggerServer.createListener();
+ ok(listener, "Socket listener created");
+ listener.portOrPath = -1 /* any available port */;
+ listener.allowConnection = () => true;
+ listener.encryption = true;
+ yield listener.open();
+ equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+ try {
+ yield DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port
+ // encryption: false is the default
+ });
+ } catch(e) {
+ ok(true, "Client failed to connect as expected");
+ listener.close();
+ equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+ return;
+ }
+
+ do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(function*() {
+ DebuggerServer.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/testactors.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ActorPool, appendExtraActors, createExtraActors } =
+ require("devtools/server/actors/common");
+const { RootActor } = require("devtools/server/actors/root");
+const { ThreadActor } = require("devtools/server/actors/script");
+const { DebuggerServer } = require("devtools/server/main");
+const promise = require("promise");
+
+var gTestGlobals = [];
+DebuggerServer.addTestGlobal = function(aGlobal) {
+ gTestGlobals.push(aGlobal);
+};
+
+// A mock tab list, for use by tests. This simply presents each global in
+// gTestGlobals as a tab, and the list is fixed: it never calls its
+// onListChanged handler.
+//
+// As implemented now, we consult gTestGlobals when we're constructed, not
+// when we're iterated over, so tests have to add their globals before the
+// root actor is created.
+function TestTabList(aConnection) {
+ this.conn = aConnection;
+
+ // An array of actors for each global added with
+ // DebuggerServer.addTestGlobal.
+ this._tabActors = [];
+
+ // A pool mapping those actors' names to the actors.
+ this._tabActorPool = new ActorPool(aConnection);
+
+ for (let global of gTestGlobals) {
+ let actor = new TestTabActor(aConnection, global);
+ actor.selected = false;
+ this._tabActors.push(actor);
+ this._tabActorPool.addActor(actor);
+ }
+ if (this._tabActors.length > 0) {
+ this._tabActors[0].selected = true;
+ }
+
+ aConnection.addActorPool(this._tabActorPool);
+}
+
+TestTabList.prototype = {
+ constructor: TestTabList,
+ getList: function () {
+ return promise.resolve([tabActor for (tabActor of this._tabActors)]);
+ }
+};
+
+function createRootActor(aConnection) {
+ let root = new RootActor(aConnection, {
+ tabList: new TestTabList(aConnection),
+ globalActorFactories: DebuggerServer.globalActorFactories
+ });
+ root.applicationType = "xpcshell-tests";
+ return root;
+}
+
+function TestTabActor(aConnection, aGlobal) {
+ this.conn = aConnection;
+ this._global = aGlobal;
+ this._threadActor = new ThreadActor(this, this._global);
+ this.conn.addActor(this._threadActor);
+ this._attached = false;
+ this._extraActors = {};
+}
+
+TestTabActor.prototype = {
+ constructor: TestTabActor,
+ actorPrefix: "TestTabActor",
+
+ get window() {
+ return { wrappedJSObject: this._global };
+ },
+
+ get url() {
+ return this._global.__name;
+ },
+
+ form: function() {
+ let response = { actor: this.actorID, title: this._global.__name };
+
+ // Walk over tab actors added by extensions and add them to a new ActorPool.
+ let actorPool = new ActorPool(this.conn);
+ this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
+ if (!actorPool.isEmpty()) {
+ this._tabActorPool = actorPool;
+ this.conn.addActorPool(this._tabActorPool);
+ }
+
+ this._appendExtraActors(response);
+
+ return response;
+ },
+
+ onAttach: function(aRequest) {
+ this._attached = true;
+
+ let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
+ this._appendExtraActors(response);
+
+ return response;
+ },
+
+ onDetach: function(aRequest) {
+ if (!this._attached) {
+ return { "error":"wrongState" };
+ }
+ return { type: "detached" };
+ },
+
+ /* Support for DebuggerServer.addTabActor. */
+ _createExtraActors: createExtraActors,
+ _appendExtraActors: appendExtraActors
+};
+
+TestTabActor.prototype.requestTypes = {
+ "attach": TestTabActor.prototype.onAttach,
+ "detach": TestTabActor.prototype.onDetach
+};
+
+exports.register = function(handle) {
+ handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function(handle) {
+ handle.setRootActor(null);
+};
--- a/toolkit/devtools/security/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/security/tests/unit/xpcshell.ini
@@ -1,6 +1,10 @@
[DEFAULT]
-head =
+head = head_dbg.js
tail =
skip-if = toolkit == 'android'
+support-files=
+ testactors.js
+
[test_cert.js]
+[test_encryption.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/animation.js
@@ -0,0 +1,273 @@
+/* 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";
+
+/**
+ * Set of actors that expose the Web Animations API to devtools protocol clients.
+ *
+ * The |Animations| actor is the main entry point. It is used to discover
+ * animation players on given nodes.
+ * There should only be one instance per debugger server.
+ *
+ * The |AnimationPlayer| actor provides attributes and methods to inspect an
+ * animation as well as pause/resume/seek it.
+ *
+ * The Web Animation spec implementation is ongoing in Gecko, and so this set
+ * of actors should evolve when the implementation progresses.
+ *
+ * References:
+ * - WebAnimation spec:
+ * http://w3c.github.io/web-animations/
+ * - WebAnimation WebIDL files:
+ * /dom/webidl/Animation*.webidl
+ */
+
+const {ActorClass, Actor,
+ FrontClass, Front,
+ Arg, method, RetVal} = require("devtools/server/protocol");
+const {NodeActor} = require("devtools/server/actors/inspector");
+
+/**
+ * The AnimationPlayerActor provides information about a given animation: its
+ * startTime, currentTime, current state, etc.
+ *
+ * Since the state of a player changes as the animation progresses it is often
+ * useful to call getCurrentState at regular intervals to get the current state.
+ *
+ * This actor also allows playing and pausing the animation.
+ */
+let AnimationPlayerActor = ActorClass({
+ typeName: "animationplayer",
+
+ /**
+ * @param {AnimationsActor} The main AnimationsActor instance
+ * @param {AnimationPlayer} The player object returned by getAnimationPlayers
+ * @param {DOMNode} The node targeted by this player
+ * @param {Number} Temporary work-around used to retrieve duration and
+ * iteration count from computed-style rather than from waapi. This is needed
+ * to know which duration to get, in case there are multiple css animations
+ * applied to the same node.
+ */
+ initialize: function(animationsActor, player, node, playerIndex) {
+ this.player = player;
+ this.node = node;
+ this.playerIndex = playerIndex;
+ this.styles = node.ownerDocument.defaultView.getComputedStyle(node);
+ Actor.prototype.initialize.call(this, animationsActor.conn);
+ },
+
+ destroy: function() {
+ this.player = this.node = this.styles = null;
+ Actor.prototype.destroy.call(this);
+ },
+
+ /**
+ * Release the actor, when it isn't needed anymore.
+ * Protocol.js uses this release method to call the destroy method.
+ */
+ release: method(function() {}, {release: true}),
+
+ form: function(detail) {
+ if (detail === "actorid") {
+ return this.actorID;
+ }
+
+ let data = this.getCurrentState();
+ data.actor = this.actorID;
+
+ return data;
+ },
+
+ /**
+ * Get the animation duration from this player, in milliseconds.
+ * Note that the Web Animations API doesn't yet offer a way to retrieve this
+ * directly from the AnimationPlayer object, so for now, a duration is only
+ * returned if found in the node's computed styles.
+ * @return {Number}
+ */
+ getDuration: function() {
+ let durationText;
+ if (this.styles.animationDuration !== "0s") {
+ durationText = this.styles.animationDuration;
+ } else if (this.styles.transitionDuration !== "0s") {
+ durationText = this.styles.transitionDuration;
+ } else {
+ return null;
+ }
+
+ if (durationText.indexOf(",") !== -1) {
+ durationText = durationText.split(",")[this.playerIndex];
+ }
+
+ return parseFloat(durationText) * 1000;
+ },
+
+ /**
+ * Get the animation iteration count for this player. That is, how many times
+ * is the animation scheduled to run.
+ * Note that the Web Animations API doesn't yet offer a way to retrieve this
+ * directly from the AnimationPlayer object, so for now, check for
+ * animationIterationCount in the node's computed styles, and return that.
+ * This style property defaults to 1 anyway.
+ * @return {Number}
+ */
+ getIterationCount: function() {
+ let iterationText = this.styles.animationIterationCount;
+ if (iterationText.indexOf(",") !== -1) {
+ iterationText = iterationText.split(",")[this.playerIndex];
+ }
+
+ return parseInt(iterationText, 10);
+ },
+
+ /**
+ * Get the current state of the AnimationPlayer (currentTime, playState, ...).
+ * Note that the initial state is returned as the form of this actor when it
+ * is initialized.
+ * @return {Object}
+ */
+ getCurrentState: method(function() {
+ return {
+ /**
+ * Return the player's current startTime value.
+ * Will be null whenever the animation is paused or waiting to start.
+ */
+ startTime: this.player.startTime,
+ currentTime: this.player.currentTime,
+ playState: this.player.playState,
+ name: this.player.source.effect.name,
+ duration: this.getDuration(),
+ iterationCount: this.getIterationCount(),
+ /**
+ * Is the animation currently running on the compositor. This is important for
+ * developers to know if their animation is hitting the fast path or not.
+ * Currently this will only be true for Firefox OS though (where we have
+ * compositor animations enabled).
+ * Returns false whenever the animation is paused as it is taken off the
+ * compositor then.
+ */
+ isRunningOnCompositor: this.player.isRunningOnCompositor
+ };
+ }, {
+ request: {},
+ response: {
+ data: RetVal("json")
+ }
+ }),
+
+ /**
+ * Pause the player.
+ */
+ pause: method(function() {
+ this.player.pause();
+ }, {
+ request: {},
+ response: {}
+ }),
+
+ /**
+ * Play the player.
+ */
+ play: method(function() {
+ this.player.play();
+ }, {
+ request: {},
+ response: {}
+ })
+});
+
+let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
+ initialize: function(conn, form, detail, ctx) {
+ Front.prototype.initialize.call(this, conn, form, detail, ctx);
+ },
+
+ form: function(form, detail) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+ this._form = form;
+ },
+
+ destroy: function() {
+ Front.prototype.destroy.call(this);
+ },
+
+ /**
+ * Getter for the initial state of the player. Up to date states can be
+ * retrieved by calling the getCurrentState method.
+ */
+ get initialState() {
+ return {
+ startTime: this._form.startTime,
+ currentTime: this._form.currentTime,
+ playState: this._form.playState,
+ name: this._form.name,
+ duration: this._form.duration,
+ iterationCount: this._form.iterationCount,
+ isRunningOnCompositor: this._form.isRunningOnCompositor
+ }
+ }
+});
+
+/**
+ * The Animations actor lists animation players for a given node.
+ */
+let AnimationsActor = exports.AnimationsActor = ActorClass({
+ typeName: "animations",
+
+ initialize: function(conn, tabActor) {
+ Actor.prototype.initialize.call(this, conn);
+ },
+
+ destroy: function() {
+ Actor.prototype.destroy.call(this);
+ },
+
+ /**
+ * Since AnimationsActor doesn't have a protocol.js parent actor that takes
+ * care of its lifetime, implementing disconnect is required to cleanup.
+ */
+ disconnect: function() {
+ this.destroy();
+ },
+
+ /**
+ * Retrieve the list of AnimationPlayerActor actors corresponding to
+ * currently running animations for a given node.
+ * @param {NodeActor} nodeActor The NodeActor type is defined in
+ * /toolkit/devtools/server/actors/inspector
+ */
+ getAnimationPlayersForNode: method(function(nodeActor) {
+ let players = nodeActor.rawNode.getAnimationPlayers();
+
+ let actors = [];
+ for (let i = 0; i < players.length; i ++) {
+ // XXX: for now the index is passed along as the AnimationPlayerActor uses
+ // it to retrieve animation information from CSS.
+ actors.push(AnimationPlayerActor(this, players[i], nodeActor.rawNode, i));
+ }
+
+ return actors;
+ }, {
+ request: {
+ actorID: Arg(0, "domnode")
+ },
+ response: {
+ players: RetVal("array:animationplayer")
+ }
+ })
+});
+
+let AnimationsFront = exports.AnimationsFront = FrontClass(AnimationsActor, {
+ initialize: function(client, {animationsActor}) {
+ Front.prototype.initialize.call(this, client, {actor: animationsActor});
+ this.manage(this);
+ },
+
+ destroy: function() {
+ Front.prototype.destroy.call(this);
+ }
+});
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -58,38 +58,17 @@ Debugger.Object.prototype.getPromiseStat
};
};
/**
* A BreakpointActorMap is a map from locations to instances of BreakpointActor.
*/
function BreakpointActorMap() {
this._size = 0;
-
- // If we have a whole-line breakpoint set at LINE in URL, then
- //
- // this._wholeLineBreakpoints[URL][LINE]
- //
- // is an object
- //
- // { url, line[, actor] }
- //
- // where the `actor` property is optional.
- this._wholeLineBreakpoints = Object.create(null);
-
- // If we have a breakpoint set at LINE, COLUMN in URL, then
- //
- // this._breakpoints[URL][LINE][COLUMN]
- //
- // is an object
- //
- // { url, line, column[, actor] }
- //
- // where the `actor` property is optional.
- this._breakpoints = Object.create(null);
+ this._actors = {};
}
BreakpointActorMap.prototype = {
/**
* Return the number of instances of BreakpointActor in this instance of
* BreakpointActorMap.
*
* @returns Number
@@ -106,64 +85,56 @@ BreakpointActorMap.prototype = {
*
* @param Object query
* An optional object with the following properties:
* - source (optional)
* - line (optional, requires source)
* - column (optional, requires line)
*/
findActors: function* (query = {}) {
- if (query.column != null) {
- dbg_assert(query.line != null);
- }
- if (query.line != null) {
- dbg_assert(query.source != null);
- dbg_assert(query.source.actor != null);
- }
-
- let actor = query.source ? query.source.actor : null;
- for (let actor of this._iterActors(actor)) {
- for (let line of this._iterLines(actor, query.line)) {
- // Always yield whole line breakpoints first. See comment in
- // |BreakpointActorMap.prototype.getActor|.
- if (query.column == null
- && this._wholeLineBreakpoints[actor]
- && this._wholeLineBreakpoints[actor][line]) {
- yield this._wholeLineBreakpoints[actor][line];
+ function* findKeys(object, key) {
+ if (key !== undefined) {
+ if (key in object) {
+ yield key;
}
- for (let column of this._iterColumns(actor, line, query.column)) {
- yield this._breakpoints[actor][line][column];
+ }
+ else {
+ for (let key of Object.keys(object)) {
+ yield key;
}
}
}
+
+ query.actor = query.source ? query.source.actor : undefined;
+ query.beginColumn = query.column ? query.column : undefined;
+ query.endColumn = query.column ? query.column + 1 : undefined;
+
+ for (let actor of findKeys(this._actors, query.actor))
+ for (let line of findKeys(this._actors[actor], query.line))
+ for (let beginColumn of findKeys(this._actors[actor][line], query.beginColumn))
+ for (let endColumn of findKeys(this._actors[actor][line][beginColumn], query.endColumn)) {
+ yield this._actors[actor][line][beginColumn][endColumn];
+ }
},
/**
* Return the instance of BreakpointActor at the given location in this
* instance of BreakpointActorMap.
*
* @param Object location
* An object with the following properties:
* - source
* - line
* - column (optional)
*
* @returns BreakpointActor actor
* The instance of BreakpointActor at the given location.
*/
getActor: function (location) {
- let { source: { actor }, line, column } = location;
-
- dbg_assert(actor != null);
- dbg_assert(line != null);
for (let actor of this.findActors(location)) {
- // We will get whole line breakpoints before individual columns, so just
- // return the first one and if they didn't specify a column then they will
- // get the whole line breakpoint, and otherwise we will find the correct
- // one.
return actor;
}
return null;
},
/**
* Set the given instance of BreakpointActor to the given location in this
@@ -176,147 +147,67 @@ BreakpointActorMap.prototype = {
* - column (optional)
*
* @param BreakpointActor actor
* The instance of BreakpointActor to be set to the given location.
*/
setActor: function (location, actor) {
let { source, line, column } = location;
- if (column != null) {
- if (!this._breakpoints[source.actor]) {
- this._breakpoints[source.actor] = [];
- }
- if (!this._breakpoints[source.actor][line]) {
- this._breakpoints[source.actor][line] = [];
- }
-
- if (!this._breakpoints[source.actor][line][column]) {
- this._breakpoints[source.actor][line][column] = actor;
- this._size++;
- }
- return this._breakpoints[source.actor][line][column];
- } else {
- // Add a breakpoint that breaks on the whole line.
- if (!this._wholeLineBreakpoints[source.actor]) {
- this._wholeLineBreakpoints[source.actor] = [];
- }
-
- if (!this._wholeLineBreakpoints[source.actor][line]) {
- this._wholeLineBreakpoints[source.actor][line] = actor;
- this._size++;
- }
- return this._wholeLineBreakpoints[source.actor][line];
- }
+ let beginColumn = column ? column : 0;
+ let endColumn = column ? column + 1 : Infinity;
+
+ if (!this._actors[source.actor]) {
+ this._actors[source.actor] = [];
+ }
+ if (!this._actors[source.actor][line]) {
+ this._actors[source.actor][line] = [];
+ }
+ if (!this._actors[source.actor][line][beginColumn]) {
+ this._actors[source.actor][line][beginColumn] = [];
+ }
+ if (!this._actors[source.actor][line][beginColumn][endColumn]) {
+ ++this._size;
+ }
+ this._actors[source.actor][line][beginColumn][endColumn] = actor;
},
/**
* Delete the instance of BreakpointActor from the given location in this
* instance of BreakpointActorMap.
*
* @param Object location
* An object with the following properties:
* - source
* - line
* - column (optional)
*/
deleteActor: function (location) {
- let { source: { actor }, line, column } = location;
-
- if (column != null) {
- if (this._breakpoints[actor]) {
- if (this._breakpoints[actor][line]) {
- if (this._breakpoints[actor][line][column]) {
- delete this._breakpoints[actor][line][column];
- this._size--;
-
- // If this was the last breakpoint on this line, delete the line from
- // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield
- // this line even though we no longer have breakpoints on
- // it. Furthermore, we use Object.keys() instead of just checking
- // `this._breakpoints[url].length` directly, because deleting
- // properties from sparse arrays doesn't update the `length` property
- // like adding them does.
- if (Object.keys(this._breakpoints[actor][line]).length === 0) {
- delete this._breakpoints[actor][line];
- }
+ let { source, line, column } = location;
+
+ let beginColumn = column ? column : 0;
+ let endColumn = column ? column + 1 : Infinity;
+
+ if (this._actors[source.actor]) {
+ if (this._actors[source.actor][line]) {
+ if (this._actors[source.actor][line][beginColumn]) {
+ if (this._actors[source.actor][line][beginColumn][endColumn]) {
+ --this._size;
+ }
+ delete this._actors[source.actor][line][beginColumn][endColumn];
+ if (Object.keys(this._actors[source.actor][line][beginColumn]).length === 0) {
+ delete this._actors[source.actor][line][beginColumn];
}
}
- }
- } else {
- if (this._wholeLineBreakpoints[actor]) {
- if (this._wholeLineBreakpoints[actor][line]) {
- delete this._wholeLineBreakpoints[actor][line];
- this._size--;
+ if (Object.keys(this._actors[source.actor][line]).length === 0) {
+ delete this._actors[source.actor][line];
}
}
}
- },
-
- _iterActors: function* (aActor) {
- if (aActor) {
- if (this._breakpoints[aActor] || this._wholeLineBreakpoints[aActor]) {
- yield aActor;
- }
- } else {
- for (let actor of Object.keys(this._wholeLineBreakpoints)) {
- yield actor;
- }
- for (let actor of Object.keys(this._breakpoints)) {
- if (actor in this._wholeLineBreakpoints) {
- continue;
- }
- yield actor;
- }
- }
- },
-
- _iterLines: function* (aActor, aLine) {
- if (aLine != null) {
- if ((this._wholeLineBreakpoints[aActor]
- && this._wholeLineBreakpoints[aActor][aLine])
- || (this._breakpoints[aActor] && this._breakpoints[aActor][aLine])) {
- yield aLine;
- }
- } else {
- const wholeLines = this._wholeLineBreakpoints[aActor]
- ? Object.keys(this._wholeLineBreakpoints[aActor])
- : [];
- const columnLines = this._breakpoints[aActor]
- ? Object.keys(this._breakpoints[aActor])
- : [];
-
- const lines = wholeLines.concat(columnLines).sort();
-
- let lastLine;
- for (let line of lines) {
- if (line === lastLine) {
- continue;
- }
- yield line;
- lastLine = line;
- }
- }
- },
-
- _iterColumns: function* (aActor, aLine, aColumn) {
- if (!this._breakpoints[aActor] || !this._breakpoints[aActor][aLine]) {
- return;
- }
-
- if (aColumn != null) {
- if (this._breakpoints[aActor][aLine][aColumn]) {
- yield aColumn;
- }
- } else {
- for (let column in this._breakpoints[aActor][aLine]) {
- yield column;
- }
- }
- },
+ }
};
exports.BreakpointActorMap = BreakpointActorMap;
/**
* Keeps track of persistent sources across reloads and ties different
* source instances to the same actor id so that things like
* breakpoints survive reloads. ThreadSources uses this to force the
@@ -1288,17 +1179,17 @@ ThreadActor.prototype = {
*/
_breakOnEnter: function(script) {
let offsets = script.getAllOffsets();
let sourceActor = this.sources.source({ source: script.source });
for (let line = 0, n = offsets.length; line < n; line++) {
if (offsets[line]) {
let location = { line: line };
- let resp = sourceActor._setBreakpoint(location);
+ let resp = sourceActor.setBreakpoint(location);
dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
if (resp.error) {
reportError(new Error("Unable to set breakpoint on event listener"));
return;
}
let bpActor = this.breakpointActorMap.getActor({
source: sourceActor.form(),
line: location.line
@@ -2111,17 +2002,17 @@ ThreadActor.prototype = {
// Set any stored breakpoints.
let endLine = aScript.startLine + aScript.lineCount - 1;
let source = this.sources.source({ source: aScript.source });
for (let bpActor of this.breakpointActorMap.findActors({ source: source.form() })) {
// Limit the search to the line numbers contained in the new script.
if (bpActor.location.line >= aScript.startLine
&& bpActor.location.line <= endLine) {
- source._setBreakpoint(bpActor.location, aScript);
+ source.setBreakpoint(bpActor.location, aScript);
}
}
return true;
},
/**
@@ -2795,17 +2686,17 @@ SourceActor.prototype = {
else {
return this._createBreakpoint(genLoc, originalLoc, aRequest.condition);
}
});
},
_createBreakpoint: function(loc, originalLoc, condition) {
return resolve(null).then(() => {
- return this._setBreakpoint({
+ return this.setBreakpoint({
line: loc.line,
column: loc.column,
condition: condition
});
}).then(response => {
var actual = response.actualLocation;
if (actual) {
if (this.source) {
@@ -2988,17 +2879,17 @@ SourceActor.prototype = {
*
* @param object aLocation
* The location of the breakpoint (in the generated source, if source
* mapping).
* @param Debugger.Script aOnlyThisScript [optional]
* If provided, only set breakpoints in this Debugger.Script, and
* nowhere else.
*/
- _setBreakpoint: function (aLocation, aOnlyThisScript=null) {
+ setBreakpoint: function (aLocation, aOnlyThisScript=null) {
const location = {
source: this.form(),
line: aLocation.line,
column: aLocation.column,
condition: aLocation.condition
};
const actor = location.actor = this._getOrCreateBreakpointActor(location);
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -506,16 +506,21 @@ var DebuggerServer = {
});
if ("nsIProfiler" in Ci) {
this.registerModule("devtools/server/actors/profiler", {
prefix: "profiler",
constructor: "ProfilerActor",
type: { global: true, tab: true }
});
}
+ this.registerModule("devtools/server/actors/animation", {
+ prefix: "animations",
+ constructor: "AnimationsActor",
+ type: { global: true, tab: true }
+ });
},
/**
* Passes a set of options to the BrowserAddonActors for the given ID.
*
* @param aId string
* The ID of the add-on to pass the options to
* @param aOptions object
@@ -537,38 +542,44 @@ var DebuggerServer = {
return all(promises);
},
get listeningSockets() {
return this._listeners.length;
},
/**
- * Listens on the given port or socket file for remote debugger connections.
+ * Creates a socket listener for remote debugger connections.
*
- * @param portOrPath int, string
- * If given an integer, the port to listen on.
- * Otherwise, the path to the unix socket domain file to listen on.
+ * After calling this, set some socket options, such as the port / path to
+ * listen on, and then call |open| on the listener.
+ *
+ * See SocketListener in toolkit/devtools/security/socket.js for available
+ * options.
+ *
* @return SocketListener
- * A SocketListener instance that is already opened is returned. This
- * single listener can be closed at any later time by calling |close|
- * on the SocketListener. If a SocketListener could not be opened, an
- * error is thrown. If remote connections are disabled, undefined is
- * returned.
+ * A SocketListener instance that is waiting to be configured and
+ * opened is returned. This single listener can be closed at any
+ * later time by calling |close| on the SocketListener. If remote
+ * connections are disabled, an error is thrown.
*/
- openListener: function(portOrPath) {
+ createListener: function() {
if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
- return;
+ throw new Error("Can't create listener, remote debugging disabled");
}
this._checkInit();
+ return DebuggerSocket.createListener();
+ },
- let listener = DebuggerSocket.createListener();
- listener.open(portOrPath);
+ /**
+ * Add a SocketListener instance to the server's set of active
+ * SocketListeners. This is called by a SocketListener after it is opened.
+ */
+ _addListener: function(listener) {
this._listeners.push(listener);
- return listener;
},
/**
* Remove a SocketListener instance from the server's set of active
* SocketListeners. This is called by a SocketListener after it is closed.
*/
_removeListener: function(listener) {
this._listeners = this._listeners.filter(l => l !== listener);
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -29,16 +29,17 @@ EXTRA_JS_MODULES.devtools.server += [
'child.js',
'content-globals.js',
'main.js',
'protocol.js',
]
EXTRA_JS_MODULES.devtools.server.actors += [
'actors/actor-registry.js',
+ 'actors/animation.js',
'actors/call-watcher.js',
'actors/canvas.js',
'actors/child-process.js',
'actors/childtab.js',
'actors/common.js',
'actors/csscoverage.js',
'actors/device.js',
'actors/eventlooplag.js',
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/animation.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<style>
+ .simple-animation {
+ display: inline-block;
+
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: red;
+
+ animation: move 2s infinite;
+ }
+
+ .multiple-animations {
+ display: inline-block;
+
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: #eee;
+
+ animation: move 2s infinite, glow 1s 5;
+ }
+
+ .transition {
+ display: inline-block;
+
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: #f06;
+
+ transition: width 5s;
+ }
+ .transition.get-round {
+ width: 200px;
+ }
+
+ .short-animation {
+ display: inline-block;
+
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: purple;
+
+ animation: move 1s;
+ }
+
+ @keyframes move {
+ 100% {
+ transform: translateY(100px);
+ }
+ }
+
+ @keyframes glow {
+ 100% {
+ background: yellow;
+ }
+ }
+</style>
+<div class="not-animated"></div>
+<div class="simple-animation"></div>
+<div class="multiple-animations"></div>
+<div class="transition"></div>
+<div class="short-animation"></div>
+<script type="text/javascript">
+ // Get the transition started when the page loads
+ var players;
+ addEventListener("load", function() {
+ document.querySelector(".transition").classList.add("get-round");
+ });
+</script>
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -1,23 +1,28 @@
[DEFAULT]
skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
subsuite = devtools
support-files =
head.js
+ animation.html
navigate-first.html
navigate-second.html
storage-dynamic-windows.html
storage-listings.html
storage-unsecured-iframe.html
storage-updates.html
storage-secured-iframe.html
timeline-iframe-child.html
timeline-iframe-parent.html
+[browser_animation_actors_01.js]
+[browser_animation_actors_02.js]
+[browser_animation_actors_03.js]
+[browser_animation_actors_04.js]
[browser_navigateEvents.js]
[browser_storage_dynamic_windows.js]
[browser_storage_listings.js]
[browser_storage_updates.js]
[browser_timeline.js]
skip-if = buildapp == 'mulet'
[browser_timeline_actors.js]
skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_01.js
@@ -0,0 +1,40 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the AnimationsActor
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+ let doc = yield addTab("data:text/html;charset=utf-8,<title>test</title><div></div>");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ let form = yield connectDebuggerClient(client);
+ let inspector = InspectorFront(client, form);
+ let walker = yield inspector.getWalker();
+ let front = AnimationsFront(client, form);
+
+ ok(front, "The AnimationsFront was created");
+ ok(front.getAnimationPlayersForNode, "The getAnimationPlayersForNode method exists");
+
+ let didThrow = false;
+ try {
+ yield front.getAnimationPlayersForNode(null);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "An exception was thrown for a missing NodeActor");
+
+ let invalidNode = yield walker.querySelector(walker.rootNode, "title");
+ let players = yield front.getAnimationPlayersForNode(invalidNode);
+ ok(Array.isArray(players), "An array of players was returned");
+ is(players.length, 0, "0 players have been returned for the invalid node");
+
+ yield closeDebuggerClient(client);
+ gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_02.js
@@ -0,0 +1,62 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getAnimationPlayersForNode
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+ let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ let form = yield connectDebuggerClient(client);
+ let inspector = InspectorFront(client, form);
+ let walker = yield inspector.getWalker();
+ let front = AnimationsFront(client, form);
+
+ yield theRightNumberOfPlayersIsReturned(walker, front);
+ yield playersCanBePausedAndResumed(walker, front);
+
+ yield closeDebuggerClient(client);
+ gBrowser.removeCurrentTab();
+});
+
+function* theRightNumberOfPlayersIsReturned(walker, front) {
+ let node = yield walker.querySelector(walker.rootNode, ".not-animated");
+ let players = yield front.getAnimationPlayersForNode(node);
+ is(players.length, 0, "0 players were returned for the unanimated node");
+
+ node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ players = yield front.getAnimationPlayersForNode(node);
+ is(players.length, 1, "One animation player was returned");
+
+ node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
+ players = yield front.getAnimationPlayersForNode(node);
+ is(players.length, 2, "Two animation players were returned");
+
+ node = yield walker.querySelector(walker.rootNode, ".transition");
+ players = yield front.getAnimationPlayersForNode(node);
+ is(players.length, 1, "One animation player was returned for the transitioned node");
+}
+
+function* playersCanBePausedAndResumed(walker, front) {
+ let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ let [player] = yield front.getAnimationPlayersForNode(node);
+
+ ok(player.initialState, "The player has an initialState");
+ ok(player.getCurrentState, "The player has the getCurrentState method");
+ is(player.initialState.playState, "running", "The animation is currently running");
+
+ yield player.pause();
+ let state = yield player.getCurrentState();
+ is(state.playState, "paused", "The animation is now paused");
+
+ yield player.play();
+ state = yield player.getCurrentState();
+ is(state.playState, "running", "The animation is now running again");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -0,0 +1,79 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's initial state
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+ let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ let form = yield connectDebuggerClient(client);
+ let inspector = InspectorFront(client, form);
+ let walker = yield inspector.getWalker();
+ let front = AnimationsFront(client, form);
+
+ yield playerHasAnInitialState(walker, front);
+ yield playerStateIsCorrect(walker, front);
+
+ yield closeDebuggerClient(client);
+ gBrowser.removeCurrentTab();
+});
+
+function* playerHasAnInitialState(walker, front) {
+ let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ let [player] = yield front.getAnimationPlayersForNode(node);
+
+ ok(player.initialState, "The player front has an initial state");
+ ok("startTime" in player.initialState, "Player's state has startTime");
+ ok("currentTime" in player.initialState, "Player's state has currentTime");
+ ok("playState" in player.initialState, "Player's state has playState");
+ ok("name" in player.initialState, "Player's state has name");
+ ok("duration" in player.initialState, "Player's state has duration");
+ ok("iterationCount" in player.initialState, "Player's state has iterationCount");
+ ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
+}
+
+function* playerStateIsCorrect(walker, front) {
+ info("Checking the state of the simple animation");
+
+ let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ let [player] = yield front.getAnimationPlayersForNode(node);
+ let state = player.initialState;
+
+ is(state.name, "move", "Name is correct");
+ is(state.duration, 2000, "Duration is correct");
+ // null = infinite count
+ is(state.iterationCount, null, "Iteration count is correct");
+ is(state.playState, "running", "PlayState is correct");
+
+ info("Checking the state of the transition");
+
+ node = yield walker.querySelector(walker.rootNode, ".transition");
+ [player] = yield front.getAnimationPlayersForNode(node);
+ state = player.initialState;
+
+ is(state.name, "", "Transition has no name");
+ is(state.duration, 5000, "Transition duration is correct");
+ // transitions run only once
+ is(state.iterationCount, 1, "Transition iteration count is correct");
+ is(state.playState, "running", "Transition playState is correct");
+
+ info("Checking the state of one of multiple animations on a node");
+
+ node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
+ // Checking the 2nd player
+ [, player] = yield front.getAnimationPlayersForNode(node);
+ state = player.initialState;
+
+ is(state.name, "glow", "The 2nd animation's name is correct");
+ is(state.duration, 1000, "The 2nd animation's duration is correct");
+ is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+ is(state.playState, "running", "The 2nd animation's playState is correct");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_04.js
@@ -0,0 +1,58 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's updated state
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+ let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+ initDebuggerServer();
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ let form = yield connectDebuggerClient(client);
+ let inspector = InspectorFront(client, form);
+ let walker = yield inspector.getWalker();
+ let front = AnimationsFront(client, form);
+
+ yield playStateIsUpdatedDynamically(walker, front);
+
+ yield closeDebuggerClient(client);
+ gBrowser.removeCurrentTab();
+});
+
+function* playStateIsUpdatedDynamically(walker, front) {
+ let node = yield walker.querySelector(walker.rootNode, ".short-animation");
+
+ // Restart the animation to make sure we can get the player (it might already
+ // be finished by now). Do this by toggling the class and forcing a sync reflow
+ // using the CPOW.
+ let cpow = content.document.querySelector(".short-animation");
+ cpow.classList.remove("short-animation");
+ let reflow = cpow.offsetWidth;
+ cpow.classList.add("short-animation");
+
+ let [player] = yield front.getAnimationPlayersForNode(node);
+
+ is(player.initialState.playState, "running",
+ "The playState is running while the transition is running");
+
+ info("Wait until the animation stops (more than 1000ms)");
+ yield wait(1500); // Waiting 1.5sec for good measure
+
+ let state = yield player.getCurrentState();
+ is(state.playState, "finished",
+ "The animation has ended and the state has been updated");
+ ok(state.currentTime > player.initialState.currentTime,
+ "The currentTime has been updated");
+}
+
+function wait(ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
--- a/toolkit/devtools/server/tests/browser/browser_timeline.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline.js
@@ -4,17 +4,17 @@
"use strict";
// Test that the timeline front's start/stop/isRecording methods work in a
// simple use case, and that markers events are sent when operations occur.
const {TimelineFront} = require("devtools/server/actors/timeline");
-let test = asyncTest(function*() {
+add_task(function*() {
let doc = yield addTab("data:text/html;charset=utf-8,mop");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = TimelineFront(client, form);
--- a/toolkit/devtools/server/tests/browser/browser_timeline_actors.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline_actors.js
@@ -4,17 +4,17 @@
"use strict";
// Test that the timeline can also record data from the memory and framerate
// actors, emitted as events in tadem with the markers.
const {TimelineFront} = require("devtools/server/actors/timeline");
-let test = asyncTest(function*() {
+add_task(function*() {
let doc = yield addTab("data:text/html;charset=utf-8,mop");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = TimelineFront(client, form);
info("Start timeline marker recording");
--- a/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
@@ -4,17 +4,17 @@
"use strict";
// Test the timeline front receives markers events for operations that occur in
// iframes.
const {TimelineFront} = require("devtools/server/actors/timeline");
-let test = asyncTest(function*() {
+add_task(function*() {
let doc = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html");
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = TimelineFront(client, form);
info("Start timeline marker recording");
--- a/toolkit/devtools/server/tests/browser/head.js
+++ b/toolkit/devtools/server/tests/browser/head.js
@@ -16,23 +16,16 @@ const PATH = "browser/toolkit/devtools/s
const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
// All tests are asynchronous.
waitForExplicitFinish();
/**
- * Define an async test based on a generator function.
- */
-function asyncTest(generator) {
- return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
-}
-
-/**
* Add a new test tab in the browser and load the given url.
* @param {String} url The url to be loaded in the new tab
* @return a promise that resolves to the document when the url is loaded
*/
let addTab = Task.async(function* (url) {
info("Adding a new tab with URL: '" + url + "'");
let tab = gBrowser.selectedTab = gBrowser.addTab();
let loaded = once(gBrowser.selectedBrowser, "load", true);
--- a/toolkit/devtools/server/tests/unit/test_dbgglobal.js
+++ b/toolkit/devtools/server/tests/unit/test_dbgglobal.js
@@ -4,36 +4,36 @@
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
function run_test()
{
// Should get an exception if we try to interact with DebuggerServer
// before we initialize it...
check_except(function() {
- DebuggerServer.openListener(-1);
+ DebuggerServer.createListener();
});
check_except(DebuggerServer.closeAllListeners);
check_except(DebuggerServer.connectPipe);
// Allow incoming connections.
DebuggerServer.init();
// These should still fail because we haven't added a createRootActor
// implementation yet.
check_except(function() {
- DebuggerServer.openListener(-1);
+ DebuggerServer.createListener();
});
check_except(DebuggerServer.closeAllListeners);
check_except(DebuggerServer.connectPipe);
DebuggerServer.registerModule("xpcshell-test/testactors");
// Now they should work.
- DebuggerServer.openListener(-1);
+ DebuggerServer.createListener();
DebuggerServer.closeAllListeners();
// Make sure we got the test's root actor all set up.
let client1 = DebuggerServer.connectPipe();
client1.hooks = {
onPacket: function(aPacket1) {
do_check_eq(aPacket1.from, "root");
do_check_eq(aPacket1.applicationType, "xpcshell-tests");
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_xpcshell_debugging.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the xpcshell-test debug support. Ideally we should have this test
+// next to the xpcshell support code, but that's tricky...
+
+function run_test() {
+ let testFile = do_get_file("xpcshell_debugging_script.js");
+
+ // _setupDebuggerServer is from xpcshell-test's head.js
+ let testResumed = false;
+ let DebuggerServer = _setupDebuggerServer([testFile.path], () => testResumed = true);
+ let transport = DebuggerServer.connectPipe();
+ let client = new DebuggerClient(transport);
+ client.connect(() => {
+ // Even though we have no tabs, listTabs gives us the chromeDebugger.
+ client.listTabs(response => {
+ let chromeDebugger = response.chromeDebugger;
+ client.attachThread(chromeDebugger, (response, threadClient) => {
+ threadClient.addOneTimeListener("paused", (event, packet) => {
+ equal(packet.why.type, "breakpoint",
+ "yay - hit the breakpoint at the first line in our script");
+ // Resume again - next stop should be our "debugger" statement.
+ threadClient.addOneTimeListener("paused", (event, packet) => {
+ equal(packet.why.type, "debuggerStatement",
+ "yay - hit the 'debugger' statement in our script");
+ threadClient.resume(() => {
+ finishClient(client);
+ });
+ });
+ threadClient.resume();
+ });
+ // tell the thread to do the initial resume. This would cause the
+ // xpcshell test harness to resume and load the file under test.
+ threadClient.resume(response => {
+ // should have been told to resume the test itself.
+ ok(testResumed);
+ // Now load our test script.
+ load(testFile.path);
+ // and our "paused" listener above should get hit.
+ });
+ });
+ });
+ });
+ do_test_pending();
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -221,8 +221,10 @@ reason = bug 937197
[test_protocolSpec.js]
[test_registerClient.js]
[test_client_request.js]
[test_monitor_actor.js]
[test_symbols-01.js]
[test_symbols-02.js]
[test_get-executable-lines.js]
[test_get-executable-lines-source-map.js]
+[test_xpcshell_debugging.js]
+support-files = xpcshell_debugging_script.js
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/xpcshell_debugging_script.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a file that test_xpcshell_debugging.js debugs.
+
+// We should hit this dump as it is the first debuggable line
+dump("hello from the debugee!\n")
+
+debugger; // and why not check we hit this!?
--- a/toolkit/devtools/transport/tests/unit/head_dbg.js
+++ b/toolkit/devtools/transport/tests/unit/head_dbg.js
@@ -7,16 +7,17 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;
const { devtools } =
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { Promise: promise } =
Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
const Services = devtools.require("Services");
const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
// We do not want to log packets by default, because in some tests,
// we can be sending large amounts of data. The test harness has
// trouble dealing with logging all the data, and we end up with
// intermittent time outs (e.g. bug 775924).
@@ -253,28 +254,30 @@ function writeTestTempFile(aFileName, aC
} while (aContent.length > 0);
} finally {
stream.close();
}
}
/*** Transport Factories ***/
-function socket_transport() {
+let socket_transport = Task.async(function*() {
if (!DebuggerServer.listeningSockets) {
- let listener = DebuggerServer.openListener(-1);
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = -1 /* any available port */;
listener.allowConnection = () => true;
+ yield listener.open();
}
let port = DebuggerServer._listeners[0].port;
do_print("Debugger server port is " + port);
- return DebuggerClient.socketConnect("127.0.0.1", port);
-}
+ return DebuggerClient.socketConnect({ host: "127.0.0.1", port });
+});
function local_transport() {
- return DebuggerServer.connectPipe();
+ return promise.resolve(DebuggerServer.connectPipe());
}
/*** Sample Data ***/
let gReallyLong;
function really_long() {
if (gReallyLong) {
return gReallyLong;
--- a/toolkit/devtools/transport/tests/unit/test_bulk_error.js
+++ b/toolkit/devtools/transport/tests/unit/test_bulk_error.js
@@ -46,33 +46,33 @@ TestBulkActor.prototype.requestTypes = {
};
function add_test_bulk_actor() {
DebuggerServer.addGlobalActor(TestBulkActor);
}
/*** Tests ***/
-function test_string_error(transportFactory, onReady) {
+let test_string_error = Task.async(function*(transportFactory, onReady) {
let deferred = promise.defer();
- let transport = transportFactory();
+ let transport = yield transportFactory();
let client = new DebuggerClient(transport);
client.connect((app, traits) => {
do_check_eq(traits.bulk, true);
client.listTabs(response => {
deferred.resolve(onReady(client, response).then(() => {
client.close();
transport.close();
}));
});
});
return deferred.promise;
-}
+});
/*** Reply Types ***/
function json_reply(client, response) {
let reallyLong = really_long();
let request = client.startBulkRequest({
actor: response.testBulk,
--- a/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
@@ -129,26 +129,26 @@ let replyHandlers = {
});
return replyDeferred.promise;
}
};
/*** Tests ***/
-function test_bulk_request_cs(transportFactory, actorType, replyType) {
+let test_bulk_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
// Ensure test files are not present from a failed run
cleanup_files();
writeTestTempFile("bulk-input", really_long());
let clientDeferred = promise.defer();
let serverDeferred = promise.defer();
let bulkCopyDeferred = promise.defer();
- let transport = transportFactory();
+ let transport = yield transportFactory();
let client = new DebuggerClient(transport);
client.connect((app, traits) => {
do_check_eq(traits.bulk, true);
client.listTabs(clientDeferred.resolve);
});
clientDeferred.promise.then(response => {
@@ -181,27 +181,27 @@ function test_bulk_request_cs(transportF
}
});
return promise.all([
clientDeferred.promise,
bulkCopyDeferred.promise,
serverDeferred.promise
]);
-}
+});
-function test_json_request_cs(transportFactory, actorType, replyType) {
+let test_json_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
// Ensure test files are not present from a failed run
cleanup_files();
writeTestTempFile("bulk-input", really_long());
let clientDeferred = promise.defer();
let serverDeferred = promise.defer();
- let transport = transportFactory();
+ let transport = yield transportFactory();
let client = new DebuggerClient(transport);
client.connect((app, traits) => {
do_check_eq(traits.bulk, true);
client.listTabs(clientDeferred.resolve);
});
clientDeferred.promise.then(response => {
@@ -222,17 +222,17 @@ function test_json_request_cs(transportF
serverDeferred.resolve();
}
});
return promise.all([
clientDeferred.promise,
serverDeferred.promise
]);
-}
+});
/*** Test Utils ***/
function verify_files() {
let reallyLong = really_long();
let inputFile = getTestTempFile("bulk-input");
let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
@@ -7,92 +7,97 @@ Cu.import("resource://gre/modules/devtoo
let gPort;
let gExtraListener;
function run_test()
{
do_print("Starting test at " + new Date().toTimeString());
initTestDebuggerServer();
- add_test(test_socket_conn);
- add_test(test_socket_shutdown);
+ add_task(test_socket_conn);
+ add_task(test_socket_shutdown);
add_test(test_pipe_conn);
run_next_test();
}
-function test_socket_conn()
+function* test_socket_conn()
{
do_check_eq(DebuggerServer.listeningSockets, 0);
- let listener = DebuggerServer.openListener(-1);
+ let listener = DebuggerServer.createListener();
+ do_check_true(listener);
+ listener.portOrPath = -1 /* any available port */;
listener.allowConnection = () => true;
- do_check_true(listener);
+ listener.open();
do_check_eq(DebuggerServer.listeningSockets, 1);
gPort = DebuggerServer._listeners[0].port;
do_print("Debugger server port is " + gPort);
// Open a second, separate listener
- gExtraListener = DebuggerServer.openListener(-1);
+ gExtraListener = DebuggerServer.createListener();
+ gExtraListener.portOrPath = -1;
gExtraListener.allowConnection = () => true;
+ gExtraListener.open();
do_check_eq(DebuggerServer.listeningSockets, 2);
do_print("Starting long and unicode tests at " + new Date().toTimeString());
let unicodeString = "(╯°□°)╯︵ ┻━┻";
- let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
+ let transport = yield DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: gPort
+ });
+ let closedDeferred = promise.defer();
transport.hooks = {
onPacket: function(aPacket) {
this.onPacket = function(aPacket) {
do_check_eq(aPacket.unicode, unicodeString);
transport.close();
}
// Verify that things work correctly when bigger than the output
// transport buffers and when transporting unicode...
transport.send({to: "root",
type: "echo",
reallylong: really_long(),
unicode: unicodeString});
do_check_eq(aPacket.from, "root");
},
onClosed: function(aStatus) {
- run_next_test();
+ closedDeferred.resolve();
},
};
transport.ready();
+ return closedDeferred.promise;
}
-function test_socket_shutdown()
+function* test_socket_shutdown()
{
do_check_eq(DebuggerServer.listeningSockets, 2);
gExtraListener.close();
do_check_eq(DebuggerServer.listeningSockets, 1);
do_check_true(DebuggerServer.closeAllListeners());
do_check_eq(DebuggerServer.listeningSockets, 0);
// Make sure closing the listener twice does nothing.
do_check_false(DebuggerServer.closeAllListeners());
do_check_eq(DebuggerServer.listeningSockets, 0);
do_print("Connecting to a server socket at " + new Date().toTimeString());
- let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
- transport.hooks = {
- onPacket: function(aPacket) {
- // Shouldn't reach this, should never connect.
- do_check_true(false);
- },
+ try {
+ let transport = yield DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: gPort
+ });
+ } catch(e if e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
+ e.result == Cr.NS_ERROR_NET_TIMEOUT) {
+ // The connection should be refused here, but on slow or overloaded
+ // machines it may just time out.
+ do_check_true(true);
+ return;
+ }
- onClosed: function(aStatus) {
- do_print("test_socket_shutdown onClosed called at " + new Date().toTimeString());
- // The connection should be refused here, but on slow or overloaded
- // machines it may just time out.
- let expected = [ Cr.NS_ERROR_CONNECTION_REFUSED, Cr.NS_ERROR_NET_TIMEOUT ];
- do_check_neq(expected.indexOf(aStatus), -1);
- run_next_test();
- }
- };
-
- do_print("Initializing input stream at " + new Date().toTimeString());
- transport.ready();
+ // Shouldn't reach this, should never connect.
+ do_check_true(false);
}
function test_pipe_conn()
{
let transport = DebuggerServer.connectPipe();
transport.hooks = {
onPacket: function(aPacket) {
do_check_eq(aPacket.from, "root");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
@@ -12,20 +12,20 @@ Cu.import("resource://gre/modules/devtoo
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
const { RawPacket } = devtools.require("devtools/toolkit/transport/packets");
function run_test() {
do_print("Starting test at " + new Date().toTimeString());
initTestDebuggerServer();
- add_test(test_socket_conn_drops_after_invalid_header);
- add_test(test_socket_conn_drops_after_invalid_header_2);
- add_test(test_socket_conn_drops_after_too_large_length);
- add_test(test_socket_conn_drops_after_too_long_header);
+ add_task(test_socket_conn_drops_after_invalid_header);
+ add_task(test_socket_conn_drops_after_invalid_header_2);
+ add_task(test_socket_conn_drops_after_too_large_length);
+ add_task(test_socket_conn_drops_after_too_long_header);
run_next_test();
}
function test_socket_conn_drops_after_invalid_header() {
return test_helper('fluff30:27:{"to":"root","type":"echo"}');
}
function test_socket_conn_drops_after_invalid_header_2() {
@@ -41,31 +41,38 @@ function test_socket_conn_drops_after_to
// The packet header is currently limited to no more than 200 bytes
let rawPacket = '4305724038957487634549823475894325';
for (let i = 0; i < 8; i++) {
rawPacket += rawPacket;
}
return test_helper(rawPacket + ':');
}
-function test_helper(payload) {
- let listener = DebuggerServer.openListener(-1);
+let test_helper = Task.async(function*(payload) {
+ let listener = DebuggerServer.createListener();
+ listener.portOrPath = -1;
listener.allowConnection = () => true;
+ listener.open();
- let transport = DebuggerClient.socketConnect("127.0.0.1", listener.port);
+ let transport = yield DebuggerClient.socketConnect({
+ host: "127.0.0.1",
+ port: listener.port
+ });
+ let closedDeferred = promise.defer();
transport.hooks = {
onPacket: function(aPacket) {
this.onPacket = function(aPacket) {
do_throw(new Error("This connection should be dropped."));
transport.close();
};
// Inject the payload directly into the stream.
transport._outgoing.push(new RawPacket(transport, payload));
transport._flushOutgoing();
},
onClosed: function(aStatus) {
do_check_true(true);
- run_next_test();
+ closedDeferred.resolve();
},
};
transport.ready();
-}
+ return closedDeferred.promise;
+});
--- a/toolkit/devtools/transport/tests/unit/test_no_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_no_bulk.js
@@ -21,28 +21,28 @@ function run_test() {
DebuggerServer.destroy();
});
run_next_test();
}
/*** Tests ***/
-function test_bulk_send_error(transportFactory) {
+let test_bulk_send_error = Task.async(function*(transportFactory) {
let deferred = promise.defer();
- let transport = transportFactory();
+ let transport = yield transportFactory();
let client = new DebuggerClient(transport);
client.connect((app, traits) => {
do_check_false(traits.bulk);
try {
client.startBulkRequest();
do_throw(new Error("Can't use bulk since server doesn't support it"));
} catch(e) {
do_check_true(true);
}
deferred.resolve();
});
return deferred.promise;
-}
+});
--- a/toolkit/devtools/transport/tests/unit/test_queue.js
+++ b/toolkit/devtools/transport/tests/unit/test_queue.js
@@ -20,28 +20,28 @@ function run_test() {
DebuggerServer.destroy();
});
run_next_test();
}
/*** Tests ***/
-function test_transport(transportFactory) {
+let test_transport = Task.async(function*(transportFactory) {
let clientDeferred = promise.defer();
let serverDeferred = promise.defer();
// Ensure test files are not present from a failed run
cleanup_files();
let reallyLong = really_long();
writeTestTempFile("bulk-input", reallyLong);
do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
- let transport = transportFactory();
+ let transport = yield transportFactory();
// Sending from client to server
function write_data({copyFrom}) {
NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
copyFrom(input).then(() => {
input.close();
});
});
@@ -128,17 +128,17 @@ function test_transport(transportFactory
onClosed: function() {
do_throw("Transport closed before we expected");
}
};
transport.ready();
return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
/*** Test Utils ***/
function verify() {
let reallyLong = really_long();
let inputFile = getTestTempFile("bulk-input");
let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
@@ -18,30 +18,30 @@ function run_test() {
run_next_test();
}
/*** Tests ***/
/**
* This tests a one-way bulk transfer at the transport layer.
*/
-function test_bulk_transfer_transport(transportFactory) {
+let test_bulk_transfer_transport = Task.async(function*(transportFactory) {
do_print("Starting bulk transfer test at " + new Date().toTimeString());
let clientDeferred = promise.defer();
let serverDeferred = promise.defer();
// Ensure test files are not present from a failed run
cleanup_files();
let reallyLong = really_long();
writeTestTempFile("bulk-input", reallyLong);
do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
- let transport = transportFactory();
+ let transport = yield transportFactory();
// Sending from client to server
function write_data({copyFrom}) {
NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
copyFrom(input).then(() => {
input.close();
});
});
@@ -99,17 +99,17 @@ function test_bulk_transfer_transport(tr
onClosed: function() {
do_throw("Transport closed before we expected");
}
};
transport.ready();
return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
/*** Test Utils ***/
function verify() {
let reallyLong = really_long();
let inputFile = getTestTempFile("bulk-input");
let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -5,38 +5,43 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"Sqlite",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+// The time to wait before considering a transaction stuck and rejecting it.
+const TRANSACTIONS_QUEUE_TIMEOUT_MS = 120000 // 2 minutes
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
- "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
"@mozilla.org/toolkit/finalizationwitness;1",
"nsIFinalizationWitnessService");
-
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/devtools/Console.jsm");
// Counts the number of created connections per database basename(). This is
// used for logging to distinguish connection instances.
let connectionCounters = new Map();
// Tracks identifiers of wrapped connections, that are Storage connections
// opened through mozStorage and then wrapped by Sqlite.jsm to use its syntactic
// sugar API. Since these connections have an unknown origin, we use this set
@@ -186,17 +191,17 @@ XPCOMUtils.defineLazyGetter(this, "Barri
* OpenedConnection. When the witness detects a garbage collection,
* this object can be used to close the connection.
*
* This object contains more methods than just `close`. When
* OpenedConnection needs to use the methods in this object, it will
* dispatch its method calls here.
*/
function ConnectionData(connection, identifier, options={}) {
- this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." +
+ this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection",
identifier + ": ");
this._log.info("Opened");
this._dbConn = connection;
// This is a unique identifier for the connection, generated through
// getIdentifierByPath. It may be used for logging or as a key in Maps.
this._identifier = identifier;
@@ -209,37 +214,42 @@ function ConnectionData(connection, iden
// A map from statement index to mozIStoragePendingStatement, to allow for
// canceling prior to finalizing the mozIStorageStatements.
this._pendingStatements = new Map();
// Increments for each executed statement for the life of the connection.
this._statementCounter = 0;
- this._inProgressTransaction = null;
+ this._hasInProgressTransaction = false;
+ // Manages a chain of transactions promises, so that new transactions
+ // always happen in queue to the previous ones. It never rejects.
+ this._transactionQueue = Promise.resolve();
this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
if (this._idleShrinkMS) {
this._idleShrinkTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
// We wait for the first statement execute to start the timer because
// shrinking now would not do anything.
}
- this._deferredClose = Promise.defer();
+ // Deferred whose promise is resolved when the connection closing procedure
+ // is complete.
+ this._deferredClose = PromiseUtils.defer();
this._closeRequested = false;
Barriers.connections.client.addBlocker(
this._identifier + ": waiting for shutdown",
this._deferredClose.promise,
() => ({
identifier: this._identifier,
isCloseRequested: this._closeRequested,
hasDbConn: !!this._dbConn,
- hasInProgressTransaction: !!this._inProgressTransaction,
+ hasInProgressTransaction: this._hasInProgressTransaction,
pendingStatements: this._pendingStatements.size,
statementCounter: this._statementCounter,
})
);
}
/**
* Map of connection identifiers to ConnectionData objects
@@ -263,33 +273,26 @@ ConnectionData.prototype = Object.freeze
this._log.debug("Request to close connection.");
this._clearIdleShrinkTimer();
// We need to take extra care with transactions during shutdown.
//
// If we don't have a transaction in progress, we can proceed with shutdown
// immediately.
- if (!this._inProgressTransaction) {
- this._finalize(this._deferredClose);
- return this._deferredClose.promise;
+ if (!this._hasInProgressTransaction) {
+ return this._finalize();
}
- // Else if we do have a transaction in progress, we forcefully roll it
- // back. This is an async task, so we wait on it to finish before
- // performing finalization.
+ // If instead we do have a transaction in progress, it might be rollback-ed
+ // automaticall by closing the connection. Regardless, we wait for its
+ // completion, next enqueued transactions will be rejected.
this._log.warn("Transaction in progress at time of close. Rolling back.");
- let onRollback = this._finalize.bind(this, this._deferredClose);
-
- this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
- this._inProgressTransaction.reject(new Error("Connection being closed."));
- this._inProgressTransaction = null;
-
- return this._deferredClose.promise;
+ return this._transactionQueue.then(() => this._finalize());
},
clone: function (readOnly=false) {
this.ensureOpen();
this._log.debug("Request to clone connection.");
let options = {
@@ -297,17 +300,17 @@ ConnectionData.prototype = Object.freeze
readOnly: readOnly,
};
if (this._idleShrinkMS)
options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
return cloneStorageConnection(options);
},
- _finalize: function (deferred) {
+ _finalize: function () {
this._log.debug("Finalizing connection.");
// Cancel any pending statements.
for (let [k, statement] of this._pendingStatements) {
statement.cancel();
}
this._pendingStatements.clear();
// We no longer need to track these.
@@ -330,26 +333,27 @@ ConnectionData.prototype = Object.freeze
// We must always close the connection at the Sqlite.jsm-level, not
// necessarily at the mozStorage-level.
let markAsClosed = () => {
this._log.info("Closed");
this._dbConn = null;
// Now that the connection is closed, no need to keep
// a blocker for Barriers.connections.
- Barriers.connections.client.removeBlocker(deferred.promise);
- deferred.resolve();
+ Barriers.connections.client.removeBlocker(this._deferredClose.promise);
+ this._deferredClose.resolve();
}
if (wrappedConnections.has(this._identifier)) {
wrappedConnections.delete(this._identifier);
markAsClosed();
} else {
this._log.debug("Calling asyncClose().");
this._dbConn.asyncClose(markAsClosed);
}
+ return this._deferredClose.promise;
},
executeCached: function (sql, params=null, onRow=null) {
this.ensureOpen();
if (!sql) {
throw new Error("sql argument is empty.");
}
@@ -357,35 +361,33 @@ ConnectionData.prototype = Object.freeze
let statement = this._cachedStatements.get(sql);
if (!statement) {
statement = this._dbConn.createAsyncStatement(sql);
this._cachedStatements.set(sql, statement);
}
this._clearIdleShrinkTimer();
- let deferred = Promise.defer();
-
- try {
- this._executeStatement(sql, statement, params, onRow).then(
- result => {
- this._startIdleShrinkTimer();
- deferred.resolve(result);
- },
- error => {
- this._startIdleShrinkTimer();
- deferred.reject(error);
- }
- );
- } catch (ex) {
- this._startIdleShrinkTimer();
- throw ex;
- }
-
- return deferred.promise;
+ return new Promise((resolve, reject) => {
+ try {
+ this._executeStatement(sql, statement, params, onRow).then(
+ result => {
+ this._startIdleShrinkTimer();
+ resolve(result);
+ },
+ error => {
+ this._startIdleShrinkTimer();
+ reject(error);
+ }
+ );
+ } catch (ex) {
+ this._startIdleShrinkTimer();
+ throw ex;
+ }
+ });
},
execute: function (sql, params=null, onRow=null) {
if (typeof(sql) != "string") {
throw new Error("Must define SQL to execute as a string: " + sql);
}
this.ensureOpen();
@@ -397,113 +399,142 @@ ConnectionData.prototype = Object.freeze
this._clearIdleShrinkTimer();
let onFinished = () => {
this._anonymousStatements.delete(index);
statement.finalize();
this._startIdleShrinkTimer();
};
- let deferred = Promise.defer();
-
- try {
- this._executeStatement(sql, statement, params, onRow).then(
- rows => {
- onFinished();
- deferred.resolve(rows);
- },
- error => {
- onFinished();
- deferred.reject(error);
- }
- );
- } catch (ex) {
- onFinished();
- throw ex;
- }
-
- return deferred.promise;
+ return new Promise((resolve, reject) => {
+ try {
+ this._executeStatement(sql, statement, params, onRow).then(
+ rows => {
+ onFinished();
+ resolve(rows);
+ },
+ error => {
+ onFinished();
+ reject(error);
+ }
+ );
+ } catch (ex) {
+ onFinished();
+ throw ex;
+ }
+ });
},
get transactionInProgress() {
- return this._open && !!this._inProgressTransaction;
+ return this._open && this._hasInProgressTransaction;
},
executeTransaction: function (func, type) {
this.ensureOpen();
- if (this._inProgressTransaction) {
- throw new Error("A transaction is already active. Only one transaction " +
- "can be active at a time.");
- }
-
this._log.debug("Beginning transaction");
- let deferred = Promise.defer();
- this._inProgressTransaction = deferred;
- Task.spawn(function doTransaction() {
- // It's tempting to not yield here and rely on the implicit serial
- // execution of issued statements. However, the yield serves an important
- // purpose: catching errors in statement execution.
- yield this.execute("BEGIN " + type + " TRANSACTION");
- let result;
- try {
- result = yield Task.spawn(func);
- } catch (ex) {
- // It's possible that a request to close the connection caused the
- // error.
- // Assertion: close() will unset
- // this._inProgressTransaction when called.
- if (!this._inProgressTransaction) {
- this._log.warn("Connection was closed while performing transaction. " +
- "Received error should be due to closed connection: " +
- CommonUtils.exceptionStr(ex));
- throw ex;
- }
-
- this._log.warn("Error during transaction. Rolling back: " +
- CommonUtils.exceptionStr(ex));
- try {
- yield this.execute("ROLLBACK TRANSACTION");
- } catch (inner) {
- this._log.warn("Could not roll back transaction. This is weird: " +
- CommonUtils.exceptionStr(inner));
- }
-
- throw ex;
+ let promise = this._transactionQueue.then(() => {
+ if (this._closeRequested) {
+ throw new Error("Transaction canceled due to a closed connection.");
}
- // See comment above about connection being closed during transaction.
- if (!this._inProgressTransaction) {
- this._log.warn("Connection was closed while performing transaction. " +
- "Unable to commit.");
- throw new Error("Connection closed before transaction committed.");
- }
+ let transactionPromise = Task.spawn(function* () {
+ // At this point we should never have an in progress transaction, since
+ // they are enqueued.
+ if (this._hasInProgressTransaction) {
+ console.error("Unexpected transaction in progress when trying to start a new one.");
+ }
+ this._hasInProgressTransaction = true;
+ try {
+ // We catch errors in statement execution to detect nested transactions.
+ try {
+ yield this.execute("BEGIN " + type + " TRANSACTION");
+ } catch (ex) {
+ // Unfortunately, if we are wrapping an existing connection, a
+ // transaction could have been started by a client of the same
+ // connection that doesn't use Sqlite.jsm (e.g. C++ consumer).
+ // The best we can do is proceed without a transaction and hope
+ // things won't break.
+ if (wrappedConnections.has(this._identifier)) {
+ this._log.warn("A new transaction could not be started cause the wrapped connection had one in progress: " +
+ CommonUtils.exceptionStr(ex));
+ // Unmark the in progress transaction, since it's managed by
+ // some other non-Sqlite.jsm client. See the comment above.
+ this._hasInProgressTransaction = false;
+ } else {
+ this._log.warn("A transaction was already in progress, likely a nested transaction: " +
+ CommonUtils.exceptionStr(ex));
+ throw ex;
+ }
+ }
- try {
- yield this.execute("COMMIT TRANSACTION");
- } catch (ex) {
- this._log.warn("Error committing transaction: " +
- CommonUtils.exceptionStr(ex));
- throw ex;
- }
+ let result;
+ try {
+ result = yield Task.spawn(func);
+ } catch (ex) {
+ // It's possible that the exception has been caused by trying to
+ // close the connection in the middle of a transaction.
+ if (this._closeRequested) {
+ this._log.warn("Connection closed while performing a transaction: " +
+ CommonUtils.exceptionStr(ex));
+ } else {
+ this._log.warn("Error during transaction. Rolling back: " +
+ CommonUtils.exceptionStr(ex));
+ // If we began a transaction, we must rollback it.
+ if (this._hasInProgressTransaction) {
+ try {
+ yield this.execute("ROLLBACK TRANSACTION");
+ } catch (inner) {
+ this._log.warn("Could not roll back transaction: " +
+ CommonUtils.exceptionStr(inner));
+ }
+ }
+ }
+ // Rethrow the exception.
+ throw ex;
+ }
+
+ // See comment above about connection being closed during transaction.
+ if (this._closeRequested) {
+ this._log.warn("Connection closed before committing the transaction.");
+ throw new Error("Connection closed before committing the transaction.");
+ }
- throw new Task.Result(result);
- }.bind(this)).then(
- function onSuccess(result) {
- this._inProgressTransaction = null;
- deferred.resolve(result);
- }.bind(this),
- function onError(error) {
- this._inProgressTransaction = null;
- deferred.reject(error);
- }.bind(this)
- );
+ // If we began a transaction, we must commit it.
+ if (this._hasInProgressTransaction) {
+ try {
+ yield this.execute("COMMIT TRANSACTION");
+ } catch (ex) {
+ this._log.warn("Error committing transaction: " +
+ CommonUtils.exceptionStr(ex));
+ throw ex;
+ }
+ }
- return deferred.promise;
+ return result;
+ } finally {
+ this._hasInProgressTransaction = false;
+ }
+ }.bind(this));
+
+ // If a transaction yields on a never resolved promise, or is mistakenly
+ // nested, it could hang the transactions queue forever. Thus we timeout
+ // the execution after a meaningful amount of time, to ensure in any case
+ // we'll proceed after a while.
+ let timeoutPromise = new Promise((resolve, reject) => {
+ setTimeout(() => reject(new Error("Transaction timeout, most likely caused by unresolved pending work.")),
+ TRANSACTIONS_QUEUE_TIMEOUT_MS);
+ });
+ return Promise.race([transactionPromise, timeoutPromise]);
+ });
+ // Atomically update the queue before anyone else has a chance to enqueue
+ // further transactions.
+ this._transactionQueue = promise.catch(ex => { console.error(ex) });
+ return promise;
},
shrinkMemory: function () {
this._log.info("Shrinking memory usage.");
let onShrunk = this._clearIdleShrinkTimer.bind(this);
return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk);
},
@@ -570,17 +601,17 @@ ConnectionData.prototype = Object.freeze
if (onRow && typeof(onRow) != "function") {
throw new Error("onRow must be a function. Got: " + onRow);
}
this._bindParameters(statement, params);
let index = this._statementCounter++;
- let deferred = Promise.defer();
+ let deferred = PromiseUtils.defer();
let userCancelled = false;
let errors = [];
let rows = [];
let handledRow = false;
// Don't incur overhead for serializing params unless the messages go
// somewhere.
if (this._log.level <= Log.Level.Trace) {
@@ -733,17 +764,16 @@ function openConnection(options) {
if (!options.path) {
throw new Error("path not specified in connection options.");
}
if (isClosed) {
throw new Error("Sqlite.jsm has been shutdown. Cannot open connection to: " + options.path);
}
-
// Retains absolute paths and normalizes relative as relative to profile.
let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
let sharedMemoryCache = "sharedMemoryCache" in options ?
options.sharedMemoryCache : true;
let openedOptions = {};
@@ -756,40 +786,41 @@ function openConnection(options) {
openedOptions.shrinkMemoryOnConnectionIdleMS =
options.shrinkMemoryOnConnectionIdleMS;
}
let file = FileUtils.File(path);
let identifier = getIdentifierByPath(path);
log.info("Opening database: " + path + " (" + identifier + ")");
- let deferred = Promise.defer();
- let dbOptions = null;
- if (!sharedMemoryCache) {
- dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
- createInstance(Ci.nsIWritablePropertyBag);
- dbOptions.setProperty("shared", false);
- }
- Services.storage.openAsyncDatabase(file, dbOptions, function(status, connection) {
- if (!connection) {
- log.warn("Could not open connection: " + status);
- deferred.reject(new Error("Could not open connection: " + status));
- return;
+
+ return new Promise((resolve, reject) => {
+ let dbOptions = null;
+ if (!sharedMemoryCache) {
+ dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ dbOptions.setProperty("shared", false);
}
- log.info("Connection opened");
- try {
- deferred.resolve(
- new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
- identifier, openedOptions));
- } catch (ex) {
- log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
- deferred.reject(ex);
- }
+ Services.storage.openAsyncDatabase(file, dbOptions, (status, connection) => {
+ if (!connection) {
+ log.warn("Could not open connection: " + status);
+ reject(new Error("Could not open connection: " + status));
+ return;
+ }
+ log.info("Connection opened");
+ try {
+ resolve(
+ new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
+ identifier, openedOptions));
+ } catch (ex) {
+ log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
+ reject(ex);
+ }
+ });
});
- return deferred.promise;
}
/**
* Creates a clone of an existing and open Storage connection. The clone has
* the same underlying characteristics of the original connection and is
* returned in form of an OpenedConnection handle.
*
* The following parameters can control the cloned connection:
@@ -841,33 +872,33 @@ function cloneStorageConnection(options)
openedOptions.shrinkMemoryOnConnectionIdleMS =
options.shrinkMemoryOnConnectionIdleMS;
}
let path = source.databaseFile.path;
let identifier = getIdentifierByPath(path);
log.info("Cloning database: " + path + " (" + identifier + ")");
- let deferred = Promise.defer();
- source.asyncClone(!!options.readOnly, (status, connection) => {
- if (!connection) {
- log.warn("Could not clone connection: " + status);
- deferred.reject(new Error("Could not clone connection: " + status));
- }
- log.info("Connection cloned");
- try {
- let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
- deferred.resolve(new OpenedConnection(conn, identifier, openedOptions));
- } catch (ex) {
- log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
- deferred.reject(ex);
- }
+ return new Promise((resolve, reject) => {
+ source.asyncClone(!!options.readOnly, (status, connection) => {
+ if (!connection) {
+ log.warn("Could not clone connection: " + status);
+ reject(new Error("Could not clone connection: " + status));
+ }
+ log.info("Connection cloned");
+ try {
+ let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
+ resolve(new OpenedConnection(conn, identifier, openedOptions));
+ } catch (ex) {
+ log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
+ reject(ex);
+ }
+ });
});
- return deferred.promise;
}
/**
* Wraps an existing and open Storage connection with Sqlite.jsm API. The
* wrapped connection clone has the same underlying characteristics of the
* original connection and is returned in form of an OpenedConnection handle.
*
* Clients are responsible for closing both the Sqlite.jsm wrapper and the
@@ -1150,16 +1181,31 @@ OpenedConnection.prototype = Object.free
*/
get transactionInProgress() {
return this._connectionData.transactionInProgress;
},
/**
* Perform a transaction.
*
+ * *****************************************************************************
+ * YOU SHOULD _NEVER_ NEST executeTransaction CALLS FOR ANY REASON, NOR
+ * DIRECTLY, NOR THROUGH OTHER PROMISES.
+ * FOR EXAMPLE, NEVER DO SOMETHING LIKE:
+ * yield executeTransaction(function* () {
+ * ...some_code...
+ * yield executeTransaction(function* () { // WRONG!
+ * ...some_code...
+ * })
+ * yield someCodeThatExecuteTransaction(); // WRONG!
+ * yield neverResolvedPromise; // WRONG!
+ * });
+ * NESTING CALLS WILL BLOCK ANY FUTURE TRANSACTION UNTIL A TIMEOUT KICKS IN.
+ * *****************************************************************************
+ *
* A transaction is specified by a user-supplied function that is a
* generator function which can be used by Task.jsm's Task.spawn(). The
* function receives this connection instance as its argument.
*
* The supplied function is expected to yield promises. These are often
* promises created by calling `execute` and `executeCached`. If the
* generator is exhausted without any errors being thrown, the
* transaction is committed. If an error occurs, the transaction is
--- a/toolkit/modules/tests/xpcshell/test_sqlite.js
+++ b/toolkit/modules/tests/xpcshell/test_sqlite.js
@@ -45,77 +45,77 @@ function getConnection(dbName, extraOpti
let options = {path: path};
for (let [k, v] in Iterator(extraOptions)) {
options[k] = v;
}
return Sqlite.openConnection(options);
}