Merge fx-team to m-c a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2015-05-18 17:24:16 -07:00
commit 6b7e8dce5b
36 changed files with 5018 additions and 1958 deletions

View File

@ -1378,7 +1378,7 @@ pref("devtools.toolbox.splitconsoleHeight", 100);
// Toolbox Button preferences
pref("devtools.command-button-pick.enabled", true);
pref("devtools.command-button-frames.enabled", false);
pref("devtools.command-button-frames.enabled", true);
pref("devtools.command-button-splitconsole.enabled", true);
pref("devtools.command-button-paintflashing.enabled", false);
pref("devtools.command-button-tilt.enabled", false);

View File

@ -55,6 +55,35 @@ XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
"resource:///modules/Pocket.jsm");
// Can't use XPCOMUtils for these because the scripts try to define the variables
// on window, and so the defineProperty inside defineLazyGetter fails.
Object.defineProperty(window, "pktApi", {
get: function() {
// Avoid this getter running again:
delete window.pktApi;
Services.scriptloader.loadSubScript("chrome://browser/content/pocket/pktApi.js", window);
return window.pktApi;
},
configurable: true,
enumerable: true
});
function pktUIGetter(prop) {
return {
get: function() {
// Avoid either of these getters running again:
delete window.pktUI;
delete window.pktUIMessaging;
Services.scriptloader.loadSubScript("chrome://browser/content/pocket/main.js", window);
return window[prop];
},
configurable: true,
enumerable: true
};
}
Object.defineProperty(window, "pktUI", pktUIGetter("pktUI"));
Object.defineProperty(window, "pktUIMessaging", pktUIGetter("pktUIMessaging"));
const nsIWebNavigation = Ci.nsIWebNavigation;
var gLastBrowserCharset = null;
@ -4171,7 +4200,6 @@ var XULBrowserWindow = {
BookmarkingUI.onLocationChange();
SocialUI.updateState(location);
UITour.onLocationChange(location);
Pocket.onLocationChange(browser, aLocationURI);
}
// Utility functions for disabling find

View File

@ -1307,7 +1307,4 @@
# starting with an empty iframe here in browser.xul from a Ts standpoint.
</deck>
<script type="application/javascript" src="chrome://browser/content/pocket/pktApi.js"/>
<script type="application/javascript" src="chrome://browser/content/pocket/main.js"/>
</window>

View File

@ -77,8 +77,8 @@ add_task(function* checkAllTheJS() {
" browser/base/content/test/general/browser_parsable_script.js");
return;
}
// Request a 10 minutes timeout (30 seconds * 20) for debug builds.
requestLongerTimeout(20);
// Request a 15 minutes timeout (30 seconds * 30) for debug builds.
requestLongerTimeout(30);
}
let uris;

View File

