merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-06-27 15:47:39 +02:00
commit 55495c33fa
36 changed files with 1016 additions and 124 deletions

View File

@ -472,13 +472,6 @@ SettingsListener.observe('debugger.remote-mode', false, function(value) {
#endif
});
// If debug access to certified apps is allowed, we need to preserve system
// sources so that they are visible in the debugger.
let forbidCertified =
Services.prefs.getBoolPref('devtools.debugger.forbid-certified-apps');
Services.prefs.setBoolPref('javascript.options.discardSystemSource',
forbidCertified);
// =================== Device Storage ====================
SettingsListener.observe('device.storage.writable.name', 'sdcard', function(value) {
if (Services.prefs.getPrefType('device.storage.writable.name') != Ci.nsIPrefBranch.PREF_STRING) {

View File

@ -33,13 +33,11 @@ let test = asyncTest(function*() {
* Just check current page
*/
function* navigate(usage, options) {
let running = yield usage._testOnly_isRunning();
ok(!running, "csscoverage not is running");
ok(!usage.isRunning(), "csscoverage is not running");
yield usage.oneshot();
running = yield usage._testOnly_isRunning();
ok(!running, "csscoverage not is running");
ok(!usage.isRunning(), "csscoverage is still not running");
}
/**

View File

@ -35,8 +35,7 @@ let test = asyncTest(function*() {
function* navigate(usage, options) {
yield usage.start();
let running = yield usage._testOnly_isRunning();
ok(running, "csscoverage is running");
ok(usage.isRunning(), "csscoverage is running");
yield helpers.navigate(PAGE_1, options);
@ -49,8 +48,7 @@ function* navigate(usage, options) {
yield usage.stop();
running = yield usage._testOnly_isRunning();
ok(!running, "csscoverage not is running");
ok(!usage.isRunning(), "csscoverage not is running");
}
/**

View File

@ -4,18 +4,19 @@
(function() {
const DEVTOOLS_SKIN_URL = "chrome://browser/skin/devtools/";
let documentElement = document.documentElement;
function forceStyle() {
let computedStyle = window.getComputedStyle(document.documentElement);
let computedStyle = window.getComputedStyle(documentElement);
if (!computedStyle) {
// Null when documentElement is not ready. This method is anyways not
// required then as scrollbars would be in their state without flushing.
return;
}
let display = computedStyle.display; // Save display value
document.documentElement.style.display = "none";
window.getComputedStyle(document.documentElement).display; // Flush
document.documentElement.style.display = display; // Restore
documentElement.style.display = "none";
window.getComputedStyle(documentElement).display; // Flush
documentElement.style.display = display; // Restore
}
function switchTheme(newTheme, oldTheme) {
@ -61,8 +62,8 @@
forceStyle();
}
document.documentElement.classList.remove("theme-" + oldTheme);
document.documentElement.classList.add("theme-" + newTheme);
documentElement.classList.remove("theme-" + oldTheme);
documentElement.classList.add("theme-" + newTheme);
}
function handlePrefChange(event, data) {
@ -78,11 +79,14 @@
const {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
const StylesheetUtils = devtools.require("sdk/stylesheet/utils");
let theme = Services.prefs.getCharPref("devtools.theme");
switchTheme(theme);
if (documentElement.hasAttribute("force-theme")) {
switchTheme(documentElement.getAttribute("force-theme"));
} else {
switchTheme(Services.prefs.getCharPref("devtools.theme"));
gDevTools.on("pref-changed", handlePrefChange);
window.addEventListener("unload", function() {
gDevTools.off("pref-changed", handlePrefChange);
});
gDevTools.on("pref-changed", handlePrefChange);
window.addEventListener("unload", function() {
gDevTools.off("pref-changed", handlePrefChange);
});
}
})();

View File

@ -157,6 +157,7 @@ function Editor(config) {
autoCloseBrackets: "()[]{}''\"\"",
autoCloseEnabled: useAutoClose,
theme: "mozilla",
themeSwitching: true,
autocomplete: false
};
@ -258,6 +259,9 @@ Editor.prototype = {
env.removeEventListener("load", onLoad, true);
let win = env.contentWindow.wrappedJSObject;
if (!this.config.themeSwitching)
win.document.documentElement.setAttribute("force-theme", "light");
CM_SCRIPTS.forEach((url) =>
Services.scriptloader.loadSubScript(url, win, "utf8"));

View File

@ -2869,7 +2869,13 @@ function getParentTextProperty(node) {
if (!parent) {
return null;
}
return parent.querySelector(".ruleview-propertyvalue").textProperty;
let propValue = parent.querySelector(".ruleview-propertyvalue");
if (!propValue) {
return null;
}
return propValue.textProperty;
}
/**

View File

@ -270,6 +270,7 @@ run-if = os == "mac"
[browser_webconsole_expandable_timestamps.js]
[browser_webconsole_autocomplete_in_debugger_stackframe.js]
[browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js]
[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js]
[browser_webconsole_output_01.js]
[browser_webconsole_output_02.js]

View File

@ -0,0 +1,42 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
// Test that properties starting with underscores or dollars can be
// autocompleted (bug 967468).
function test() {
const TEST_URI = "data:text/html;charset=utf8,test autocompletion with $ or _";
Task.spawn(runner).then(finishTest);
function* runner() {
function autocomplete(term) {
let deferred = promise.defer();
jsterm.setInputValue(term);
jsterm.complete(jsterm.COMPLETE_HINT_ONLY, deferred.resolve);
yield deferred.promise;
ok(popup.itemCount > 0, "There's suggestions for '" + term + "'");
}
yield addTab(TEST_URI);
let { jsterm } = yield openConsole(tab);
let popup = jsterm.autocompletePopup;
jsterm.execute("let testObject = {$$aaab: '', $$aaac: ''}");
// Should work with bug 967468.
yield autocomplete("Object.__d");
yield autocomplete("testObject.$$a");
// Here's when things go wrong in bug 967468.
yield autocomplete("Object.__de");
yield autocomplete("testObject.$$aa");
}
}

View File

@ -4292,8 +4292,8 @@ JSTerm.prototype = {
if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
let filterBy = input;
// Find the last non-alphanumeric if exists.
let lastNonAlpha = input.match(/[^a-zA-Z0-9][a-zA-Z0-9]*$/);
// Find the last non-alphanumeric other than _ or $ if it exists.
let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/);
// If input contains non-alphanumerics, use the part after the last one
// to filter the cache
if (lastNonAlpha) {

View File

@ -233,7 +233,15 @@ let UI = {
/********** RUNTIME **********/
updateRuntimeList: function() {
let wifiHeaderNode = document.querySelector("#runtime-header-wifi-devices");
if (AppManager.isWiFiScanningEnabled) {
wifiHeaderNode.removeAttribute("hidden");
} else {
wifiHeaderNode.setAttribute("hidden", "true");
}
let USBListNode = document.querySelector("#runtime-panel-usbruntime");
let WiFiListNode = document.querySelector("#runtime-panel-wifi-devices");
let simulatorListNode = document.querySelector("#runtime-panel-simulators");
let customListNode = document.querySelector("#runtime-panel-custom");
@ -261,6 +269,7 @@ let UI = {
for (let [type, parent] of [
["usb", USBListNode],
["wifi", WiFiListNode],
["simulator", simulatorListNode],
["custom", customListNode],
]) {
@ -775,6 +784,8 @@ let Cmds = {
},
showRuntimePanel: function() {
AppManager.scanForWiFiRuntimes();
let panel = document.querySelector("#runtime-panel");
let anchor = document.querySelector("#runtime-panel-button > .panel-button-anchor");

View File

@ -144,6 +144,8 @@
<toolbarbutton class="panel-item-help" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
<toolbarbutton class="panel-item-help" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
<vbox id="runtime-panel-usbruntime"></vbox>
<label class="panel-header" id="runtime-header-wifi-devices">&runtimePanel_WiFiDevices;</label>
<vbox id="runtime-panel-wifi-devices"></vbox>
<label class="panel-header">&runtimePanel_simulators;</label>
<toolbarbutton class="panel-item-help" label="&runtimePanel_nosimulator;" id="runtime-panel-nosimulator" command="cmd_showAddons"/>
<vbox id="runtime-panel-simulators"></vbox>

View File

@ -59,6 +59,7 @@
<!ENTITY projectPanel_myProjects "My Projects">
<!ENTITY projectPanel_runtimeApps "Runtime Apps">
<!ENTITY runtimePanel_USBDevices "USB Devices">
<!ENTITY runtimePanel_WiFiDevices "WiFi Devices">
<!ENTITY runtimePanel_simulators "Simulators">
<!ENTITY runtimePanel_custom "Custom">
<!ENTITY runtimePanel_nosimulator "Install Simulator">

View File

@ -20,10 +20,14 @@ const AppActorFront = require("devtools/app-actor-front");
const {getDeviceFront} = require("devtools/server/actors/device");
const {setTimeout} = require("sdk/timers");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {USBRuntime, SimulatorRuntime, gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
const {USBRuntime, WiFiRuntime, SimulatorRuntime,
gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
const discovery = require("devtools/toolkit/discovery/discovery");
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
const WIFI_SCANNING_PREF = "devtools.remote.wifi.scan";
exports.AppManager = AppManager = {
// FIXME: will break when devtools/app-manager will be removed:
@ -42,12 +46,21 @@ exports.AppManager = AppManager = {
this.webAppsStore = new WebappsStore(this.connection);
this.webAppsStore.on("store-ready", this.onWebAppsStoreready);
this.runtimeList = {usb: [], simulator: [], custom: [gRemoteRuntime]};
this.runtimeList = {
usb: [],
wifi: [],
simulator: [],
custom: [gRemoteRuntime]
};
if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
this.runtimeList.custom.push(gLocalRuntime);
}
this.trackUSBRuntimes();
this.trackWiFiRuntimes();
this.trackSimulatorRuntimes();
this.observe = this.observe.bind(this);
Services.prefs.addObserver(WIFI_SCANNING_PREF, this, false);
},
uninit: function() {
@ -55,6 +68,7 @@ exports.AppManager = AppManager = {
this.selectedProject = null;
this.selectedRuntime = null;
this.untrackUSBRuntimes();
this.untrackWiFiRuntimes();
this.untrackSimulatorRuntimes();
this._runningApps.clear();
this.runtimeList = null;
@ -65,6 +79,17 @@ exports.AppManager = AppManager = {
this._listTabsResponse = null;
this.connection.disconnect();
this.connection = null;
Services.prefs.removeObserver(WIFI_SCANNING_PREF, this);
},
observe: function(subject, topic, data) {
if (data !== WIFI_SCANNING_PREF) {
return;
}
// Cycle WiFi tracking to reflect the new value
this.untrackWiFiRuntimes();
this.trackWiFiRuntimes();
this._updateWiFiRuntimes();
},
update: function(what, details) {
@ -505,6 +530,40 @@ exports.AppManager = AppManager = {
this.update("runtimelist");
},
get isWiFiScanningEnabled() {
return Services.prefs.getBoolPref(WIFI_SCANNING_PREF);
},
scanForWiFiRuntimes: function() {
if (!this.isWiFiScanningEnabled) {
return;
}
discovery.scan();
},
trackWiFiRuntimes: function() {
if (!this.isWiFiScanningEnabled) {
return;
}
this._updateWiFiRuntimes = this._updateWiFiRuntimes.bind(this);
discovery.on("devtools-device-added", this._updateWiFiRuntimes);
discovery.on("devtools-device-updated", this._updateWiFiRuntimes);
discovery.on("devtools-device-removed", this._updateWiFiRuntimes);
},
untrackWiFiRuntimes: function() {
if (!this.isWiFiScanningEnabled) {
return;
}
discovery.off("devtools-device-added", this._updateWiFiRuntimes);
discovery.off("devtools-device-updated", this._updateWiFiRuntimes);
discovery.off("devtools-device-removed", this._updateWiFiRuntimes);
},
_updateWiFiRuntimes: function() {
this.runtimeList.wifi = [];
for (let device of discovery.getRemoteDevicesWithService("devtools")) {
this.runtimeList.wifi.push(new WiFiRuntime(device));
}
this.update("runtimelist");
},
trackSimulatorRuntimes: function() {
this._updateSimulatorRuntimes = this._updateSimulatorRuntimes.bind(this);
Simulator.on("register", this._updateSimulatorRuntimes);

View File

@ -8,6 +8,7 @@ const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm");
const discovery = require("devtools/toolkit/discovery/discovery");
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
@ -19,7 +20,7 @@ USBRuntime.prototype = {
connect: function(connection) {
let device = Devices.getByName(this.id);
if (!device) {
return promise.reject("Can't find device: " + id);
return promise.reject("Can't find device: " + this.getName());
}
return device.connect().then((port) => {
connection.host = "localhost";
@ -35,6 +36,29 @@ USBRuntime.prototype = {
},
}
function WiFiRuntime(deviceName) {
this.deviceName = deviceName;
}
WiFiRuntime.prototype = {
connect: function(connection) {
let service = discovery.getRemoteService("devtools", this.deviceName);
if (!service) {
return promise.reject("Can't find device: " + this.getName());
}
connection.host = service.host;
connection.port = service.port;
connection.connect();
return promise.resolve();
},
getID: function() {
return this.deviceName;
},
getName: function() {
return this.deviceName;
},
}
function SimulatorRuntime(version) {
this.version = version;
}
@ -44,7 +68,7 @@ SimulatorRuntime.prototype = {
let port = ConnectionManager.getFreeTCPPort();
let simulator = Simulator.getByVersion(this.version);
if (!simulator || !simulator.launch) {
return promise.reject("Can't find simulator: " + this.version);
return promise.reject("Can't find simulator: " + this.getName());
}
return simulator.launch({port: port}).then(() => {
connection.port = port;
@ -102,6 +126,7 @@ let gRemoteRuntime = {
}
exports.USBRuntime = USBRuntime;
exports.WiFiRuntime = WiFiRuntime;
exports.SimulatorRuntime = SimulatorRuntime;
exports.gRemoteRuntime = gRemoteRuntime;
exports.gLocalRuntime = gLocalRuntime;

View File

@ -187,6 +187,7 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
#runtime-permissions,
#runtime-screenshot,
.runtime-panel-item-usb,
.runtime-panel-item-wifi,
.runtime-panel-item-custom,
.runtime-panel-item-simulator {
list-style-image: url("icons.png");
@ -195,6 +196,7 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
#runtime-screenshot { -moz-image-region: rect(200px, 640px, 240px, 600px) }
#runtime-permissions { -moz-image-region: rect(100px, 840px, 140px, 800px) }
.runtime-panel-item-usb { -moz-image-region: rect(100px, 640px, 140px, 600px) }
.runtime-panel-item-wifi { -moz-image-region: rect(100px, 640px, 140px, 600px) }
.runtime-panel-item-custom { -moz-image-region: rect(100px, 640px, 140px, 600px) }
.runtime-panel-item-simulator { -moz-image-region: rect(100px, 740px, 140px, 700px) }

View File

@ -27,8 +27,8 @@ public class GlobalConstants {
public static final String MOZ_APP_DISPLAYNAME = "@MOZ_APP_DISPLAYNAME@";
public static final String MOZ_APP_VERSION = "@MOZ_APP_VERSION@";
public static final String BROWSER_INTENT_PACKAGE = "org.mozilla.gecko";
public static final String BROWSER_INTENT_CLASS = BROWSER_INTENT_PACKAGE + ".BrowserApp";
public static final String BROWSER_INTENT_PACKAGE = "@ANDROID_PACKAGE_NAME@";
public static final String BROWSER_INTENT_CLASS = "org.mozilla.gecko.BrowserApp";
/**
* Bug 800244: this signing-level permission protects broadcast intents that

View File

@ -40,10 +40,10 @@ TabSource.prototype = {
let label;
if (tab.browser.contentTitle)
label = tab.browser.contentTitle;
else if (tab.browser.contentURI && tab.browser.contentURI.spec)
else if (tab.browser.contentURI)
label = tab.browser.contentURI.spec;
else
label = tab.originalURI;
label = tab.originalURI.spec;
return { label: label,
icon: "thumbnail:" + tab.id }
}));

View File

@ -638,6 +638,11 @@ pref("devtools.defaultColorUnit", "hex");
// Used for devtools debugging
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);
// view source
pref("view_source.syntax_highlight", true);
pref("view_source.wrap_long_lines", false);

View File

@ -41,3 +41,4 @@
[include:js/jsd/test/xpcshell.ini]
[include:security/manager/ssl/tests/unit/xpcshell.ini]
[include:toolkit/devtools/qrcode/tests/unit/xpcshell.ini]
[include:toolkit/devtools/discovery/tests/unit/xpcshell.ini]

View File

@ -20,3 +20,4 @@
[include:ipc/testshell/tests/xpcshell.ini]
[include:b2g/components/test/unit/xpcshell.ini]
[include:security/manager/ssl/tests/unit/xpcshell.ini]
[include:toolkit/devtools/discovery/tests/unit/xpcshell.ini]

View File

@ -5,6 +5,7 @@ const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const EventEmitter = require("devtools/toolkit/event-emitter");
// XXX: bug 912476 make this module a real protocol.js front
// by converting webapps actor to protocol.js
@ -18,6 +19,9 @@ const CHUNK_SIZE = 10000;
const appTargets = new Map();
const AppActorFront = exports;
EventEmitter.decorate(AppActorFront);
function addDirToZip(writer, dir, basePath) {
let files = dir.directoryEntries;
@ -102,15 +106,34 @@ function uploadPackageJSON(client, webappsActor, packageFile) {
openFile(res.actor);
});
let fileSize;
let bytesRead = 0;
function emitProgress() {
emitInstallProgress({
bytesSent: bytesRead,
totalBytes: fileSize
});
}
function openFile(actor) {
let openedFile;
OS.File.open(packageFile.path)
.then(function (file) {
uploadChunk(actor, file);
.then(file => {
openedFile = file;
return openedFile.stat();
})
.then(fileInfo => {
fileSize = fileInfo.size;
emitProgress();
uploadChunk(actor, openedFile);
});
}
function uploadChunk(actor, file) {
file.read(CHUNK_SIZE)
.then(function (bytes) {
bytesRead += bytes.length;
emitProgress();
// To work around the fact that JSON.stringify translates the typed
// array to object, we are encoding the typed array here into a string
let chunk = String.fromCharCode.apply(null, bytes);
@ -168,7 +191,11 @@ function uploadPackageBulk(client, webappsActor, packageFile) {
request.on("bulk-send-ready", ({copyFrom}) => {
NetUtil.asyncFetch(packageFile, function(inputStream) {
copyFrom(inputStream).then(() => {
let copying = copyFrom(inputStream);
copying.on("progress", (e, progress) => {
emitInstallProgress(progress);
});
copying.then(() => {
console.log("Bulk upload done");
inputStream.close();
deferred.resolve(actor);
@ -236,6 +263,16 @@ function installPackaged(client, webappsActor, packagePath, appId) {
}
exports.installPackaged = installPackaged;
/**
* Emits numerous events as packaged app installation proceeds.
* The progress object contains:
* * bytesSent: The number of bytes sent so far
* * totalBytes: The total number of bytes to send
*/
function emitInstallProgress(progress) {
AppActorFront.emit("install-progress", progress);
}
function installHosted(client, webappsActor, appId, metadata, manifest) {
let deferred = promise.defer();
let request = {

View File

@ -3,7 +3,9 @@
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {installHosted, installPackaged} = require("devtools/app-actor-front");
const AppActorFront = require("devtools/app-actor-front");
const {installHosted, installPackaged} = AppActorFront;
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let gAppId = "actor-test";
const APP_ORIGIN = "app://" + gAppId;
@ -178,29 +180,54 @@ add_test(function testFileUploadInstall() {
// Disable the bulk trait temporarily to test the JSON upload path
gClient.traits.bulk = false;
installPackaged(gClient, gActor, packageFile.path, gAppId)
let progressDeferred = promise.defer();
// Ensure we get at least one progress event at the end
AppActorFront.on("install-progress", function onProgress(e, progress) {
if (progress.bytesSent == progress.totalBytes) {
AppActorFront.off("install-progress", onProgress);
progressDeferred.resolve();
}
});
let installed =
installPackaged(gClient, gActor, packageFile.path, gAppId)
.then(function ({ appId }) {
do_check_eq(appId, gAppId);
// Restore default bulk trait value
gClient.traits.bulk = true;
run_next_test();
}, function (e) {
do_throw("Failed install uploaded packaged app: " + e.error + ": " + e.message);
});
promise.all([progressDeferred.promise, installed])
.then(() => {
// Restore default bulk trait value
gClient.traits.bulk = true;
run_next_test();
});
});
add_test(function testBulkUploadInstall() {
let packageFile = do_get_file("data/app.zip");
do_check_true(gClient.traits.bulk);
installPackaged(gClient, gActor, packageFile.path, gAppId)
let progressDeferred = promise.defer();
// Ensure we get at least one progress event at the end
AppActorFront.on("install-progress", function onProgress(e, progress) {
if (progress.bytesSent == progress.totalBytes) {
AppActorFront.off("install-progress", onProgress);
progressDeferred.resolve();
}
});
let installed =
installPackaged(gClient, gActor, packageFile.path, gAppId)
.then(function ({ appId }) {
do_check_eq(appId, gAppId);
run_next_test();
}, function (e) {
do_throw("Failed bulk install uploaded packaged app: " + e.error + ": " + e.message);
});
promise.all([progressDeferred.promise, installed])
.then(run_next_test);
});
add_test(function testInstallHosted() {
@ -250,4 +277,3 @@ function run_test() {
run_next_test();
}

View File

@ -663,6 +663,8 @@ DebuggerClient.prototype = {
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
request: function (aRequest, aOnResponse) {
if (!this.mainRoot) {
@ -728,6 +730,8 @@ DebuggerClient.prototype = {
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
* * json-reply: The server replied with a JSON packet, which is
* passed as event data.
* * bulk-reply: The server replied with bulk data, which you can read
@ -753,6 +757,8 @@ DebuggerClient.prototype = {
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
startBulkRequest: function(request) {
if (!this.traits.bulk) {
@ -932,6 +938,8 @@ DebuggerClient.prototype = {
* @return Promise
* The promise is resolved when copying completes or rejected
* if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
onBulkPacket: function(packet) {
let { actor, type, length } = packet;

View File

@ -0,0 +1,399 @@
/* 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";
/**
* This implements a UDP mulitcast device discovery protocol that:
* * Is optimized for mobile devices
* * Doesn't require any special schema for service info
*
* To ensure it works well on mobile devices, there is no heartbeat or other
* recurring transmission.
*
* Devices are typically in one of two groups: scanning for services or
* providing services (though they may be in both groups as well).
*
* Scanning devices listen on UPDATE_PORT for UDP multicast traffic. When the
* scanning device wants to force an update of the services available, it sends
* a status packet to SCAN_PORT.
*
* Service provider devices listen on SCAN_PORT for any packets from scanning
* devices. If one is recevied, the provider device sends a status packet
* (listing the services it offers) to UPDATE_PORT.
*
* Scanning devices purge any previously known devices after REPLY_TIMEOUT ms
* from that start of a scan if no reply is received during the most recent
* scan.
*
* When a service is registered, is supplies a regular object with any details
* about itself (a port number, for example) in a service-defined format, which
* is then available to scanning devices.
*/
const { Cu, CC, Cc, Ci } = require("chrome");
const EventEmitter = require("devtools/toolkit/event-emitter");
const { setTimeout, clearTimeout } = require("sdk/timers");
const UDPSocket = CC("@mozilla.org/network/udp-socket;1",
"nsIUDPSocket",
"init");
// TODO Bug 1027456: May need to reserve these with IANA
const SCAN_PORT = 50624;
const UPDATE_PORT = 50625;
const ADDRESS = "224.0.0.200";
const REPLY_TIMEOUT = 5000;
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
XPCOMUtils.defineLazyGetter(this, "converter", () => {
let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
conv.charset = "utf8";
return conv;
});
XPCOMUtils.defineLazyGetter(this, "sysInfo", () => {
return Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
});
XPCOMUtils.defineLazyGetter(this, "libcutils", function () {
Cu.import("resource://gre/modules/systemlibs.js");
return libcutils;
});
let logging = Services.prefs.getBoolPref("devtools.discovery.log");
function log(msg) {
if (logging) {
console.log("DISCOVERY: " + msg);
}
}
/**
* Each Transport instance owns a single UDPSocket.
* @param port integer
* The port to listen on for incoming UDP multicast packets.
*/
function Transport(port) {
EventEmitter.decorate(this);
try {
this.socket = new UDPSocket(port, false);
this.socket.joinMulticast(ADDRESS);
this.socket.asyncListen(this);
} catch(e) {
log("Failed to start new socket: " + e);
}
}
Transport.prototype = {
/**
* Send a object to some UDP port.
* @param object object
* Object which is the message to send
* @param port integer
* UDP port to send the message to
*/
send: function(object, port) {
if (logging) {
log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
}
let message = JSON.stringify(object);
let rawMessage = converter.convertToByteArray(message);
try {
this.socket.send(ADDRESS, port, rawMessage, rawMessage.length);
} catch(e) {
log("Failed to send message: " + e);
}
},
destroy: function() {
this.socket.close();
},
// nsIUDPSocketListener
onPacketReceived: function(socket, message) {
let messageData = message.data;
let object = JSON.parse(messageData);
object.from = message.fromAddr.address;
let port = message.fromAddr.port;
if (port == this.socket.port) {
log("Ignoring looped message");
return;
}
if (logging) {
log("Recv on " + this.socket.port + ":\n" +
JSON.stringify(object, null, 2));
}
this.emit("message", object);
},
onStopListening: function() {}
};
function Discovery() {
EventEmitter.decorate(this);
this.localServices = {};
this.remoteServices = {};
this.device = { name: "unknown" };
this.replyTimeout = REPLY_TIMEOUT;
// Defaulted to Transport, but can be altered by tests
this._factories = { Transport: Transport };
this._transports = {
scan: null,
update: null
};
this._expectingReplies = {
from: new Set()
};
this._onRemoteScan = this._onRemoteScan.bind(this);
this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
this._getSystemInfo();
}
Discovery.prototype = {
/**
* Add a new service offered by this device.
* @param service string
* Name of the service
* @param info object
* Arbitrary data about the service to announce to scanning devices
*/
addService: function(service, info) {
log("ADDING LOCAL SERVICE");
if (Object.keys(this.localServices).length === 0) {
this._startListeningForScan();
}
this.localServices[service] = info;
},
/**
* Remove a service offered by this device.
* @param service string
* Name of the service
*/
removeService: function(service) {
delete this.localServices[service];
if (Object.keys(this.localServices).length === 0) {
this._stopListeningForScan();
}
},
/**
* Scan for service updates from other devices.
*/
scan: function() {
this._startListeningForUpdate();
this._waitForReplies();
// TODO Bug 1027457: Use timer to debounce
this._sendStatusTo(SCAN_PORT);
},
/**
* Get a list of all remote devices currently offering some service.:w
*/
getRemoteDevices: function() {
let devices = new Set();
for (let service in this.remoteServices) {
for (let device in this.remoteServices[service]) {
devices.add(device);
}
}
return [...devices];
},
/**
* Get a list of all remote devices currently offering a particular service.
*/
getRemoteDevicesWithService: function(service) {
let devicesWithService = this.remoteServices[service] || {};
return Object.keys(devicesWithService);
},
/**
* Get service info (any details registered by the remote device) for a given
* service on a device.
*/
getRemoteService: function(service, device) {
let devicesWithService = this.remoteServices[service] || {};
return devicesWithService[device];
},
_waitForReplies: function() {
clearTimeout(this._expectingReplies.timer);
this._expectingReplies.from = new Set(this.getRemoteDevices());
this._expectingReplies.timer =
setTimeout(this._purgeMissingDevices, this.replyTimeout);
},
/**
* Determine a unique name to identify the current device.
*/
_getSystemInfo: function() {
// TODO Bug 1027787: Uniquify device name somehow?
try {
if (Services.appinfo.widgetToolkit == "gonk") {
this.device.name = libcutils.property_get("ro.product.device");
} else {
this.device.name = sysInfo.get("host");
}
log("Device: " + this.device.name);
} catch(e) {
log("Failed to get system info");
this.device.name = "unknown";
}
},
get Transport() {
return this._factories.Transport;
},
_startListeningForScan: function() {
if (this._transports.scan) {
return; // Already listening
}
log("LISTEN FOR SCAN");
this._transports.scan = new this.Transport(SCAN_PORT);
this._transports.scan.on("message", this._onRemoteScan);
},
_stopListeningForScan: function() {
if (!this._transports.scan) {
return; // Not listening
}
this._transports.scan.off("message", this._onRemoteScan);
this._transports.scan.destroy();
this._transports.scan = null;
},
_startListeningForUpdate: function() {
if (this._transports.update) {
return; // Already listening
}
log("LISTEN FOR UPDATE");
this._transports.update = new this.Transport(UPDATE_PORT);
this._transports.update.on("message", this._onRemoteUpdate);
},
_stopListeningForUpdate: function() {
if (!this._transports.update) {
return; // Not listening
}
this._transports.update.off("message", this._onRemoteUpdate);
this._transports.update.destroy();
this._transports.update = null;
},
/**
* When sending message, we can use either transport, so just pick the first
* one currently alive.
*/
get _outgoingTransport() {
if (this._transports.scan) {
return this._transports.scan;
}
if (this._transports.update) {
return this._transports.update;
}
return null;
},
_sendStatusTo: function(port) {
let status = {
device: this.device.name,
services: this.localServices
};
this._outgoingTransport.send(status, port);
},
_onRemoteScan: function() {
// Send my own status in response
log("GOT SCAN REQUEST");
this._sendStatusTo(UPDATE_PORT);
},
_onRemoteUpdate: function(e, update) {
log("GOT REMOTE UPDATE");
let remoteDevice = update.device;
let remoteHost = update.from;
// First, loop over the known services
for (let service in this.remoteServices) {
let devicesWithService = this.remoteServices[service];
let hadServiceForDevice = !!devicesWithService[remoteDevice];
let haveServiceForDevice = service in update.services;
// If the remote device used to have service, but doesn't any longer, then
// it was deleted, so we remove it here.
if (hadServiceForDevice && !haveServiceForDevice) {
delete devicesWithService[remoteDevice];
log("REMOVED " + service + ", DEVICE " + remoteDevice);
this.emit(service + "-device-removed", remoteDevice);
}
}
// Second, loop over the services in the received update
for (let service in update.services) {
// Detect if this is a new device for this service
let newDevice = !this.remoteServices[service] ||
!this.remoteServices[service][remoteDevice];
// Look up the service info we may have received previously from the same
// remote device
let devicesWithService = this.remoteServices[service] || {};
let oldDeviceInfo = devicesWithService[remoteDevice];
// Store the service info from the remote device
let newDeviceInfo = Cu.cloneInto(update.services[service], {});
newDeviceInfo.host = remoteHost;
devicesWithService[remoteDevice] = newDeviceInfo;
this.remoteServices[service] = devicesWithService;
// If this is a new service for the remote device, announce the addition
if (newDevice) {
log("ADDED " + service + ", DEVICE " + remoteDevice);
this.emit(service + "-device-added", remoteDevice, newDeviceInfo);
}
// If we've seen this service from the remote device, but the details have
// changed, announce the update
if (!newDevice &&
JSON.stringify(oldDeviceInfo) != JSON.stringify(newDeviceInfo)) {
log("UPDATED " + service + ", DEVICE " + remoteDevice);
this.emit(service + "-device-updated", remoteDevice, newDeviceInfo);
}
}
},
_purgeMissingDevices: function() {
log("PURGING MISSING DEVICES");
for (let service in this.remoteServices) {
let devicesWithService = this.remoteServices[service];
for (let remoteDevice in devicesWithService) {
// If we're still expecting a reply from a remote device when it's time
// to purge, then the service is removed.
if (this._expectingReplies.from.has(remoteDevice)) {
delete devicesWithService[remoteDevice];
log("REMOVED " + service + ", DEVICE " + remoteDevice);
this.emit(service + "-device-removed", remoteDevice);
}
}
}
}
};
let discovery = new Discovery();
module.exports = discovery;

View File

@ -0,0 +1,13 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
TEST_DIRS += ['tests']
JS_MODULES_PATH = 'modules/devtools/discovery'
EXTRA_JS_MODULES += [
'discovery.js',
]

View File

@ -0,0 +1,7 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']

View File

@ -0,0 +1,139 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const Cu = Components.utils;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
Services.prefs.setBoolPref("devtools.discovery.log", true);
do_register_cleanup(() => {
Services.prefs.clearUserPref("devtools.discovery.log");
});
const { devtools } =
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { Promise: promise } =
Cu.import("resource://gre/modules/Promise.jsm", {});
const { require } = devtools;
const EventEmitter = require("devtools/toolkit/event-emitter");
const discovery = require("devtools/toolkit/discovery/discovery");
const { setTimeout, clearTimeout } = require("sdk/timers");
function log(msg) {
do_print("DISCOVERY: " + msg);
}
// Global map of actively listening ports to TestTransport instances
let gTestTransports = {};
/**
* Implements the same API as Transport in discovery.js. Here, no UDP sockets
* are used. Instead, messages are delivered immediately.
*/
function TestTransport(port) {
EventEmitter.decorate(this);
this.port = port;
gTestTransports[this.port] = this;
}
TestTransport.prototype = {
send: function(object, port) {
log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
if (!gTestTransports[port]) {
log("No listener on port " + port);
return;
}
let message = JSON.stringify(object);
gTestTransports[port].onPacketReceived(null, message);
},
destroy: function() {
delete gTestTransports[this.port];
},
// nsIUDPSocketListener
onPacketReceived: function(socket, message) {
let object = JSON.parse(message);
object.from = "localhost";
log("Recv on " + this.port + ":\n" + JSON.stringify(object, null, 2));
this.emit("message", object);
},
onStopListening: function(socket, status) {}
};
// Use TestTransport instead of the usual Transport
discovery._factories.Transport = TestTransport;
discovery.device.name = "test-device";
function run_test() {
run_next_test();
}
add_task(function*() {
// At startup, no remote devices are known
deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
discovery.scan();
// No services added yet, still empty
deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
discovery.addService("devtools", { port: 1234 });
// Changes not visible until next scan
deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
yield scanForChange("devtools", "added");
// Now we see the new service
deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);
discovery.addService("penguins", { tux: true });
yield scanForChange("penguins", "added");
deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
deepEqual(discovery.getRemoteDevicesWithService("penguins"), ["test-device"]);
deepEqual(discovery.getRemoteDevices(), ["test-device"]);
deepEqual(discovery.getRemoteService("devtools", "test-device"),
{ port: 1234, host: "localhost" });
deepEqual(discovery.getRemoteService("penguins", "test-device"),
{ tux: true, host: "localhost" });
discovery.removeService("devtools");
yield scanForChange("devtools", "removed");
discovery.addService("penguins", { tux: false });
yield scanForChange("penguins", "updated");
// Split the scanning side from the service side to simulate the machine with
// the service becoming unreachable
gTestTransports = {};
discovery.removeService("penguins");
yield scanForChange("penguins", "removed");
});
function scanForChange(service, changeType) {
let deferred = promise.defer();
let timer = setTimeout(() => {
deferred.reject(new Error("Reply never arrived"));
}, discovery.replyTimeout + 500);
discovery.on(service + "-device-" + changeType, function onChange() {
discovery.off(service + "-device-" + changeType, onChange);
clearTimeout(timer);
deferred.resolve();
});
discovery.scan();
return deferred.promise;
}

View File

@ -0,0 +1,5 @@
[DEFAULT]
head =
tail =
[test_discovery.js]

View File

@ -71,6 +71,25 @@ exports.items = [
name: "csscoverage toggle",
hidden: true,
description: l10n.lookup("csscoverageToggleDesc2"),
state: {
isChecked: function(target) {
return csscoverage.getUsage(target).then(usage => {
return usage.isRunning();
});
},
onChange: function(target, handler) {
csscoverage.getUsage(target).then(usage => {
this.handler = ev => { handler("state-change", ev); };
usage.on("state-change", this.handler);
});
},
offChange: function(target, handler) {
csscoverage.getUsage(target).then(usage => {
usage.off("state-change", this.handler);
this.handler = undefined;
});
},
},
exec: function*(args, context) {
let target = context.environment.target;
let usage = yield csscoverage.getUsage(target);
@ -78,13 +97,8 @@ exports.items = [
throw new Error(l10n.lookup("csscoverageNoRemoteError"));
}
let running = yield usage.toggle();
if (running) {
return l10n.lookup("csscoverageRunningReply");
}
yield usage.stop();
yield gDevTools.showToolbox(target, "styleeditor");
yield usage.toggle(context.environment.chromeWindow,
context.environment.target);
}
},
{

View File

@ -17,6 +17,7 @@ PARALLEL_DIRS += [
'qrcode',
'transport',
'tern',
'discovery'
]
MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']

View File

@ -7,7 +7,9 @@
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const events = require("sdk/event/core");
const protocol = require("devtools/server/protocol");
const { method, custom, RetVal, Arg } = protocol;
@ -75,6 +77,13 @@ const l10n = exports.l10n = {
let UsageReportActor = protocol.ActorClass({
typeName: "usageReport",
events: {
"state-change" : {
type: "stateChange",
stateChange: Arg(0, "json")
}
},
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
@ -83,6 +92,9 @@ let UsageReportActor = protocol.ActorClass({
this._onTabLoad = this._onTabLoad.bind(this);
this._onChange = this._onChange.bind(this);
this._notifyOn = Ci.nsIWebProgress.NOTIFY_STATUS |
Ci.nsIWebProgress.NOTIFY_STATE_ALL
},
destroy: function() {
@ -107,12 +119,34 @@ let UsageReportActor = protocol.ActorClass({
this._running = true;
this._tooManyUnused = false;
this._tabActor.browser.addEventListener("load", this._onTabLoad, true);
this._progressListener = {
QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference ]),
this._observeMutations(this._tabActor.window.document);
onStateChange: (progress, request, flags, status) => {
let isStop = flags & Ci.nsIWebProgressListener.STATE_STOP;
let isWindow = flags & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
if (isStop && isWindow) {
this._onTabLoad(progress.DOMWindow.document);
}
},
onLocationChange: () => {},
onProgressChange: () => {},
onSecurityChange: () => {},
onStatusChange: () => {},
destroy: () => {}
};
this._progress = this._tabActor.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this._progress.addProgressListener(this._progressListener, this._notifyOn);
this._populateKnownRules(this._tabActor.window.document);
this._updateUsage(this._tabActor.window.document, false);
events.emit(this, "state-change", { isRunning: true });
}),
/**
@ -123,19 +157,18 @@ let UsageReportActor = protocol.ActorClass({
throw new Error(l10n.lookup("csscoverageNotRunningError"));
}
this._tabActor.browser.removeEventListener("load", this._onTabLoad, true);
this._progress.removeProgressListener(this._progressListener, this._notifyOn);
this._progress = undefined;
this._running = false;
events.emit(this, "state-change", { isRunning: false });
}),
/**
* Start/stop recording usage data depending on what we're currently doing.
*/
toggle: method(function() {
return this._running ?
this.stop().then(() => false) :
this.start().then(() => true);
}, {
response: RetVal("boolean"),
return this._running ? this.stop() : this.start();
}),
/**
@ -155,10 +188,9 @@ let UsageReportActor = protocol.ActorClass({
}),
/**
* Called from the tab "load" event
* Called by the ProgressListener to simulate a "load" event
*/
_onTabLoad: function(ev) {
let document = ev.target;
_onTabLoad: function(document) {
this._populateKnownRules(document);
this._updateUsage(document, true);
@ -435,15 +467,6 @@ let UsageReportActor = protocol.ActorClass({
response: RetVal("json")
}),
/**
* For testing only. Is css coverage running.
*/
_testOnly_isRunning: method(function() {
return this._running;
}, {
response: { value: RetVal("boolean") }
}),
/**
* For testing only. What pages did we visit.
*/
@ -697,6 +720,16 @@ const sheetToUrl = exports.sheetToUrl = function(stylesheet) {
throw new Error("Unknown sheet source");
}
/**
* Running more than one usage report at a time is probably bad for performance
* and it isn't particularly useful, and it's confusing from a notification POV
* so we only allow one.
*/
let isRunning = false;
let notification;
let target;
let chromeWindow;
/**
* Front for UsageReportActor
*/
@ -707,31 +740,46 @@ const UsageReportFront = protocol.FrontClass(UsageReportActor, {
this.manage(this);
},
/**
* Server-side start is above. Client-side start adds a notification box
*/
start: custom(function(chromeWindow, target) {
if (chromeWindow != null) {
let gnb = chromeWindow.document.getElementById("global-notificationbox");
this.notification = gnb.getNotificationWithValue("csscoverage-running");
_onStateChange: protocol.preEvent("state-change", function(ev) {
isRunning = ev.isRunning;
ev.target = target;
if (this.notification == null) {
let notifyStop = ev => {
if (ev == "removed") {
if (isRunning) {
let gnb = chromeWindow.document.getElementById("global-notificationbox");
notification = gnb.getNotificationWithValue("csscoverage-running");
if (notification == null) {
let notifyStop = reason => {
if (reason == "removed") {
this.stop();
gDevTools.showToolbox(target, "styleeditor");
}
};
let msg = l10n.lookup("csscoverageRunningReply");
this.notification = gnb.appendNotification(msg,
"csscoverage-running",
"", // i.e. no image
gnb.PRIORITY_INFO_HIGH,
null, // i.e. no buttons
notifyStop);
notification = gnb.appendNotification(msg, "csscoverage-running",
"", // i.e. no image
gnb.PRIORITY_INFO_HIGH,
null, // i.e. no buttons
notifyStop);
}
}
else {
if (notification) {
notification.remove();
notification = undefined;
}
gDevTools.showToolbox(target, "styleeditor");
target = undefined;
}
}),
/**
* Server-side start is above. Client-side start adds a notification box
*/
start: custom(function(newChromeWindow, newTarget) {
target = newTarget;
chromeWindow = newChromeWindow;
return this._start();
}, {
@ -739,18 +787,23 @@ const UsageReportFront = protocol.FrontClass(UsageReportActor, {
}),
/**
* Client-side stop also removes the notification box
* Server-side start is above. Client-side start adds a notification box
*/
stop: custom(function() {
if (this.notification != null) {
this.notification.remove();
this.notification = undefined;
}
toggle: custom(function(newChromeWindow, newTarget) {
target = newTarget;
chromeWindow = newChromeWindow;
return this._stop();
return this._toggle();
}, {
impl: "_stop"
impl: "_toggle"
}),
/**
* We count STARTING and STOPPING as 'running'
*/
isRunning: function() {
return isRunning;
}
});
exports.UsageReportFront = UsageReportFront;

View File

@ -39,7 +39,8 @@ let addonManager = null;
* about them.
*/
function mapURIToAddonID(uri, id) {
if ((Services.appinfo.ID || undefined) == B2G_ID) {
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT ||
(Services.appinfo.ID || undefined) == B2G_ID) {
return false;
}
@ -5058,7 +5059,13 @@ ThreadSources.prototype = {
.QueryInterface(Ci.nsIURL);
if (url.fileExtension === "js") {
spec.contentType = "text/javascript";
spec.text = aScript.source.text;
// If the Debugger API wasn't able to load the source,
// because sources were discarded
// (javascript.options.discardSystemSource == true),
// give source() a chance to fetch them.
if (aScript.source.text != "[no source]") {
spec.text = aScript.source.text;
}
}
} catch(ex) {
// Not a valid URI.

View File

@ -1199,6 +1199,8 @@ DebuggerServerConnection.prototype = {
* @return Promise
* The promise is resolved when copying completes or rejected
* if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
onBulkPacket: function(packet) {
let { actor: actorKey, type, length } = packet;

View File

@ -28,6 +28,7 @@ const { Cc, Ci, Cu } = require("chrome");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dumpn, dumpv } = DevToolsUtils;
const StreamUtils = require("devtools/toolkit/transport/stream-utils");
const EventEmitter = require("devtools/toolkit/event-emitter");
DevToolsUtils.defineLazyGetter(this, "unicodeConverter", () => {
const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
@ -274,8 +275,9 @@ BulkPacket.prototype.read = function(stream) {
length: this.length,
copyTo: (output) => {
dumpv("CT length: " + this.length);
deferred.resolve(StreamUtils.copyStream(stream, output, this.length));
return deferred.promise;
let copying = StreamUtils.copyStream(stream, output, this.length);
deferred.resolve(copying);
return copying;
},
stream: stream,
done: deferred
@ -323,8 +325,9 @@ BulkPacket.prototype.write = function(stream) {
this._readyForWriting.resolve({
copyFrom: (input) => {
dumpv("CF length: " + this.length);
deferred.resolve(StreamUtils.copyStream(input, stream, this.length));
return deferred.promise;
let copying = StreamUtils.copyStream(input, stream, this.length);
deferred.resolve(copying);
return copying;
},
stream: stream,
done: deferred

View File

@ -8,6 +8,7 @@ const { Ci, Cc, Cu, Cr, CC } = require("chrome");
const Services = require("Services");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dumpv } = DevToolsUtils;
const EventEmitter = require("devtools/toolkit/event-emitter");
DevToolsUtils.defineLazyGetter(this, "IOUtil", () => {
return Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
@ -59,6 +60,7 @@ function copyStream(input, output, length) {
}
function StreamCopier(input, output, length) {
EventEmitter.decorate(this);
this._id = StreamCopier._nextId++;
this.input = input;
// Save off the base output stream, since we know it's async as we've required
@ -70,13 +72,20 @@ function StreamCopier(input, output, length) {
createInstance(Ci.nsIBufferedOutputStream);
this.output.init(output, BUFFER_SIZE);
}
this._length = length;
this._amountLeft = length;
this._deferred = promise.defer();
this._copy = this._copy.bind(this);
this._flush = this._flush.bind(this);
this._destroy = this._destroy.bind(this);
this._deferred.promise.then(this._destroy, this._destroy);
// Copy promise's then method up to this object.
// Allows the copier to offer a promise interface for the simple succeed or
// fail scenarios, but also emit events (due to the EventEmitter) for other
// states, like progress.
this.then = this._deferred.promise.then.bind(this._deferred.promise);
this.then(this._destroy, this._destroy);
// Stream ready callback starts as |_copy|, but may switch to |_flush| at end
// if flushing would block the output stream.
@ -86,15 +95,17 @@ StreamCopier._nextId = 0;
StreamCopier.prototype = {
get copied() { return this._deferred.promise; },
copy: function() {
try {
this._copy();
} catch(e) {
this._deferred.reject(e);
}
return this.copied;
// Dispatch to the next tick so that it's possible to attach a progress
// event listener, even for extremely fast copies (like when testing).
Services.tm.currentThread.dispatch(() => {
try {
this._copy();
} catch(e) {
this._deferred.reject(e);
}
}, 0);
return this;
},
_copy: function() {
@ -115,6 +126,7 @@ StreamCopier.prototype = {
this._amountLeft -= bytesCopied;
this._debug("Copied: " + bytesCopied +
", Left: " + this._amountLeft);
this._emitProgress();
if (this._amountLeft === 0) {
this._debug("Copy done!");
@ -126,6 +138,13 @@ StreamCopier.prototype = {
this.input.asyncWait(this, 0, 0, Services.tm.currentThread);
},
_emitProgress: function() {
this.emit("progress", {
bytesSent: this._length - this._amountLeft,
totalBytes: this._length
});
},
_flush: function() {
try {
this.output.flush();

View File

@ -88,6 +88,8 @@ const PACKET_HEADER_MAX = 200;
* @return Promise
* The promise is resolved when copying completes or rejected if any
* (unexpected) errors occur.
* This object also emits "progress" events for each chunk that is
* copied. See stream-utils.js.
*
* - onClosed(reason) - called when the connection is closed. |reason| is
* an optional nsresult or object, typically passed when the transport is
@ -172,6 +174,8 @@ DebuggerTransport.prototype = {
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
startBulkSend: function(header) {
let packet = new BulkPacket(this);
@ -576,9 +580,10 @@ LocalDebuggerTransport.prototype = {
type: type,
length: length,
copyTo: (output) => {
deferred.resolve(
StreamUtils.copyStream(pipe.inputStream, output, length));
return deferred.promise;
let copying =
StreamUtils.copyStream(pipe.inputStream, output, length);
deferred.resolve(copying);
return copying;
},
stream: pipe.inputStream,
done: deferred
@ -598,9 +603,10 @@ LocalDebuggerTransport.prototype = {
sendDeferred.resolve({
copyFrom: (input) => {
copyDeferred.resolve(
StreamUtils.copyStream(input, pipe.outputStream, length));
return copyDeferred.promise;
let copying =
StreamUtils.copyStream(input, pipe.outputStream, length);
copyDeferred.resolve(copying);
return copying;
},
stream: pipe.outputStream,
done: copyDeferred