@ -2176,9 +2176,9 @@ let CustomizableUIInternal = {
// current placement settings.
// This allows a widget to be both built-in by default but also able to be
// destroyed based on criteria that may not be available when the widget is
// created -- for example, because some other feature in the browser
// supersedes the widget.
// destroyed and removed from the area based on criteria that may not be
// available when the widget is created -- for example, because some other
// feature in the browser supersedes the widget.
let conditionalDestroyPromise = aData.conditionalDestroyPromise || null;
delete aData.conditionalDestroyPromise;
@ -2195,6 +2195,7 @@ let CustomizableUIInternal = {
conditionalDestroyPromise.then(shouldDestroy => {
if (shouldDestroy) {
this.destroyWidget(widget.id);
this.removeWidgetFromArea(widget.id);
}
}, err => {
Cu.reportError(err);

View File

@ -1106,8 +1106,13 @@ if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
viewId: "PanelUI-pocketView",
label: label,
tooltiptext: tooltiptext,
onViewShowing: Pocket.onPanelViewShowing,
onViewHiding: Pocket.onPanelViewHiding,
// Use forwarding functions here to avoid loading Pocket.jsm on startup:
onViewShowing: function() {
return Pocket.onPanelViewShowing.apply(this, arguments);
},
onViewHiding: function() {
return Pocket.onPanelViewHiding.apply(this, arguments);
},
// If the user has the "classic" Pocket add-on installed, use that instead
// and destroy the widget.

View File

@ -6,8 +6,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
"resource:///modules/ScrollbarSampler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
"resource:///modules/Pocket.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",

View File

@ -865,6 +865,12 @@
display: block;
}
.OT_audio-only.OT_publisher .OT_video-element,
.OT_audio-only.OT_subscriber .OT_video-element {
display: none;
}
.OT_video-disabled-indicator {
opacity: 1;
border: none;

File diff suppressed because it is too large Load Diff

View File

@ -81,9 +81,7 @@ LoopRoomsCache.prototype = {
try {
return (this._cache = yield CommonUtils.readJSON(this.path));
} catch(error) {
// This is really complex due to OSFile's error handling, see bug 1160109.
if ((OS.Constants.libc && error.unixErrno != OS.Constants.libc.ENOENT) ||
(OS.Constants.Win && error.winLastError != OS.Constants.Win.ERROR_FILE_NOT_FOUND)) {
if (!error.becauseNoSuchFile) {
MozLoopService.log.debug("Error reading the cache:", error);
}
return (this._cache = {});

View File

@ -66,30 +66,6 @@ let Pocket = {
window.pktUI.pocketPanelDidHide(event);
},
// Called on tab/urlbar/location changes and after customization. Update
// anything that is tab specific.
onLocationChange(browser, locationURI) {
if (!locationURI) {
return;
}
let widget = CustomizableUI.getWidget("pocket-button");
for (let instance of widget.instances) {
let node = instance.node;
if (!node ||
node.ownerDocument != browser.ownerDocument) {
continue;
}
if (node) {
let win = browser.ownerDocument.defaultView;
node.disabled = win.pktApi.isUserLoggedIn() &&
!locationURI.schemeIs("http") &&
!locationURI.schemeIs("https") &&
!(locationURI.schemeIs("about") &&
locationURI.spec.toLowerCase().startsWith("about:reader?url="));
}
}
},
_urlToSave: null,
_titleToSave: null,
savePage(browser, url, title) {

View File

@ -293,7 +293,11 @@ InternalScheduler.prototype = {
// the last success.
prefs.set("lastSync", new Date().toString());
this.state = this.STATE_OK;
this._logManager.resetFileLog(this._logManager.REASON_SUCCESS);
this._logManager.resetFileLog().then(result => {
if (result == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
}
});
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
this._currentErrorBackoff = 0; // error retry interval is reset on success.
return intervals.schedule;
@ -307,7 +311,11 @@ InternalScheduler.prototype = {
this._currentErrorBackoff = 0; // error retry interval is reset on success.
this.log.info("Can't sync due to FxA account state " + err.message);
this.state = this.STATE_OK;
this._logManager.resetFileLog(this._logManager.REASON_SUCCESS);
this._logManager.resetFileLog().then(result => {
if (result == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Reading List sync encountered an error - see about:sync-log for the log file.");
}
});
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
// it's unfortunate that we are probably going to hit this every
// 2 hours, but it should be invisible to the user.
@ -317,7 +325,7 @@ InternalScheduler.prototype = {
this.STATE_ERROR_AUTHENTICATION : this.STATE_ERROR_OTHER;
this.log.error("Sync failed, now in state '${state}': ${err}",
{state: this.state, err});
this._logManager.resetFileLog(this._logManager.REASON_ERROR);
this._logManager.resetFileLog();
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
// We back-off on error retries until it hits our normally scheduled interval.
this._currentErrorBackoff = this._currentErrorBackoff == 0 ? intervals.retry :

View File

@ -3,15 +3,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
Cu.import("resource://gre/modules/Services.jsm");
let temp = {}
Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
let DevTools = temp.DevTools;
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", temp);
let LayoutHelpers = temp.LayoutHelpers;
Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
let devtools = temp.devtools;
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let Toolbox = devtools.Toolbox;
@ -40,19 +32,27 @@ function test() {
let json = JSON.parse(event.data);
if (json.name == "toolbox-close") {
ok("Got the `toolbox-close` message");
window.removeEventListener("message", onMessage);
cleanup();
}
}
function testCustomHost(toolbox) {
function testCustomHost(t) {
toolbox = t;
is(toolbox.doc.defaultView.top, window, "Toolbox is included in browser.xul");
is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
executeSoon(() => gBrowser.removeCurrentTab());
}
function cleanup() {
window.removeEventListener("message", onMessage);
iframe.remove();
finish();
// Even if we received "toolbox-close", the toolbox may still be destroying
// toolbox.destroy() returns a singleton promise that ensures
// everything is cleaned up before proceeding.
toolbox.destroy().then(() => {
toolbox = iframe = target = tab = null;
finish();
});
}
}

View File

@ -37,11 +37,15 @@ function start() {
function testSelectTool(aToolbox) {
toolbox = aToolbox;
toolbox.once("options-selected", testRegisterFails);
toolbox.once("options-selected", () => {
testRegisterFails().then(testRegisterInstallingWorker);
});
toolbox.selectTool("options");
}
function testRegisterFails() {
let deferred = promise.defer();
let output = doc.getElementById("output");
let button = doc.getElementById("button");
@ -50,7 +54,7 @@ function testRegisterFails() {
is(output.textContent,
"SecurityError",
"SecurityError expected");
testRegisterInstallingWorker();
deferred.resolve();
}
if (output.textContent !== "No output") {
@ -61,6 +65,8 @@ function testRegisterFails() {
button.removeEventListener('click', onClick);
doTheCheck();
});
return deferred.promise;
}
function testRegisterInstallingWorker() {
@ -73,7 +79,7 @@ function testRegisterInstallingWorker() {
is(output.textContent,
"Installing worker/",
"Installing worker expected");
toggleServiceWorkersTestingCheckbox().then(finishUp);
testRegisterFailsWhenToolboxCloses();
}
if (output.textContent !== "No output") {
@ -87,6 +93,30 @@ function testRegisterInstallingWorker() {
});
}
// Workers should be turned back off when we closes the toolbox
function testRegisterFailsWhenToolboxCloses() {
info("Testing it disable worker when closing the toolbox");
toolbox.destroy()
.then(reload)
.then(testRegisterFails)
.then(finishUp);
}
function reload() {
let deferred = promise.defer();
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
doc = content.document;
deferred.resolve();
}, true);
let mm = getFrameScript();
mm.sendAsyncMessage("devtools:test:reload");
return deferred.promise;
}
function toggleServiceWorkersTestingCheckbox() {
let deferred = promise.defer();
@ -101,24 +131,13 @@ function toggleServiceWorkersTestingCheckbox() {
info("Checking checkbox to enable service workers testing");
}
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
doc = content.document;
deferred.resolve();
}, true);
cbx.click();
let mm = getFrameScript();
mm.sendAsyncMessage("devtools:test:reload");
return deferred.promise;
return reload();
}
function finishUp() {
toolbox.destroy().then(function() {
gBrowser.removeCurrentTab();
toolbox = doc = null;
finish();
});
gBrowser.removeCurrentTab();
toolbox = doc = null;
finish();
}

View File

@ -316,8 +316,8 @@ OptionsPanel.prototype = {
if (this.target.activeTab) {
this.target.client.attachTab(this.target.activeTab._actor, (response) => {
this._origJavascriptEnabled = response.javascriptEnabled;
this.disableJSNode.checked = !this._origJavascriptEnabled;
this._origJavascriptEnabled = !response.javascriptEnabled;
this.disableJSNode.checked = this._origJavascriptEnabled;
this.disableJSNode.addEventListener("click", this._disableJSClicked, false);
});
} else {
@ -370,25 +370,29 @@ OptionsPanel.prototype = {
}
let deferred = promise.defer();
this.destroyPromise = deferred.promise;
this._removeListeners();
if (this.target.activeTab) {
this.disableJSNode.removeEventListener("click", this._disableJSClicked, false);
// If JavaScript is disabled we need to revert it to it's original value.
let options = {
"javascriptEnabled": this._origJavascriptEnabled
};
this.target.activeTab.reconfigure(options, () => {
this.toolbox = null;
this.disableJSNode.removeEventListener("click", this._disableJSClicked);
// FF41+ automatically cleans up state in actor on disconnect
if (!this.target.activeTab.traits.noTabReconfigureOnClose) {
let options = {
"javascriptEnabled": this._origJavascriptEnabled,
"performReload": false
};
this.target.activeTab.reconfigure(options, deferred.resolve);
} else {
deferred.resolve();
}, true);
}
} else {
deferred.resolve();
}
this.panelWin = this.panelDoc = this.disableJSNode = null;
this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
return deferred.promise;
return this.destroyPromise;
}
};

View File

@ -54,7 +54,6 @@ function setPrefDefaults() {
Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
}
window.addEventListener("load", function() {

View File

@ -1753,7 +1753,8 @@ Toolbox.prototype = {
// Now that we are closing the toolbox we can re-enable the cache settings
// and disable the service workers testing settings for the current tab.
if (this.target.activeTab) {
// FF41+ automatically cleans up state in actor on disconnect.
if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
this.target.activeTab.reconfigure({
"cacheDisabled": false,
"serviceWorkersTestingEnabled": false

View File

@ -1502,10 +1502,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
let nameWithQuery = this._getUriNameWithQuery(uri);
let hostPort = this._getUriHostPort(uri);
let unicodeUrl = NetworkHelper.convertToUnicode(unescape(uri.spec));
let file = $(".requests-menu-file", target);
file.setAttribute("value", nameWithQuery);
file.setAttribute("tooltiptext", nameWithQuery);
file.setAttribute("tooltiptext", unicodeUrl);
let domain = $(".requests-menu-domain", target);
domain.setAttribute("value", hostPort);
@ -1986,7 +1987,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
if (!(aUrl instanceof Ci.nsIURL)) {
aUrl = nsIURL(aUrl);
}
let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName)) || "/";
let name = NetworkHelper.convertToUnicode(unescape(aUrl.fileName || aUrl.filePath || "/"));
let query = NetworkHelper.convertToUnicode(unescape(aUrl.query));
return name + (query ? "?" + query : "");
},

View File

@ -24,6 +24,7 @@ support-files =
html_sorting-test-page.html
html_statistics-test-page.html
html_status-codes-test-page.html
html_api-calls-test-page.html
html_copy-as-curl.html
html_curl-utils.html
sjs_content-type-test-server.sjs
@ -37,6 +38,7 @@ support-files =
[browser_net_aaa_leaktest.js]
[browser_net_accessibility-01.js]
[browser_net_accessibility-02.js]
[browser_net_api-calls.js]
[browser_net_autoscroll.js]
[browser_net_cached-status.js]
[browser_net_charts-01.js]

View File

@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests whether API call URLs (without a filename) are correctly displayed (including Unicode)
*/
function test() {
initNetMonitor(API_CALLS_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { document, L10N, EVENTS, Editor, NetMonitorView } = aMonitor.panelWin;
let { RequestsMenu, NetworkDetails } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
const REQUEST_URIS = [
"http://example.com/api/fileName.xml",
"http://example.com/api/file%E2%98%A2.xml",
"http://example.com/api/ascii/get/",
"http://example.com/api/unicode/%E2%98%A2/",
"http://example.com/api/search/?q=search%E2%98%A2"
];
Task.spawn(function*() {
yield waitForNetworkEvents(aMonitor, 5);
REQUEST_URIS.forEach(function(uri, index) {
verifyRequestItemTarget(RequestsMenu.getItemAtIndex(index), "GET", uri);
});
yield teardown(aMonitor);
finish();
});
aDebuggee.performRequests();
});
}

View File

@ -14,7 +14,7 @@ function test() {
RequestsMenu.lazyUpdate = false;
Task.spawn(function () {
Task.spawn(function*() {
yield waitForNetworkEvents(aMonitor, 1, 6);
RequestsMenu.selectedItem = RequestsMenu.getItemAtIndex(0);

View File

@ -10,11 +10,13 @@ let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { CurlUtils } = Cu.import("resource:///modules/devtools/Curl.jsm", {});
let NetworkHelper = devtools.require("devtools/toolkit/webconsole/network-helper");
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/";
const API_CALLS_URL = EXAMPLE_URL + "html_api-calls-test-page.html";
const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html";
const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html";
const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
@ -278,8 +280,9 @@ function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
let { attachment, target } = aRequestItem
let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
let name = uri.fileName || "/";
let query = uri.query;
let unicodeUrl = NetworkHelper.convertToUnicode(unescape(aUrl));
let name = NetworkHelper.convertToUnicode(unescape(uri.fileName || uri.filePath || "/"));
let query = NetworkHelper.convertToUnicode(unescape(uri.query));
let hostPort = uri.hostPort;
let remoteAddress = attachment.remoteAddress;
@ -297,13 +300,13 @@ function verifyRequestItemTarget(aRequestItem, aMethod, aUrl, aData = {}) {
if (fuzzyUrl) {
ok(target.querySelector(".requests-menu-file").getAttribute("value").startsWith(
name + (query ? "?" + query : "")), "The displayed file is correct.");
ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(
name + (query ? "?" + query : "")), "The tooltip file is correct.");
ok(target.querySelector(".requests-menu-file").getAttribute("tooltiptext").startsWith(unicodeUrl),
"The tooltip file is correct.");
} else {
is(target.querySelector(".requests-menu-file").getAttribute("value"),
name + (query ? "?" + query : ""), "The displayed file is correct.");
is(target.querySelector(".requests-menu-file").getAttribute("tooltiptext"),
name + (query ? "?" + query : ""), "The tooltip file is correct.");
unicodeUrl, "The tooltip file is correct.");
}
is(target.querySelector(".requests-menu-domain").getAttribute("value"),

View File

@ -0,0 +1,46 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>Network Monitor test page</title>
</head>
<body>
<p>API calls request test</p>
<script type="text/javascript">
function get(aAddress, aCallback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", aAddress, true);
xhr.onreadystatechange = function() {
if (this.readyState == this.DONE) {
aCallback();
}
};
xhr.send();
}
function performRequests() {
get("/api/fileName.xml", function() {
get("/api/file%E2%98%A2.xml", function() {
get("/api/ascii/get/", function() {
get("/api/unicode/%E2%98%A2/", function() {
get("/api/search/?q=search%E2%98%A2", function() {
// Done.
});
});
});
});
});
}
</script>
</body>
</html>

View File

@ -131,11 +131,13 @@ StyleEditorPanel.prototype = {
this._target.off("close", this.destroy);
this._target = null;
this._toolbox = null;
this._panelWin = null;
this._panelDoc = null;
this._debuggee.destroy();
this._debuggee = null;
this.UI.destroy();
this.UI = null;
}
return promise.resolve(null);

View File

@ -234,7 +234,14 @@ ElementStyle.prototype = {
return null;
});
}).then(null, promiseWarn);
}).then(null, e => {
// populate is often called after a setTimeout,
// the connection may already be closed.
if (this.destroyed) {
return;
}
return promiseWarn(e);
});
this.populated = populated;
return this.populated;
},

View File

@ -5,8 +5,7 @@
<script type="text/javascript">
function test() {
console.log("start");
console.clear()
console.timeStamp()
console.clear();
console.log("end");
}
</script>

View File

@ -1334,6 +1334,11 @@ WebConsoleFrame.prototype = {
break;
}
case "timeStamp": {
// console.timeStamp() doesn't need to display anything.
return null;
}
default:
Cu.reportError("Unknown Console API log level: " + level);
return null;

View File

@ -45,15 +45,97 @@ let consoleAppender;
// A set of all preference roots used by all instances.
let allBranches = new Set();
// A storage appender that is flushable to a file on disk. Policies for
// when to flush, to what file, log rotation etc are up to the consumer
// (although it does maintain a .sawError property to help the consumer decide
// based on its policies)
function FlushableStorageAppender(formatter) {
Log.StorageStreamAppender.call(this, formatter);
this.sawError = false;
}
FlushableStorageAppender.prototype = {
__proto__: Log.StorageStreamAppender.prototype,
append(message) {
if (message.level >= Log.Level.Error) {
this.sawError = true;
}
Log.StorageStreamAppender.prototype.append.call(this, message);
},
reset() {
Log.StorageStreamAppender.prototype.reset.call(this);
this.sawError = false;
},
// Flush the current stream to a file. Somewhat counter-intuitively, you
// must pass a log which will be written to with details of the operation.
flushToFile: Task.async(function* (subdirArray, filename, log) {
let inStream = this.getInputStream();
this.reset();
if (!inStream) {
log.debug("Failed to flush log to a file - no input stream");
return;
}
log.debug("Flushing file log");
log.trace("Beginning stream copy to " + filename + ": " + Date.now());
try {
yield this._copyStreamToFile(inStream, subdirArray, filename, log);
log.trace("onCopyComplete", Date.now());
} catch (ex) {
log.error("Failed to copy log stream to file", ex);
}
}),
/**
* Copy an input stream to the named file, doing everything off the main
* thread.
* subDirArray is an array of path components, relative to the profile
* directory, where the file will be created.
* outputFileName is the filename to create.
* Returns a promise that is resolved on completion or rejected with an error.
*/
_copyStreamToFile: Task.async(function* (inputStream, subdirArray, outputFileName, log) {
// The log data could be large, so we don't want to pass it all in a single
// message, so use BUFFER_SIZE chunks.
const BUFFER_SIZE = 8192;
// get a binary stream
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(inputStream);
let outputDirectory = OS.Path.join(OS.Constants.Path.profileDir, ...subdirArray);
yield OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir });
let fullOutputFileName = OS.Path.join(outputDirectory, outputFileName);
let output = yield OS.File.open(fullOutputFileName, { write: true} );
try {
while (true) {
let available = binaryStream.available();
if (!available) {
break;
}
let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
yield output.write(new Uint8Array(chunk));
}
} finally {
try {
binaryStream.close(); // inputStream is closed by the binaryStream
yield output.close();
} catch (ex) {
log.error("Failed to close the input stream", ex);
}
}
log.trace("finished copy to", fullOutputFileName);
}),
}
// The public LogManager object.
function LogManager(prefRoot, logNames, logFilePrefix) {
this.init(prefRoot, logNames, logFilePrefix);
}
LogManager.prototype = {
REASON_SUCCESS: "success",
REASON_ERROR: "error",
_cleaningUpFileLogs: false,
_prefObservers: [],
@ -102,11 +184,11 @@ LogManager.prototype = {
return observer;
}
this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Error, true);
this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Fatal, true);
this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true);
// The file appender doesn't get the special singleton behaviour.
let fapp = this._fileAppender = new Log.StorageStreamAppender(formatter);
let fapp = this._fileAppender = new FlushableStorageAppender(formatter);
// the stream gets a default of Debug as the user must go out of their way
// to see the stuff spewed to it.
this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug);
@ -150,109 +232,62 @@ LogManager.prototype = {
return ["weave", "logs"];
},
/**
* Copy an input stream to the named file, doing everything off the main
* thread.
* outputFileName is a string with the tail of the filename - the file will
* be created in the log directory.
* Returns a promise that is resolved<undefined> on completion or rejected if
* there is an error.
*/
_copyStreamToFile: Task.async(function* (inputStream, outputFileName) {
// The log data could be large, so we don't want to pass it all in a single
// message, so use BUFFER_SIZE chunks.
const BUFFER_SIZE = 8192;
// get a binary stream
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(inputStream);
// We assume the profile directory exists, but not that the dirs under it do.
let profd = FileUtils.getDir("ProfD", []);
let outputFile = FileUtils.getDir("ProfD", this._logFileSubDirectoryEntries);
yield OS.File.makeDir(outputFile.path, { ignoreExisting: true, from: profd.path });
outputFile.append(outputFileName);
let output = yield OS.File.open(outputFile.path, { write: true} );
try {
while (true) {
let available = binaryStream.available();
if (!available) {
break;
}
let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE));
yield output.write(new Uint8Array(chunk));
}
} finally {
try {
binaryStream.close(); // inputStream is closed by the binaryStream
yield output.close();
} catch (ex) {
this._log.error("Failed to close the input stream", ex);
}
}
this._log.trace("finished copy to", outputFile.path);
}),
// Result values for resetFileLog.
SUCCESS_LOG_WRITTEN: "success-log-written",
ERROR_LOG_WRITTEN: "error-log-written",
/**
* Possibly generate a log file for all accumulated log messages and refresh
* the input & output streams.
* Returns a promise that resolves on completion or rejects if the file could
* not be written.
* Whether a "success" or "error" log is written is determined based on
* whether an "Error" log entry was written to any of the logs.
* Returns a promise that resolves on completion with either null (for no
* file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was
* written, or ERROR_LOG_WRITTEN if an "error" log was written.
*/
resetFileLog: Task.async(function* (reason) {
resetFileLog: Task.async(function* () {
try {
let flushToFile;
let reasonPrefix;
switch (reason) {
case this.REASON_SUCCESS:
flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false);
reasonPrefix = "success";
break;
case this.REASON_ERROR:
flushToFile = this._prefs.get("log.appender.file.logOnError", true);
reasonPrefix = "error";
break;
default:
throw new Error("Invalid reason");
let reason;
if (this._fileAppender.sawError) {
reason = this.ERROR_LOG_WRITTEN;
flushToFile = this._prefs.get("log.appender.file.logOnError", true);
reasonPrefix = "error";
} else {
reason = this.SUCCESS_LOG_WRITTEN;
flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false);
reasonPrefix = "success";
}
// might as well avoid creating an input stream if we aren't going to use it.
if (!flushToFile) {
this._fileAppender.reset();
return;
return null;
}
let inStream = this._fileAppender.getInputStream();
this._fileAppender.reset();
if (inStream) {
this._log.debug("Flushing file log");
// We have reasonPrefix at the start of the filename so all "error"
// logs are grouped in about:sync-log.
let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
this._log.trace("Beginning stream copy to " + filename + ": " +
Date.now());
try {
yield this._copyStreamToFile(inStream, filename);
this._log.trace("onCopyComplete", Date.now());
} catch (ex) {
this._log.error("Failed to copy log stream to file", ex);
return;
}
// It's not completely clear to markh why we only do log cleanups
// for errors, but for now the Sync semantics have been copied...
// (one theory is that only cleaning up on error makes it less
// likely old error logs would be removed, but that's not true if
// there are occasional errors - let's address this later!)
if (reason == this.REASON_ERROR && !this._cleaningUpFileLogs) {
this._log.trace("Scheduling cleanup.");
// Note we don't return/yield or otherwise wait on this promise - it
// continues in the background
this.cleanupLogs().catch(err => {
this._log.error("Failed to cleanup logs", err);
});
}
// We have reasonPrefix at the start of the filename so all "error"
// logs are grouped in about:sync-log.
let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
yield this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log);
// It's not completely clear to markh why we only do log cleanups
// for errors, but for now the Sync semantics have been copied...
// (one theory is that only cleaning up on error makes it less
// likely old error logs would be removed, but that's not true if
// there are occasional errors - let's address this later!)
if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) {
this._log.trace("Scheduling cleanup.");
// Note we don't return/yield or otherwise wait on this promise - it
// continues in the background
this.cleanupLogs().catch(err => {
this._log.error("Failed to cleanup logs", err);
});
}
return reason;
} catch (ex) {
this._log.error("Failed to resetFileLog", ex)
this._log.error("Failed to resetFileLog", ex);
return null;
}
}),

View File

@ -30,10 +30,10 @@ add_task(function* test_noPrefs() {
let log = Log.repository.getLogger("TestLog");
let [capp, dapp, fapps] = getAppenders(log);
// the "dump" and "console" appenders should get Error level
equal(capp.level, Log.Level.Error);
// The console appender gets "Fatal" while the "dump" appender gets "Error" levels
equal(capp.level, Log.Level.Fatal);
equal(dapp.level, Log.Level.Error);
// and the file (stream) appender gets Dump by default
// and the file (stream) appender gets Debug by default
equal(fapps.length, 1, "only 1 file appender");
equal(fapps[0].level, Log.Level.Debug);
lm.finalize();
@ -62,7 +62,7 @@ add_task(function* test_PrefChanges() {
Services.prefs.setCharPref("log-manager.test.log.appender.console", "xxx");
Services.prefs.setCharPref("log-manager.test.log.appender.dump", "xxx");
Services.prefs.setCharPref("log-manager.test.log.appender.file.level", "xxx");
equal(capp.level, Log.Level.Error);
equal(capp.level, Log.Level.Fatal);
equal(dapp.level, Log.Level.Error);
equal(fapp.level, Log.Level.Debug);
lm.finalize();
@ -131,5 +131,99 @@ add_task(function* test_logFileErrorDefault() {
yield lm.resetFileLog(lm.REASON_ERROR);
// One error log file exists.
checkLogFile("error");
lm.finalize();
});
// Test that we correctly write success logs.
add_task(function* test_logFileSuccess() {
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnError", false);
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnSuccess", false);
let lm = new LogManager("log-manager.test.", ["TestLog2"], "test");
let log = Log.repository.getLogger("TestLog2");
log.info("an info message");
yield lm.resetFileLog();
// Zero log files exist.
checkLogFile(null);
// Reset logOnSuccess and do it again - log should appear.
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnSuccess", true);
log.info("an info message");
yield lm.resetFileLog();
checkLogFile("success");
// Now test with no "reason" specified and no "error" record.
log.info("an info message");
yield lm.resetFileLog();
// should get a "success" entry.
checkLogFile("success");
// With no "reason" and an error record - should get no success log.
log.error("an error message");
yield lm.resetFileLog();
// should get no entry
checkLogFile(null);
// And finally now with no error, to ensure that the fact we had an error
// previously doesn't persist after the .resetFileLog call.
log.info("an info message");
yield lm.resetFileLog();
checkLogFile("success");
lm.finalize();
});
// Test that we correctly write error logs.
add_task(function* test_logFileError() {
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnError", false);
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnSuccess", false);
let lm = new LogManager("log-manager.test.", ["TestLog2"], "test");
let log = Log.repository.getLogger("TestLog2");
log.info("an info message");
let reason = yield lm.resetFileLog();
Assert.equal(reason, null, "null returned when no file created.");
// Zero log files exist.
checkLogFile(null);
// Reset logOnSuccess - success logs should appear if no error records.
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnSuccess", true);
log.info("an info message");
reason = yield lm.resetFileLog();
Assert.equal(reason, lm.SUCCESS_LOG_WRITTEN);
checkLogFile("success");
// Set logOnError and unset logOnSuccess - error logs should appear.
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnSuccess", false);
Services.prefs.setBoolPref("log-manager.test.log.appender.file.logOnError", true);
log.error("an error message");
reason = yield lm.resetFileLog();
Assert.equal(reason, lm.ERROR_LOG_WRITTEN);
checkLogFile("error");
// Now test with no "error" record.
log.info("an info message");
reason = yield lm.resetFileLog();
// should get no file
Assert.equal(reason, null);
checkLogFile(null);
// With an error record we should get an error log.
log.error("an error message");
reason = yield lm.resetFileLog();
// should get en error log
Assert.equal(reason, lm.ERROR_LOG_WRITTEN);
checkLogFile("error");
// And finally now with success, to ensure that the fact we had an error
// previously doesn't persist after the .resetFileLog call.
log.info("an info message");
yield lm.resetFileLog();
checkLogFile(null);
lm.finalize();
});

View File

@ -602,7 +602,8 @@ ErrorHandler.prototype = {
this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception));
break;
case "weave:service:login:error":
this.resetFileLog(this._logManager.REASON_ERROR);
this._log.error("Sync encountered a login error");
this.resetFileLog();
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:login:error");
@ -617,7 +618,8 @@ ErrorHandler.prototype = {
this.service.logout();
}
this.resetFileLog(this._logManager.REASON_ERROR);
this._log.error("Sync encountered an error");
this.resetFileLog();
if (this.shouldReportError()) {
this.notifyOnNextTick("weave:ui:sync:error");
@ -642,8 +644,8 @@ ErrorHandler.prototype = {
}
if (Status.service == SYNC_FAILED_PARTIAL) {
this._log.debug("Some engines did not sync correctly.");
this.resetFileLog(this._logManager.REASON_ERROR);
this._log.error("Some engines did not sync correctly.");
this.resetFileLog();
if (this.shouldReportError()) {
this.dontIgnoreErrors = false;
@ -651,7 +653,7 @@ ErrorHandler.prototype = {
break;
}
} else {
this.resetFileLog(this._logManager.REASON_SUCCESS);
this.resetFileLog();
}
this.dontIgnoreErrors = false;
this.notifyOnNextTick("weave:ui:sync:finish");
@ -681,19 +683,18 @@ ErrorHandler.prototype = {
/**
* Generate a log file for the sync that just completed
* and refresh the input & output streams.
*
* @param reason
* A constant from the LogManager that indicates the reason for the
* reset.
*/
resetFileLog: function resetFileLog(reason) {
let onComplete = () => {
resetFileLog: function resetFileLog() {
let onComplete = logType => {
Svc.Obs.notify("weave:service:reset-file-log");
this._log.trace("Notified: " + Date.now());
if (logType == this._logManager.ERROR_LOG_WRITTEN) {
Cu.reportError("Sync encountered an error - see about:sync-log for the log file.");
}
};
// Note we do not return the promise here - the caller doesn't need to wait
// for this to complete.
this._logManager.resetFileLog(reason).then(onComplete, onComplete);
this._logManager.resetFileLog().then(onComplete, onComplete);
},
/**

View File

@ -54,7 +54,7 @@ pref("services.sync.addons.ignoreUserEnabledChanges", false);
// Comma-delimited list of hostnames to trust for add-on install.
pref("services.sync.addons.trustedSourceHostnames", "addons.mozilla.org");
pref("services.sync.log.appender.console", "Warn");
pref("services.sync.log.appender.console", "Fatal");
pref("services.sync.log.appender.dump", "Error");
pref("services.sync.log.appender.file.level", "Trace");
pref("services.sync.log.appender.file.logOnError", true);

View File

@ -134,6 +134,9 @@ let AddonWatcher = {
longestDuration: Math.round(Math.log2(Preferences.get("browser.addon-watch.limits.longestDuration", 128))),
};
// By default, warn only after an add-on has been spotted misbehaving 3 times.
let tolerance = Preferences.get("browser.addon-watch.tolerance", 3);
for (let item of snapshot.componentsData) {
let addonId = item.addonId;
if (!item.isSystem || !addonId) {
@ -192,6 +195,10 @@ let AddonWatcher = {
stats.alerts[filter] = (stats.alerts[filter] || 0) + 1;
if (stats.alerts[filter] % tolerance != 0) {
continue;
}
try {
this._callback(addonId, filter);
} catch (ex) {

View File

@ -3421,6 +3421,16 @@ var InspectorActor = exports.InspectorActor = protocol.ActorClass({
this.tabActor = tabActor;
},
destroy: function () {
protocol.Actor.prototype.destroy.call(this);
},
// Forces destruction of the actor and all its children
// like highlighter, walker and style actors.
disconnect: function() {
this.destroy();
},
get window() this.tabActor.window,
getWalker: method(function(options={}) {

View File

@ -725,7 +725,15 @@ function TabActor(aConnection)
// Used on b2g to catch activity frames and in chrome to list all frames
this.listenForNewDocShells = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
this.traits = { reconfigure: true, frames: true };
this.traits = {
reconfigure: true,
// Supports frame listing via `listFrames` request and `frameUpdate` events
// as well as frame switching via `switchToFrame` request
frames: true,
// Do not require to send reconfigure request to reset the document state
// to what it was before using the TabActor
noTabReconfigureOnClose: true
};
this._workerActorList = null;
this._workerActorPool = null;
@ -1300,6 +1308,7 @@ TabActor.prototype = {
// during Firefox shutdown.
if (this.docShell) {
this._progressListener.unwatch(this.docShell);
this._restoreDocumentSettings();
}
if (this._progressListener) {
this._progressListener.destroy();
@ -1390,14 +1399,19 @@ TabActor.prototype = {
onReconfigure: function (aRequest) {
let options = aRequest.options || {};
this._toggleDevtoolsSettings(options);
if (!this.docShell) {
// The tab is already closed.
return {};
}
this._toggleDevToolsSettings(options);
return {};
},
/**
* Handle logic to enable/disable JS/cache/Service Worker testing.
*/
_toggleDevtoolsSettings: function(options) {
_toggleDevToolsSettings: function(options) {
// Wait a tick so that the response packet can be dispatched before the
// subsequent navigation event packet.
let reload = false;
@ -1429,6 +1443,16 @@ TabActor.prototype = {
}
},
/**
* Opposite of the _toggleDevToolsSettings method, that reset document state
* when closing the toolbox.
*/
_restoreDocumentSettings: function () {
this._restoreJavascript();
this._setCacheDisabled(false);
this._setServiceWorkersTestingEnabled(false);
},
/**
* Disable or enable the cache via docShell.
*/
@ -1437,29 +1461,46 @@ TabActor.prototype = {
let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING;
if (this.docShell) {
this.docShell.defaultLoadFlags = disabled ? disable : enable;
}
this.docShell.defaultLoadFlags = disabled ? disable : enable;
},
/**
* Disable or enable JS via docShell.
*/
_wasJavascriptEnabled: null,
_setJavascriptEnabled: function(allow) {
if (this.docShell) {
this.docShell.allowJavascript = allow;
if (this._wasJavascriptEnabled === null) {
this._wasJavascriptEnabled = this.docShell.allowJavascript;
}
this.docShell.allowJavascript = allow;
},
/**
* Restore JS state, before the actor modified it.
*/
_restoreJavascript: function () {
if (this._wasJavascriptEnabled !== null) {
this._setJavascriptEnabled(this._wasJavascriptEnabled);
this._wasJavascriptEnabled = null;
}
},
/**
* Return JS allowed status.
*/
_getJavascriptEnabled: function() {
if (!this.docShell) {
// The tab is already closed.
return null;
}
return this.docShell.allowJavascript;
},
/**
* Disable or enable the service workers testing features.
*/
_setServiceWorkersTestingEnabled: function(enabled) {
if (!this.docShell) {
// The tab is already closed.
return null;
}
let windowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.serviceWorkersTestingEnabled = enabled;
@ -1479,18 +1520,6 @@ TabActor.prototype = {
return this.docShell.defaultLoadFlags === disable;
},
/**
* Return JS allowed status.
*/
_getJavascriptEnabled: function() {
if (!this.docShell) {
// The tab is already closed.
return null;
}
return this.docShell.allowJavascript;
},
/**
* Return service workers testing allowed status.
*/

View File

@ -179,6 +179,19 @@ function safeCall(aCallback, ...aArgs) {
}
}
/**
* Creates a function that will call the passed callback catching and logging
* any exceptions.
*
* @param aCallback
* The callback method to call
*/
function makeSafe(aCallback) {
return function(...aArgs) {
safeCall(aCallback, ...aArgs);
}
}
/**
* Report an exception thrown by a provider API method.
*/
@ -243,6 +256,26 @@ function callProviderAsync(aProvider, aMethod, ...aArgs) {
}
}
/**
* Calls a method on a provider if it exists and consumes any thrown exception.
* Parameters after aMethod are passed to aProvider.aMethod() and an additional
* callback is added for the provider to return a result to.
*
* @param aProvider
* The provider to call
* @param aMethod
* The method name to call
* @return {Promise}
* @resolves The result the provider returns, or |undefined| if the provider
* does not implement the method or the method throws.
* @rejects Never
*/
function promiseCallProvider(aProvider, aMethod, ...aArgs) {
return new Promise(resolve => {
callProviderAsync(aProvider, aMethod, ...aArgs, resolve);
});
}
/**
* Gets the currently selected locale for display.
* @return the selected locale or "en-US" if none is selected
@ -2101,11 +2134,12 @@ var AddonManagerInternal = {
*
* @param aID
* The ID of the add-on to retrieve
* @param aCallback
* The callback to pass the retrieved add-on to
* @throws if the aID or aCallback arguments are not specified
* @return {Promise}
* @resolves The found Addon or null if no such add-on exists.
* @rejects Never
* @throws if the aID argument is not specified
*/
getAddonByID: function AMI_getAddonByID(aID, aCallback) {
getAddonByID: function AMI_getAddonByID(aID) {
if (!gStarted)
throw Components.Exception("AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED);
@ -2114,24 +2148,9 @@ var AddonManagerInternal = {
throw Components.Exception("aID must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG);
if (typeof aCallback != "function")
throw Components.Exception("aCallback must be a function",
Cr.NS_ERROR_INVALID_ARG);
new AsyncObjectCaller(this.providers, "getAddonByID", {
nextObject: function getAddonByID_nextObject(aCaller, aProvider) {
callProviderAsync(aProvider, "getAddonByID", aID,
function getAddonByID_safeCall(aAddon) {
if (aAddon)
safeCall(aCallback, aAddon);
else
aCaller.callNext();
});
},
noMoreObjects: function getAddonByID_noMoreObjects(aCaller) {
safeCall(aCallback, null);
}
let promises = [for (p of this.providers) promiseCallProvider(p, "getAddonByID", aID)];
return Promise.all(promises).then(aAddons => {
return aAddons.find(a => !!a) || null;
});
},
@ -2180,11 +2199,12 @@ var AddonManagerInternal = {
*
* @param aIDs
* The array of IDs to retrieve
* @param aCallback
* The callback to pass an array of Addons to
* @throws if the aID or aCallback arguments are not specified
* @return {Promise}
* @resolves The array of found add-ons.
* @rejects Never
* @throws if the aIDs argument is not specified
*/
getAddonsByIDs: function AMI_getAddonsByIDs(aIDs, aCallback) {
getAddonsByIDs: function AMI_getAddonsByIDs(aIDs) {
if (!gStarted)
throw Components.Exception("AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED);
@ -2193,25 +2213,8 @@ var AddonManagerInternal = {
throw Components.Exception("aIDs must be an array",
Cr.NS_ERROR_INVALID_ARG);
if (typeof aCallback != "function")
throw Components.Exception("aCallback must be a function",
Cr.NS_ERROR_INVALID_ARG);
let addons = [];
new AsyncObjectCaller(aIDs, null, {
nextObject: function getAddonsByIDs_nextObject(aCaller, aID) {
AddonManagerInternal.getAddonByID(aID,
function getAddonsByIDs_getAddonByID(aAddon) {
addons.push(aAddon);
aCaller.callNext();
});
},
noMoreObjects: function getAddonsByIDs_noMoreObjects(aCaller) {
safeCall(aCallback, addons);
}
});
let promises = [AddonManagerInternal.getAddonByID(i) for (i of aIDs)];
return Promise.all(promises);
},
/**
@ -2876,7 +2879,13 @@ this.AddonManager = {
},
getAddonByID: function AM_getAddonByID(aID, aCallback) {
AddonManagerInternal.getAddonByID(aID, aCallback);
if (typeof aCallback != "function")
throw Components.Exception("aCallback must be a function",
Cr.NS_ERROR_INVALID_ARG);
AddonManagerInternal.getAddonByID(aID)
.then(makeSafe(aCallback))
.catch(logger.error);
},
getAddonBySyncGUID: function AM_getAddonBySyncGUID(aGUID, aCallback) {
@ -2884,7 +2893,13 @@ this.AddonManager = {
},
getAddonsByIDs: function AM_getAddonsByIDs(aIDs, aCallback) {
AddonManagerInternal.getAddonsByIDs(aIDs, aCallback);
if (typeof aCallback != "function")
throw Components.Exception("aCallback must be a function",
Cr.NS_ERROR_INVALID_ARG);
AddonManagerInternal.getAddonsByIDs(aIDs)
.then(makeSafe(aCallback))
.catch(logger.error);
},
getAddonsWithOperationsByTypes:

View File

@ -27,9 +27,25 @@ function test_functions() {
if (typeof AddonManager[prop] != "function")
continue;
let args = [];
// Getter functions need a callback and in some cases not having one will
// throw before checking if the add-ons manager is initialized so pass in
// an empty one.
if (prop.startsWith("get")) {
// For now all getter functions with more than one argument take the
// callback in the second argument.
if (AddonManager[prop].length > 1) {
args.push(undefined, () => {});
}
else {
args.push(() => {});
}
}
try {
do_print("AddonManager." + prop);
AddonManager[prop]();
AddonManager[prop](...args);
do_throw(prop + " did not throw an exception");
}
catch (e) {