Backed out 2 changesets (bug 1435791) for bustage at /tests/test_mozbuild_reading.py on a CLOSED TREE

Backed out changeset 88c8ba0ee51d (bug 1435791)
Backed out changeset 3d7cea225c57 (bug 1435791)
This commit is contained in:
Coroiu Cristina 2018-02-07 12:04:33 +02:00
parent 61355e5dd1
commit 1ecbdba719
18 changed files with 1976 additions and 3 deletions

View File

@ -36,6 +36,8 @@
<!ENTITY runtimeMenu_takeScreenshot_accesskey "S">
<!ENTITY runtimeMenu_showDetails_label "Runtime Info">
<!ENTITY runtimeMenu_showDetails_accesskey "E">
<!ENTITY runtimeMenu_showMonitor_label "Monitor">
<!ENTITY runtimeMenu_showMonitor_accesskey "M">
<!ENTITY runtimeMenu_showDevicePrefs_label "Device Preferences">
<!ENTITY runtimeMenu_showDevicePrefs_accesskey "D">
<!ENTITY runtimeMenu_showSettings_label "Device Settings">
@ -145,6 +147,10 @@
<!ENTITY devicesetting_newtext "Setting value">
<!ENTITY devicesetting_addnew "Add new setting">
<!-- Monitor -->
<!ENTITY monitor_title "Monitor">
<!ENTITY monitor_help "Help">
<!-- WiFi Authentication -->
<!-- LOCALIZATION NOTE (wifi_auth_header): The header displayed on the dialog
that instructs the user to transfer an authentication token to the

View File

@ -16,6 +16,8 @@ webide.jar:
content/runtimedetails.xhtml (runtimedetails.xhtml)
content/prefs.js (prefs.js)
content/prefs.xhtml (prefs.xhtml)
content/monitor.xhtml (monitor.xhtml)
content/monitor.js (monitor.js)
content/devicepreferences.js (devicepreferences.js)
content/devicepreferences.xhtml (devicepreferences.xhtml)
content/wifi-auth.js (wifi-auth.js)

View File

@ -0,0 +1,739 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
const Services = require("Services");
const {AppManager} = require("devtools/client/webide/modules/app-manager");
const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
const {Connection} = require("devtools/shared/client/connection-manager");
const EventEmitter = require("devtools/shared/old-event-emitter");
window.addEventListener("load", function () {
window.addEventListener("resize", Monitor.resize);
window.addEventListener("unload", Monitor.unload);
document.querySelector("#close").onclick = () => {
window.parent.UI.openProject();
};
Monitor.load();
}, {once: true});
/**
* The Monitor is a WebIDE tool used to display any kind of time-based data in
* the form of graphs.
*
* The data can come from a Firefox OS device, or from a WebSockets
* server running locally.
*
* The format of a data update is typically an object like:
*
* { graph: 'mygraph', curve: 'mycurve', value: 42, time: 1234 }
*
* or an array of such objects. For more details on the data format, see the
* `Graph.update(data)` method.
*/
var Monitor = {
apps: new Map(),
graphs: new Map(),
front: null,
socket: null,
wstimeout: null,
b2ginfo: false,
b2gtimeout: null,
/**
* Add new data to the graphs, create a new graph if necessary.
*/
update: function (data, fallback) {
if (Array.isArray(data)) {
data.forEach(d => Monitor.update(d, fallback));
return;
}
if (Monitor.b2ginfo && data.graph === "USS") {
// If we're polling b2g-info, ignore USS updates from the device's
// USSAgents (see Monitor.pollB2GInfo()).
return;
}
if (fallback) {
for (let key in fallback) {
if (!data[key]) {
data[key] = fallback[key];
}
}
}
let graph = Monitor.graphs.get(data.graph);
if (!graph) {
let element = document.createElement("div");
element.classList.add("graph");
document.body.appendChild(element);
graph = new Graph(data.graph, element);
Monitor.resize(); // a scrollbar might have dis/reappeared
Monitor.graphs.set(data.graph, graph);
}
graph.update(data);
},
/**
* Initialize the Monitor.
*/
load: function () {
AppManager.on("app-manager-update", Monitor.onAppManagerUpdate);
Monitor.connectToRuntime();
Monitor.connectToWebSocket();
},
/**
* Clean up the Monitor.
*/
unload: function () {
AppManager.off("app-manager-update", Monitor.onAppManagerUpdate);
Monitor.disconnectFromRuntime();
Monitor.disconnectFromWebSocket();
},
/**
* Resize all the graphs.
*/
resize: function () {
for (let graph of Monitor.graphs.values()) {
graph.resize();
}
},
/**
* When WebIDE connects to a new runtime, start its data forwarders.
*/
onAppManagerUpdate: function (event, what, details) {
switch (what) {
case "runtime-global-actors":
Monitor.connectToRuntime();
break;
case "connection":
if (AppManager.connection.status == Connection.Status.DISCONNECTED) {
Monitor.disconnectFromRuntime();
}
break;
}
},
/**
* Use an AppActorFront on a runtime to watch track its apps.
*/
connectToRuntime: function () {
Monitor.pollB2GInfo();
let client = AppManager.connection && AppManager.connection.client;
let resp = AppManager._listTabsResponse;
if (client && resp && !Monitor.front) {
Monitor.front = new AppActorFront(client, resp);
Monitor.front.watchApps(Monitor.onRuntimeAppEvent);
}
},
/**
* Destroy our AppActorFront.
*/
disconnectFromRuntime: function () {
Monitor.unpollB2GInfo();
if (Monitor.front) {
Monitor.front.unwatchApps(Monitor.onRuntimeAppEvent);
Monitor.front = null;
}
},
/**
* Try connecting to a local websockets server and accept updates from it.
*/
connectToWebSocket: function () {
let webSocketURL = Services.prefs.getCharPref("devtools.webide.monitorWebSocketURL");
try {
Monitor.socket = new WebSocket(webSocketURL);
Monitor.socket.onmessage = function (event) {
Monitor.update(JSON.parse(event.data));
};
Monitor.socket.onclose = function () {
Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
};
} catch (e) {
Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000);
}
},
/**
* Used when cleaning up.
*/
disconnectFromWebSocket: function () {
clearTimeout(Monitor.wstimeout);
if (Monitor.socket) {
Monitor.socket.onclose = () => {};
Monitor.socket.close();
}
},
/**
* When an app starts on the runtime, start a monitor actor for its process.
*/
onRuntimeAppEvent: function (type, app) {
if (type !== "appOpen" && type !== "appClose") {
return;
}
let client = AppManager.connection.client;
app.getForm().then(form => {
if (type === "appOpen") {
app.monitorClient = new MonitorClient(client, form);
app.monitorClient.start();
app.monitorClient.on("update", Monitor.onRuntimeUpdate);
Monitor.apps.set(form.monitorActor, app);
} else {
let app = Monitor.apps.get(form.monitorActor);
if (app) {
app.monitorClient.stop(() => app.monitorClient.destroy());
Monitor.apps.delete(form.monitorActor);
}
}
});
},
/**
* Accept data updates from the monitor actors of a runtime.
*/
onRuntimeUpdate: function (type, packet) {
let fallback = {}, app = Monitor.apps.get(packet.from);
if (app) {
fallback.curve = app.manifest.name;
}
Monitor.update(packet.data, fallback);
},
/**
* Bug 1047355: If possible, parsing the output of `b2g-info` has several
* benefits over bug 1037465's multi-process USSAgent approach, notably:
* - Works for older Firefox OS devices (pre-2.1),
* - Doesn't need certified-apps debugging,
* - Polling time is synchronized for all processes.
* TODO: After bug 1043324 lands, consider removing this hack.
*/
pollB2GInfo: function () {
if (AppManager.selectedRuntime) {
let device = AppManager.selectedRuntime.device;
if (device && device.shell) {
device.shell("b2g-info").then(s => {
let lines = s.split("\n");
let line = "";
// Find the header row to locate NAME and USS, looks like:
// ' NAME PID NICE USS PSS RSS VSIZE OOM_ADJ USER '.
while (!line.includes("NAME")) {
if (lines.length < 1) {
// Something is wrong with this output, don't trust b2g-info.
Monitor.unpollB2GInfo();
return;
}
line = lines.shift();
}
let namelength = line.indexOf("NAME") + "NAME".length;
let ussindex = line.slice(namelength).split(/\s+/).indexOf("USS");
// Get the NAME and USS in each following line, looks like:
// 'Homescreen 375 18 12.6 16.3 27.1 67.8 4 app_375'.
while (lines.length > 0 && lines[0].length > namelength) {
line = lines.shift();
let name = line.slice(0, namelength);
let uss = line.slice(namelength).split(/\s+/)[ussindex];
Monitor.update({
curve: name.trim(),
value: 1024 * 1024 * parseFloat(uss) // Convert MB to bytes.
}, {
// Note: We use the fallback object to set the graph name to 'USS'
// so that Monitor.update() can ignore USSAgent updates.
graph: "USS"
});
}
});
}
}
Monitor.b2ginfo = true;
Monitor.b2gtimeout = setTimeout(Monitor.pollB2GInfo, 350);
},
/**
* Polling b2g-info doesn't work or is no longer needed.
*/
unpollB2GInfo: function () {
clearTimeout(Monitor.b2gtimeout);
Monitor.b2ginfo = false;
}
};
/**
* A MonitorClient is used as an actor client of a runtime's monitor actors,
* receiving its updates.
*/
function MonitorClient(client, form) {
this.client = client;
this.actor = form.monitorActor;
this.events = ["update"];
EventEmitter.decorate(this);
this.client.registerClient(this);
}
MonitorClient.prototype.destroy = function () {
this.client.unregisterClient(this);
};
MonitorClient.prototype.start = function () {
this.client.request({
to: this.actor,
type: "start"
});
};
MonitorClient.prototype.stop = function (callback) {
this.client.request({
to: this.actor,
type: "stop"
}, callback);
};
/**
* A Graph populates a container DOM element with an SVG graph and a legend.
*/
function Graph(name, element) {
this.name = name;
this.element = element;
this.curves = new Map();
this.events = new Map();
this.ignored = new Set();
this.enabled = true;
this.request = null;
this.x = d3.time.scale();
this.y = d3.scale.linear();
this.xaxis = d3.svg.axis().scale(this.x).orient("bottom");
this.yaxis = d3.svg.axis().scale(this.y).orient("left");
this.xformat = d3.time.format("%I:%M:%S");
this.yformat = this.formatter(1);
this.yaxis.tickFormat(this.formatter(0));
this.line = d3.svg.line().interpolate("linear")
.x(function (d) { return this.x(d.time); })
.y(function (d) { return this.y(d.value); });
this.color = d3.scale.category10();
this.svg = d3.select(element).append("svg").append("g")
.attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
this.xelement = this.svg.append("g").attr("class", "x axis").call(this.xaxis);
this.yelement = this.svg.append("g").attr("class", "y axis").call(this.yaxis);
// RULERS on axes
let xruler = this.xruler = this.svg.select(".x.axis").append("g").attr("class", "x ruler");
xruler.append("line").attr("y2", 6);
xruler.append("line").attr("stroke-dasharray", "1,1");
xruler.append("text").attr("y", 9).attr("dy", ".71em");
let yruler = this.yruler = this.svg.select(".y.axis").append("g").attr("class", "y ruler");
yruler.append("line").attr("x2", -6);
yruler.append("line").attr("stroke-dasharray", "1,1");
yruler.append("text").attr("x", -9).attr("dy", ".32em");
let self = this;
d3.select(element).select("svg")
.on("mousemove", function () {
let mouse = d3.mouse(this);
self.mousex = mouse[0] - self.margin.left,
self.mousey = mouse[1] - self.margin.top;
xruler.attr("transform", "translate(" + self.mousex + ",0)");
yruler.attr("transform", "translate(0," + self.mousey + ")");
});
/* .on('mouseout', function() {
self.xruler.attr('transform', 'translate(-500,0)');
self.yruler.attr('transform', 'translate(0,-500)');
});*/
this.mousex = this.mousey = -500;
let sidebar = d3.select(this.element).append("div").attr("class", "sidebar");
let title = sidebar.append("label").attr("class", "graph-title");
title.append("input")
.attr("type", "checkbox")
.attr("checked", "true")
.on("click", function () { self.toggle(); });
title.append("span").text(this.name);
this.legend = sidebar.append("div").attr("class", "legend");
this.resize = this.resize.bind(this);
this.render = this.render.bind(this);
this.averages = this.averages.bind(this);
setInterval(this.averages, 1000);
this.resize();
}
Graph.prototype = {
/**
* These margin are used to properly position the SVG graph items inside the
* container element.
*/
margin: {
top: 10,
right: 150,
bottom: 20,
left: 50
},
/**
* A Graph can be collapsed by the user.
*/
toggle: function () {
if (this.enabled) {
this.element.classList.add("disabled");
this.enabled = false;
} else {
this.element.classList.remove("disabled");
this.enabled = true;
}
Monitor.resize();
},
/**
* If the container element is resized (e.g. because the window was resized or
* a scrollbar dis/appeared), the graph needs to be resized as well.
*/
resize: function () {
let style = getComputedStyle(this.element),
height = parseFloat(style.height) - this.margin.top - this.margin.bottom,
width = parseFloat(style.width) - this.margin.left - this.margin.right;
d3.select(this.element).select("svg")
.attr("width", width + this.margin.left)
.attr("height", height + this.margin.top + this.margin.bottom);
this.x.range([0, width]);
this.y.range([height, 0]);
this.xelement.attr("transform", "translate(0," + height + ")");
this.xruler.select("line[stroke-dasharray]").attr("y2", -height);
this.yruler.select("line[stroke-dasharray]").attr("x2", width);
},
/**
* If the domain of the Graph's data changes (on the time axis and/or on the
* value axis), the axes' domains need to be updated and the graph items need
* to be rescaled in order to represent all the data.
*/
rescale: function () {
let gettime = v => { return v.time; },
getvalue = v => { return v.value; },
ignored = c => { return this.ignored.has(c.id); };
let xmin = null, xmax = null, ymin = null, ymax = null;
for (let curve of this.curves.values()) {
if (ignored(curve)) {
continue;
}
if (xmax == null || curve.xmax > xmax) {
xmax = curve.xmax;
}
if (xmin == null || curve.xmin < xmin) {
xmin = curve.xmin;
}
if (ymax == null || curve.ymax > ymax) {
ymax = curve.ymax;
}
if (ymin == null || curve.ymin < ymin) {
ymin = curve.ymin;
}
}
for (let event of this.events.values()) {
if (ignored(event)) {
continue;
}
if (xmax == null || event.xmax > xmax) {
xmax = event.xmax;
}
if (xmin == null || event.xmin < xmin) {
xmin = event.xmin;
}
}
let oldxdomain = this.x.domain();
if (xmin != null && xmax != null) {
this.x.domain([xmin, xmax]);
let newxdomain = this.x.domain();
if (newxdomain[0] !== oldxdomain[0] || newxdomain[1] !== oldxdomain[1]) {
this.xelement.call(this.xaxis);
}
}
let oldydomain = this.y.domain();
if (ymin != null && ymax != null) {
this.y.domain([ymin, ymax]).nice();
let newydomain = this.y.domain();
if (newydomain[0] !== oldydomain[0] || newydomain[1] !== oldydomain[1]) {
this.yelement.call(this.yaxis);
}
}
},
/**
* Add new values to the graph.
*/
update: function (data) {
delete data.graph;
let time = data.time || Date.now();
delete data.time;
let curve = data.curve;
delete data.curve;
// Single curve value, e.g. { curve: 'memory', value: 42, time: 1234 }.
if ("value" in data) {
this.push(this.curves, curve, [{time: time, value: data.value}]);
delete data.value;
}
// Several curve values, e.g. { curve: 'memory', values: [{value: 42, time: 1234}] }.
if ("values" in data) {
this.push(this.curves, curve, data.values);
delete data.values;
}
// Punctual event, e.g. { event: 'gc', time: 1234 },
// event with duration, e.g. { event: 'jank', duration: 425, time: 1234 }.
if ("event" in data) {
this.push(this.events, data.event, [{time: time, value: data.duration}]);
delete data.event;
delete data.duration;
}
// Remaining keys are curves, e.g. { time: 1234, memory: 42, battery: 13, temperature: 45 }.
for (let key in data) {
this.push(this.curves, key, [{time: time, value: data[key]}]);
}
// If no render is currently pending, request one.
if (this.enabled && !this.request) {
this.request = requestAnimationFrame(this.render);
}
},
/**
* Insert new data into the graph's data structures.
*/
push: function (collection, id, values) {
// Note: collection is either `this.curves` or `this.events`.
let item = collection.get(id);
if (!item) {
item = { id: id, values: [], xmin: null, xmax: null, ymin: 0, ymax: null, average: 0 };
collection.set(id, item);
}
for (let v of values) {
let time = new Date(v.time), value = +v.value;
// Update the curve/event's domain values.
if (item.xmax == null || time > item.xmax) {
item.xmax = time;
}
if (item.xmin == null || time < item.xmin) {
item.xmin = time;
}
if (item.ymax == null || value > item.ymax) {
item.ymax = value;
}
if (item.ymin == null || value < item.ymin) {
item.ymin = value;
}
// Note: A curve's average is not computed here. Call `graph.averages()`.
item.values.push({ time: time, value: value });
}
},
/**
* Render the SVG graph with curves, events, crosshair and legend.
*/
render: function () {
this.request = null;
this.rescale();
// DATA
let self = this,
getid = d => { return d.id; },
gettime = d => { return d.time.getTime(); },
getline = d => { return self.line(d.values); },
getcolor = d => { return self.color(d.id); },
getvalues = d => { return d.values; },
ignored = d => { return self.ignored.has(d.id); };
// Convert our maps to arrays for d3.
let curvedata = [...this.curves.values()],
eventdata = [...this.events.values()],
data = curvedata.concat(eventdata);
// CURVES
// Map curve data to curve elements.
let curves = this.svg.selectAll(".curve").data(curvedata, getid);
// Create new curves (no element corresponding to the data).
curves.enter().append("g").attr("class", "curve").append("path")
.style("stroke", getcolor);
// Delete old curves (elements corresponding to data not present anymore).
curves.exit().remove();
// Update all curves from data.
this.svg.selectAll(".curve").select("path")
.attr("d", d => { return ignored(d) ? "" : getline(d); });
let height = parseFloat(getComputedStyle(this.element).height) - this.margin.top - this.margin.bottom;
// EVENTS
// Map event data to event elements.
let events = this.svg.selectAll(".event-slot").data(eventdata, getid);
// Create new events.
events.enter().append("g").attr("class", "event-slot");
// Remove old events.
events.exit().remove();
// Get all occurences of an event, and map its data to them.
let lines = this.svg.selectAll(".event-slot")
.style("stroke", d => { return ignored(d) ? "none" : getcolor(d); })
.selectAll(".event")
.data(getvalues, gettime);
// Create new event occurrence.
lines.enter().append("line").attr("class", "event").attr("y2", height);
// Delete old event occurrence.
lines.exit().remove();
// Update all event occurrences from data.
this.svg.selectAll(".event")
.attr("transform", d => { return "translate(" + self.x(d.time) + ",0)"; });
// CROSSHAIR
// TODO select curves and events, intersect with curves and show values/hovers
// e.g. look like http://code.shutterstock.com/rickshaw/examples/lines.html
// Update crosshair labels on each axis.
this.xruler.select("text").text(self.xformat(self.x.invert(self.mousex)));
this.yruler.select("text").text(self.yformat(self.y.invert(self.mousey)));
// LEGEND
// Map data to legend elements.
let legends = this.legend.selectAll("label").data(data, getid);
// Update averages.
legends.attr("title", c => { return "Average: " + self.yformat(c.average); });
// Create new legends.
let newlegend = legends.enter().append("label");
newlegend.append("input").attr("type", "checkbox").attr("checked", "true").on("click", function (c) {
if (ignored(c)) {
this.parentElement.classList.remove("disabled");
self.ignored.delete(c.id);
} else {
this.parentElement.classList.add("disabled");
self.ignored.add(c.id);
}
self.update({}); // if no re-render is pending, request one.
});
newlegend.append("span").attr("class", "legend-color").style("background-color", getcolor);
newlegend.append("span").attr("class", "legend-id").text(getid);
// Delete old legends.
legends.exit().remove();
},
/**
* Returns a SI value formatter with a given precision.
*/
formatter: function (decimals) {
return value => {
// Don't use sub-unit SI prefixes (milli, micro, etc.).
if (Math.abs(value) < 1) return value.toFixed(decimals);
// SI prefix, e.g. 1234567 will give '1.2M' at precision 1.
let prefix = d3.formatPrefix(value);
return prefix.scale(value).toFixed(decimals) + prefix.symbol;
};
},
/**
* Compute the average of each time series.
*/
averages: function () {
for (let c of this.curves.values()) {
let length = c.values.length;
if (length > 0) {
let total = 0;
c.values.forEach(v => total += v.value);
c.average = (total / length);
}
}
},
/**
* Bisect a time serie to find the data point immediately left of `time`.
*/
bisectTime: d3.bisector(d => d.time).left,
/**
* Get all curve values at a given time.
*/
valuesAt: function (time) {
let values = { time: time };
for (let id of this.curves.keys()) {
let curve = this.curves.get(id);
// Find the closest value just before `time`.
let i = this.bisectTime(curve.values, time);
if (i < 0) {
// Curve starts after `time`, use first value.
values[id] = curve.values[0].value;
} else if (i > curve.values.length - 2) {
// Curve ends before `time`, use last value.
values[id] = curve.values[curve.values.length - 1].value;
} else {
// Curve has two values around `time`, interpolate.
let v1 = curve.values[i],
v2 = curve.values[i + 1],
delta = (time - v1.time) / (v2.time - v1.time);
values[id] = v1.value + (v2.value - v1.time) * delta;
}
}
return values;
}
};

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html [
<!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
<link rel="stylesheet" href="chrome://webide/skin/monitor.css" type="text/css"/>
<script src="chrome://devtools/content/shared/vendor/d3.js"></script>
<script type="application/javascript" src="monitor.js"></script>
</head>
<body>
<div id="controls">
<a href="https://developer.mozilla.org/docs/Tools/WebIDE/Monitor" target="_blank">&monitor_help;</a>
<a id="close">&deck_close;</a>
</div>
<h1>&monitor_title;</h1>
</body>
</html>

View File

@ -177,6 +177,9 @@ var UI = {
this.updateTitle();
this.updateCommands();
break;
case "install-progress":
this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes));
break;
case "runtime-targets":
this.autoSelectProject();
break;
@ -210,6 +213,13 @@ var UI = {
_busyOperationDescription: null,
_busyPromise: null,
updateProgress: function (percent) {
let progress = document.querySelector("#action-busy-determined");
progress.mode = "determined";
progress.value = percent;
this.setupBusyTimeout();
},
busy: function () {
let win = document.querySelector("window");
win.classList.add("busy");
@ -379,6 +389,7 @@ var UI = {
}
// Runtime commands
let monitorCmd = document.querySelector("#cmd_showMonitor");
let screenshotCmd = document.querySelector("#cmd_takeScreenshot");
let detailsCmd = document.querySelector("#cmd_showRuntimeDetails");
let disconnectCmd = document.querySelector("#cmd_disconnectRuntime");
@ -387,6 +398,7 @@ var UI = {
if (AppManager.connected) {
if (AppManager.deviceFront) {
monitorCmd.removeAttribute("disabled");
detailsCmd.removeAttribute("disabled");
screenshotCmd.removeAttribute("disabled");
}
@ -395,6 +407,7 @@ var UI = {
}
disconnectCmd.removeAttribute("disabled");
} else {
monitorCmd.setAttribute("disabled", "true");
detailsCmd.setAttribute("disabled", "true");
screenshotCmd.setAttribute("disabled", "true");
disconnectCmd.setAttribute("disabled", "true");
@ -878,6 +891,10 @@ var Cmds = {
UI.selectDeckPanel("devicepreferences");
},
showMonitor: function () {
UI.selectDeckPanel("monitor");
},
play: Task.async(function* () {
let busy;
switch (AppManager.selectedProject.type) {

View File

@ -44,6 +44,7 @@
<command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
<command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
<command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/>
<command id="cmd_showMonitor" oncommand="Cmds.showMonitor()" label="&runtimeMenu_showMonitor_label;"/>
<command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
<command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
<command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
@ -79,6 +80,7 @@
<menu id="menu-runtime" label="&runtimeMenu_label;" accesskey="&runtimeMenu_accesskey;">
<menupopup id="menu-runtime-popup">
<menuitem command="cmd_showMonitor" accesskey="&runtimeMenu_showMonitor_accesskey;"/>
<menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/>
<menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/>
<menuitem command="cmd_showDevicePrefs" accesskey="&runtimeMenu_showDevicePrefs_accesskey;"/>
@ -149,6 +151,7 @@
<iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
<iframe id="deck-panel-prefs" flex="1" src="prefs.xhtml"/>
<iframe id="deck-panel-runtimedetails" flex="1" lazysrc="runtimedetails.xhtml"/>
<iframe id="deck-panel-monitor" flex="1" lazysrc="monitor.xhtml"/>
<iframe id="deck-panel-devicepreferences" flex="1" lazysrc="devicepreferences.xhtml"/>
</deck>
<splitter class="devtools-side-splitter" id="runtime-listing-splitter"/>

View File

@ -13,6 +13,7 @@ const {AppProjects} = require("devtools/client/webide/modules/app-projects");
const TabStore = require("devtools/client/webide/modules/tab-store");
const {AppValidator} = require("devtools/client/webide/modules/app-validator");
const {ConnectionManager, Connection} = require("devtools/shared/client/connection-manager");
const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
const {getDeviceFront} = require("devtools/shared/fronts/device");
const {getPreferenceFront} = require("devtools/shared/fronts/preference");
const {Task} = require("devtools/shared/task");
@ -54,6 +55,8 @@ var AppManager = exports.AppManager = {
RuntimeScanners.enable();
this._rebuildRuntimeList();
this.onInstallProgress = this.onInstallProgress.bind(this);
this._telemetry = new Telemetry();
},
@ -90,6 +93,10 @@ var AppManager = exports.AppManager = {
* |cancel| callback that will abort the project change if desired.
* connection:
* The connection status has changed (connected, disconnected, etc.)
* install-progress:
* A project being installed to a runtime has made further progress. This
* event contains additional details about exactly how far the process is
* when such information is available.
* project:
* The selected project has changed.
* project-started:
@ -104,6 +111,8 @@ var AppManager = exports.AppManager = {
* name, manifest details, etc.
* runtime:
* The selected runtime has changed.
* runtime-apps-icons:
* The list of URLs for the runtime app icons are available.
* runtime-global-actors:
* The list of global actors for the entire runtime (but not actors for a
* specific tab or app) are now available, so we can test for features
@ -151,12 +160,38 @@ var AppManager = exports.AppManager = {
}
if (!this.connected) {
if (this._appsFront) {
this._appsFront.off("install-progress", this.onInstallProgress);
this._appsFront.unwatchApps();
this._appsFront = null;
}
this._listTabsResponse = null;
} else {
this.connection.client.listTabs().then((response) => {
this._listTabsResponse = response;
this._recordRuntimeInfo();
this.update("runtime-global-actors");
if (response.webappsActor) {
let front = new AppActorFront(this.connection.client,
response);
front.on("install-progress", this.onInstallProgress);
front.watchApps(() => this.checkIfProjectIsRunning())
.then(() => {
// This can't be done earlier as many operations
// in the apps actor require watchApps to be called
// first.
this._appsFront = front;
this._listTabsResponse = response;
this._recordRuntimeInfo();
this.update("runtime-global-actors");
})
.then(() => {
this.checkIfProjectIsRunning();
this.update("runtime-targets", { type: "apps" });
front.fetchIcons().then(() => this.update("runtime-apps-icons"));
});
} else {
this._listTabsResponse = response;
this._recordRuntimeInfo();
this.update("runtime-global-actors");
}
});
}
@ -176,6 +211,10 @@ var AppManager = exports.AppManager = {
}
},
onInstallProgress: function (event, details) {
this.update("install-progress", details);
},
isProjectRunning: function () {
if (this.selectedProject.type == "mainProcess" ||
this.selectedProject.type == "tab") {

View File

@ -42,6 +42,7 @@ ProjectList.prototype = {
// See AppManager.update() for descriptions of what these events mean.
switch (what) {
case "project-removed":
case "runtime-apps-icons":
case "runtime-targets":
case "connection":
this.update(details);

View File

@ -12,6 +12,7 @@ webide.jar:
skin/deck.css (deck.css)
skin/addons.css (addons.css)
skin/runtimedetails.css (runtimedetails.css)
skin/monitor.css (monitor.css)
skin/config-view.css (config-view.css)
skin/wifi-auth.css (wifi-auth.css)
skin/panel-listing.css (panel-listing.css)

View File

@ -0,0 +1,86 @@
/* 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/. */
/* Graph */
.graph {
height: 500px;
width: 100%;
padding-top: 20px;
padding-bottom: 20px;
margin-bottom: 30px;
background-color: white;
}
.graph > svg, .sidebar {
display: inline-block;
vertical-align: top;
}
.disabled {
opacity: 0.5;
}
.graph.disabled {
height: 30px;
}
.graph.disabled > svg {
visibility: hidden;
}
.curve path, .event-slot line {
fill: none;
stroke-width: 1.5px;
}
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis path {
fill: none;
stroke: black;
stroke-width: 1px;
shape-rendering: crispEdges;
}
.tick text, .x.ruler text, .y.ruler text {
font-size: 0.9em;
}
.x.ruler text {
text-anchor: middle;
}
.y.ruler text {
text-anchor: end;
}
/* Sidebar */
.sidebar {
width: 150px;
overflow-x: hidden;
}
.sidebar label {
cursor: pointer;
display: block;
}
.sidebar span:not(.color) {
vertical-align: 13%;
}
.sidebar input {
visibility: hidden;
}
.sidebar input:hover {
visibility: visible;
}
.graph-title {
margin-top: 5px;
font-size: 1.2em;
}
.legend-color {
display: inline-block;
height: 10px;
width: 10px;
margin-left: 1px;
margin-right: 3px;
}
.legend-id {
font-size: .9em;
}
.graph.disabled > .sidebar > .legend {
display: none;
}

View File

@ -9,6 +9,7 @@ pref("devtools.webide.restoreLastProject", true);
pref("devtools.webide.enableLocalRuntime", false);
pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
pref("devtools.webide.lastConnectedRuntime", "");
pref("devtools.webide.lastSelectedProject", "");
pref("devtools.webide.zoom", "1");

View File

@ -0,0 +1,147 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Ci, Cc} = require("chrome");
const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
const Services = require("Services");
function MonitorActor(connection) {
this.conn = connection;
this._updates = [];
this._started = false;
}
MonitorActor.prototype = {
actorPrefix: "monitor",
// Updates.
_sendUpdate: function () {
if (this._started) {
this.conn.sendActorEvent(this.actorID, "update", { data: this._updates });
this._updates = [];
}
},
// Methods available from the front.
start: function () {
if (!this._started) {
this._started = true;
Services.obs.addObserver(this, "devtools-monitor-update");
Services.obs.notifyObservers(null, "devtools-monitor-start");
this._agents.forEach(agent => this._startAgent(agent));
}
return {};
},
stop: function () {
if (this._started) {
this._agents.forEach(agent => agent.stop());
Services.obs.notifyObservers(null, "devtools-monitor-stop");
Services.obs.removeObserver(this, "devtools-monitor-update");
this._started = false;
}
return {};
},
destroy: function () {
this.stop();
},
// nsIObserver.
observe: function (subject, topic, data) {
if (topic == "devtools-monitor-update") {
try {
data = JSON.parse(data);
} catch (e) {
console.error("Observer notification data is not a valid JSON-string:",
data, e.message);
return;
}
if (!Array.isArray(data)) {
this._updates.push(data);
} else {
this._updates = this._updates.concat(data);
}
this._sendUpdate();
}
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
// Update agents (see USSAgent for an example).
_agents: [],
_startAgent: function (agent) {
try {
agent.start();
} catch (e) {
this._removeAgent(agent);
}
},
_addAgent: function (agent) {
this._agents.push(agent);
if (this._started) {
this._startAgent(agent);
}
},
_removeAgent: function (agent) {
let index = this._agents.indexOf(agent);
if (index > -1) {
this._agents.splice(index, 1);
}
},
};
MonitorActor.prototype.requestTypes = {
"start": MonitorActor.prototype.start,
"stop": MonitorActor.prototype.stop,
};
exports.MonitorActor = MonitorActor;
var USSAgent = {
_mgr: null,
_timeout: null,
_packet: {
graph: "USS",
time: null,
value: null
},
start: function () {
USSAgent._mgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
Ci.nsIMemoryReporterManager);
if (!USSAgent._mgr.residentUnique) {
throw new Error("Couldn't get USS.");
}
USSAgent.update();
},
update: function () {
if (!USSAgent._mgr) {
USSAgent.stop();
return;
}
USSAgent._packet.time = Date.now();
USSAgent._packet.value = USSAgent._mgr.residentUnique;
Services.obs.notifyObservers(null, "devtools-monitor-update",
JSON.stringify(USSAgent._packet));
USSAgent._timeout = setTimeout(USSAgent.update, 300);
},
stop: function () {
clearTimeout(USSAgent._timeout);
USSAgent._mgr = null;
}
};
MonitorActor.prototype._addAgent(USSAgent);

View File

@ -40,6 +40,7 @@ DevToolsModules(
'highlighters.js',
'layout.js',
'memory.js',
'monitor.js',
'object.js',
'pause-scoped.js',
'perf.js',

View File

@ -519,6 +519,11 @@ var DebuggerServer = {
constructor: "CSSUsageActor",
type: { tab: true }
});
this.registerModule("devtools/server/actors/monitor", {
prefix: "monitor",
constructor: "MonitorActor",
type: { tab: true }
});
this.registerModule("devtools/server/actors/timeline", {
prefix: "timeline",
constructor: "TimelineActor",

View File

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test the monitor actor.
*/
"use strict";
function run_test() {
let EventEmitter = require("devtools/shared/old-event-emitter");
function MonitorClient(client, form) {
this.client = client;
this.actor = form.monitorActor;
this.events = ["update"];
EventEmitter.decorate(this);
client.registerClient(this);
}
MonitorClient.prototype.destroy = function () {
this.client.unregisterClient(this);
};
MonitorClient.prototype.start = function (callback) {
this.client.request({
to: this.actor,
type: "start"
}, callback);
};
MonitorClient.prototype.stop = function (callback) {
this.client.request({
to: this.actor,
type: "stop"
}, callback);
};
let monitor, client;
// Start the monitor actor.
get_chrome_actors((c, form) => {
client = c;
monitor = new MonitorClient(client, form);
monitor.on("update", gotUpdate);
monitor.start(update);
});
let time = Date.now();
function update() {
let event = {
graph: "Test",
curve: "test",
value: 42,
time: time,
};
Services.obs.notifyObservers(null, "devtools-monitor-update", JSON.stringify(event));
}
function gotUpdate(type, packet) {
packet.data.forEach(function (event) {
// Ignore updates that were not sent by this test.
if (event.graph === "Test") {
Assert.equal(event.curve, "test");
Assert.equal(event.value, 42);
Assert.equal(event.time, time);
monitor.stop(function (response) {
monitor.destroy();
finishClient(client);
});
}
});
}
do_test_pending();
}

View File

@ -216,6 +216,7 @@ reason = bug 937197
[test_protocolSpec.js]
[test_registerClient.js]
[test_client_request.js]
[test_monitor_actor.js]
[test_symbols-01.js]
[test_symbols-02.js]
[test_get-executable-lines.js]

View File

@ -0,0 +1,817 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Ci, Cc, Cr} = require("chrome");
const {OS} = require("resource://gre/modules/osfile.jsm");
const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const EventEmitter = require("devtools/shared/old-event-emitter");
// Bug 1188401: When loaded from xpcshell tests, we do not have browser/ files
// and can't load target.js. Should be fixed by bug 912121.
loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
// XXX: bug 912476 make this module a real protocol.js front
// by converting webapps actor to protocol.js
const PR_USEC_PER_MSEC = 1000;
const PR_RDWR = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
const CHUNK_SIZE = 10000;
const appTargets = new Map();
function addDirToZip(writer, dir, basePath) {
let files = dir.directoryEntries;
while (files.hasMoreElements()) {
let file = files.getNext().QueryInterface(Ci.nsIFile);
if (file.isHidden() ||
file.isSpecial() ||
file.equals(writer.file)) {
continue;
}
if (file.isDirectory()) {
writer.addEntryDirectory(basePath + file.leafName + "/",
file.lastModifiedTime * PR_USEC_PER_MSEC,
true);
addDirToZip(writer, file, basePath + file.leafName + "/");
} else {
writer.addEntryFile(basePath + file.leafName,
Ci.nsIZipWriter.COMPRESSION_DEFAULT,
file,
true);
}
}
}
function getResultText(code) {
/*
* If it ever becomes necessary to convert the nsresult to a useful
* string here, we'll need an API for that.
*/
return { name: "Error code", message: code + "" };
}
function zipDirectory(zipFile, dirToArchive) {
let deferred = defer();
let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
addDirToZip(writer, dirToArchive, "");
writer.processQueue({
onStartRequest: function onStartRequest(request, context) {},
onStopRequest: (request, context, status) => {
if (status == Cr.NS_OK) {
writer.close();
deferred.resolve(zipFile);
} else {
let { name, message } = getResultText(status);
deferred.reject(name + ": " + message);
}
}
}, null);
return deferred.promise;
}
function uploadPackage(client, webappsActor, packageFile, progressCallback) {
if (client.traits.bulk) {
return uploadPackageBulk(client, webappsActor, packageFile, progressCallback);
}
return uploadPackageJSON(client, webappsActor, packageFile, progressCallback);
}
function uploadPackageJSON(client, webappsActor, packageFile, progressCallback) {
let deferred = defer();
let request = {
to: webappsActor,
type: "uploadPackage"
};
client.request(request, (res) => {
openFile(res.actor);
});
let fileSize;
let bytesRead = 0;
function emitProgress() {
progressCallback({
bytesSent: bytesRead,
totalBytes: fileSize
});
}
function openFile(actor) {
let openedFile;
OS.File.open(packageFile.path).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);
let chunkRequest = {
to: actor,
type: "chunk",
chunk,
};
client.request(chunkRequest, (res) => {
if (bytes.length == CHUNK_SIZE) {
uploadChunk(actor, file);
} else {
file.close().then(function () {
endsUpload(actor);
});
}
});
});
}
function endsUpload(actor) {
let doneRequest = {
to: actor,
type: "done"
};
client.request(doneRequest, (res) => {
deferred.resolve(actor);
});
}
return deferred.promise;
}
function uploadPackageBulk(client, webappsActor, packageFile, progressCallback) {
let deferred = defer();
let request = {
to: webappsActor,
type: "uploadPackage",
bulk: true
};
client.request(request, (res) => {
startBulkUpload(res.actor);
});
function startBulkUpload(actor) {
console.log("Starting bulk upload");
let fileSize = packageFile.fileSize;
console.log("File size: " + fileSize);
let streamRequest = client.startBulkRequest({
actor: actor,
type: "stream",
length: fileSize
});
streamRequest.on("bulk-send-ready", ({copyFrom}) => {
NetUtil.asyncFetch({
uri: NetUtil.newURI(packageFile),
loadUsingSystemPrincipal: true
}, function (inputStream) {
let copying = copyFrom(inputStream);
copying.on("progress", (e, progress) => {
progressCallback(progress);
});
copying.then(() => {
console.log("Bulk upload done");
inputStream.close();
deferred.resolve(actor);
});
});
});
}
return deferred.promise;
}
function removeServerTemporaryFile(client, fileActor) {
let request = {
to: fileActor,
type: "remove"
};
client.request(request);
}
/**
* progressCallback argument:
* Function called as packaged app installation proceeds.
* The progress object passed to this function contains:
* * bytesSent: The number of bytes sent so far
* * totalBytes: The total number of bytes to send
*/
function installPackaged(client, webappsActor, packagePath, appId, progressCallback) {
let deferred = defer();
let file = FileUtils.File(packagePath);
let packagePromise;
if (file.isDirectory()) {
let tmpZipFile = FileUtils.getDir("TmpD", [], true);
tmpZipFile.append("application.zip");
tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
packagePromise = zipDirectory(tmpZipFile, file);
} else {
packagePromise = promise.resolve(file);
}
packagePromise.then((zipFile) => {
uploadPackage(client, webappsActor, zipFile, progressCallback).then((fileActor) => {
let request = {
to: webappsActor,
type: "install",
appId: appId,
upload: fileActor
};
client.request(request, (res) => {
// If the install method immediatly fails,
// reject immediatly the installPackaged promise.
// Otherwise, wait for webappsEvent for completion
if (res.error) {
deferred.reject(res);
}
if ("error" in res) {
deferred.reject({error: res.error, message: res.message});
} else {
deferred.resolve({appId: res.appId});
}
});
// Ensure deleting the temporary package file, but only if that a temporary
// package created when we pass a directory as `packagePath`
if (zipFile != file) {
zipFile.remove(false);
}
// In case of success or error, ensure deleting the temporary package file
// also created on the device, but only once install request is done
deferred.promise.then(
() => removeServerTemporaryFile(client, fileActor),
() => removeServerTemporaryFile(client, fileActor));
});
});
return deferred.promise;
}
exports.installPackaged = installPackaged;
function installHosted(client, webappsActor, appId, metadata, manifest) {
let deferred = defer();
let request = {
to: webappsActor,
type: "install",
appId: appId,
metadata: metadata,
manifest: manifest
};
client.request(request, (res) => {
if (res.error) {
deferred.reject(res);
}
if ("error" in res) {
deferred.reject({error: res.error, message: res.message});
} else {
deferred.resolve({appId: res.appId});
}
});
return deferred.promise;
}
exports.installHosted = installHosted;
function getTargetForApp(client, webappsActor, manifestURL) {
// Ensure always returning the exact same JS object for a target
// of the same app in order to show only one toolbox per app and
// avoid re-creating lot of objects twice.
let existingTarget = appTargets.get(manifestURL);
if (existingTarget) {
return promise.resolve(existingTarget);
}
let deferred = defer();
let request = {
to: webappsActor,
type: "getAppActor",
manifestURL: manifestURL,
};
client.request(request, (res) => {
if (res.error) {
deferred.reject(res.error);
} else {
let options = {
form: res.actor,
client: client,
chrome: false
};
TargetFactory.forRemoteTab(options).then((target) => {
target.isApp = true;
appTargets.set(manifestURL, target);
target.on("close", () => {
appTargets.delete(manifestURL);
});
deferred.resolve(target);
}, (error) => {
deferred.reject(error);
});
}
});
return deferred.promise;
}
exports.getTargetForApp = getTargetForApp;
function reloadApp(client, webappsActor, manifestURL) {
return getTargetForApp(
client, webappsActor, manifestURL
).then((target) => {
// Request the ContentActor to reload the app
let request = {
to: target.form.actor,
type: "reload",
options: {
force: true
},
manifestURL,
};
return client.request(request);
}, () => {
throw new Error("Not running");
});
}
exports.reloadApp = reloadApp;
function launchApp(client, webappsActor, manifestURL) {
return client.request({
to: webappsActor,
type: "launch",
manifestURL: manifestURL
});
}
exports.launchApp = launchApp;
function closeApp(client, webappsActor, manifestURL) {
return client.request({
to: webappsActor,
type: "close",
manifestURL: manifestURL
});
}
exports.closeApp = closeApp;
function getTarget(client, form) {
let deferred = defer();
let options = {
form: form,
client: client,
chrome: false
};
TargetFactory.forRemoteTab(options).then((target) => {
target.isApp = true;
deferred.resolve(target);
}, (error) => {
deferred.reject(error);
});
return deferred.promise;
}
/**
* `App` instances are client helpers to manage a given app
* and its the tab actors
*/
function App(client, webappsActor, manifest) {
this.client = client;
this.webappsActor = webappsActor;
this.manifest = manifest;
// This attribute is managed by the AppActorFront
this.running = false;
this.iconURL = null;
}
App.prototype = {
getForm: function () {
if (this._form) {
return promise.resolve(this._form);
}
let request = {
to: this.webappsActor,
type: "getAppActor",
manifestURL: this.manifest.manifestURL
};
return this.client.request(request).then(res => {
this._form = res.actor;
return this._form;
});
},
getTarget: function () {
if (this._target) {
return promise.resolve(this._target);
}
return this.getForm().then(
(form) => getTarget(this.client, form)
).then((target) => {
target.on("close", () => {
delete this._form;
delete this._target;
});
this._target = target;
return this._target;
});
},
launch: function () {
return launchApp(this.client, this.webappsActor,
this.manifest.manifestURL);
},
reload: function () {
return reloadApp(this.client, this.webappsActor,
this.manifest.manifestURL);
},
close: function () {
return closeApp(this.client, this.webappsActor,
this.manifest.manifestURL);
},
getIcon: function () {
if (this.iconURL) {
return promise.resolve(this.iconURL);
}
let deferred = defer();
let request = {
to: this.webappsActor,
type: "getIconAsDataURL",
manifestURL: this.manifest.manifestURL
};
this.client.request(request, res => {
if (res.error) {
deferred.reject(res.message || res.error);
} else if (res.url) {
this.iconURL = res.url;
deferred.resolve(res.url);
} else {
deferred.reject("Unable to fetch app icon");
}
});
return deferred.promise;
}
};
/**
* `AppActorFront` is a client for the webapps actor.
*/
function AppActorFront(client, form) {
this.client = client;
this.actor = form.webappsActor;
this._clientListener = this._clientListener.bind(this);
this._onInstallProgress = this._onInstallProgress.bind(this);
this._listeners = [];
EventEmitter.decorate(this);
}
AppActorFront.prototype = {
/**
* List `App` instances for all currently running apps.
*/
get runningApps() {
if (!this._apps) {
throw new Error("Can't get running apps before calling watchApps.");
}
let r = new Map();
for (let [manifestURL, app] of this._apps) {
if (app.running) {
r.set(manifestURL, app);
}
}
return r;
},
/**
* List `App` instances for all installed apps.
*/
get apps() {
if (!this._apps) {
throw new Error("Can't get apps before calling watchApps.");
}
return this._apps;
},
/**
* Returns a `App` object instance for the given manifest URL
* (and cache it per AppActorFront object)
*/
_getApp: function (manifestURL) {
let app = this._apps ? this._apps.get(manifestURL) : null;
if (app) {
return promise.resolve(app);
}
let request = {
to: this.actor,
type: "getApp",
manifestURL,
};
return this.client.request(request).then(res => {
app = new App(this.client, this.actor, res.app);
if (this._apps) {
this._apps.set(manifestURL, app);
}
return app;
}, e => {
console.error("Unable to retrieve app", manifestURL, e);
});
},
/**
* Starts watching for app opening/closing installing/uninstalling.
* Needs to be called before using `apps` or `runningApps` attributes.
*/
watchApps: function (listener) {
// Fixes race between two references to the same front
// calling watchApps at the same time
if (this._loadingPromise) {
return this._loadingPromise;
}
// Only call watchApps for the first listener being register,
// for all next ones, just send fake appOpen events for already
// opened apps
if (this._apps) {
this.runningApps.forEach((app, manifestURL) => {
listener("appOpen", app);
});
return promise.resolve();
}
// First retrieve all installed apps and create
// related `App` object for each
let request = {
to: this.actor,
type: "getAll"
};
this._loadingPromise = this.client.request(request).then(res => {
delete this._loadingPromise;
this._apps = new Map();
for (let a of res.apps) {
let app = new App(this.client, this.actor, a);
this._apps.set(a.manifestURL, app);
}
}).then(() => {
// Then retrieve all running apps in order to flag them as running
let listRequest = {
to: this.actor,
type: "listRunningApps"
};
return this.client.request(listRequest).then(res => res.apps);
}).then(apps => {
let promises = apps.map(manifestURL => {
// _getApp creates `App` instance and register it to AppActorFront
return this._getApp(manifestURL).then(app => {
app.running = true;
// Fake appOpen event for all already opened
this._notifyListeners("appOpen", app);
});
});
return promise.all(promises);
}).then(() => {
// Finally ask to receive all app events
return this._listenAppEvents(listener);
});
return this._loadingPromise;
},
fetchIcons: function () {
// On demand, retrieve apps icons in order to be able
// to synchronously retrieve it on `App` objects
let promises = [];
for (let [, app] of this._apps) {
promises.push(app.getIcon());
}
return DevToolsUtils.settleAll(promises)
.catch(() => {});
},
_listenAppEvents: function (listener) {
this._listeners.push(listener);
if (this._listeners.length > 1) {
return promise.resolve();
}
let client = this.client;
let f = this._clientListener;
client.addListener("appOpen", f);
client.addListener("appClose", f);
client.addListener("appInstall", f);
client.addListener("appUninstall", f);
let request = {
to: this.actor,
type: "watchApps"
};
return this.client.request(request);
},
_unlistenAppEvents: function (listener) {
let idx = this._listeners.indexOf(listener);
if (idx != -1) {
this._listeners.splice(idx, 1);
}
// Until we released all listener, we don't ask to stop sending events
if (this._listeners.length != 0) {
return promise.resolve();
}
let client = this.client;
let f = this._clientListener;
client.removeListener("appOpen", f);
client.removeListener("appClose", f);
client.removeListener("appInstall", f);
client.removeListener("appUninstall", f);
// Remove `_apps` in order to allow calling watchApps again
// and repopulate the apps Map.
delete this._apps;
let request = {
to: this.actor,
type: "unwatchApps"
};
return this.client.request(request);
},
_clientListener: function (type, message) {
let { manifestURL } = message;
// Reset the app object to get a fresh copy when we (re)install the app.
if (type == "appInstall" && this._apps && this._apps.has(manifestURL)) {
this._apps.delete(manifestURL);
}
this._getApp(manifestURL).then((app) => {
switch (type) {
case "appOpen":
app.running = true;
this._notifyListeners("appOpen", app);
break;
case "appClose":
app.running = false;
this._notifyListeners("appClose", app);
break;
case "appInstall":
// The call to _getApp is going to create App object
// This app may have been running while being installed, so check the list
// of running apps again to get the right answer.
let request = {
to: this.actor,
type: "listRunningApps"
};
this.client.request(request).then(res => {
if (res.apps.includes(manifestURL)) {
app.running = true;
this._notifyListeners("appInstall", app);
this._notifyListeners("appOpen", app);
} else {
this._notifyListeners("appInstall", app);
}
});
break;
case "appUninstall":
// Fake a appClose event if we didn't got one before uninstall
if (app.running) {
app.running = false;
this._notifyListeners("appClose", app);
}
this._apps.delete(manifestURL);
this._notifyListeners("appUninstall", app);
break;
}
});
},
_notifyListeners: function (type, app) {
this._listeners.forEach(f => {
f(type, app);
});
},
unwatchApps: function (listener) {
return this._unlistenAppEvents(listener);
},
/*
* Install a packaged app.
*
* Events are going to be emitted on the front
* as install progresses. Events will have the following fields:
* * bytesSent: The number of bytes sent so far
* * totalBytes: The total number of bytes to send
*/
installPackaged: function (packagePath, appId) {
let request = () => {
return installPackaged(this.client, this.actor, packagePath, appId,
this._onInstallProgress)
.then(response => ({
appId: response.appId,
manifestURL: "app://" + response.appId + "/manifest.webapp"
}));
};
return this._install(request);
},
_onInstallProgress: function (progress) {
this.emit("install-progress", progress);
},
_install: function (request) {
let deferred = defer();
let finalAppId = null, manifestURL = null;
let installs = {};
// We need to resolve only once the request is done *AND*
// once we receive the related appInstall message for
// the same manifestURL
let resolve = app => {
this._unlistenAppEvents(listener);
installs = null;
deferred.resolve({ app: app, appId: finalAppId });
};
// Listen for appInstall event, in order to resolve with
// the matching app object.
let listener = (type, app) => {
if (type == "appInstall") {
// Resolves immediately if the request has already resolved
// or just flag the installed app to eventually resolve
// when the request gets its response.
if (app.manifest.manifestURL === manifestURL) {
resolve(app);
} else {
installs[app.manifest.manifestURL] = app;
}
}
};
// Execute the request
this._listenAppEvents(listener).then(request).then(response => {
finalAppId = response.appId;
manifestURL = response.manifestURL;
// Resolves immediately if the appInstall event
// was dispatched during the request.
if (manifestURL in installs) {
resolve(installs[manifestURL]);
}
}, deferred.reject);
return deferred.promise;
},
/*
* Install a hosted app.
*
* Events are going to be emitted on the front
* as install progresses. Events will have the following fields:
* * bytesSent: The number of bytes sent so far
* * totalBytes: The total number of bytes to send
*/
installHosted: function (appId, metadata, manifest) {
let manifestURL = metadata.manifestURL ||
metadata.origin + "/manifest.webapp";
let request = () => {
return installHosted(
this.client, this.actor, appId, metadata, manifest
).then(response => ({
appId: response.appId,
manifestURL: manifestURL
}));
};
return this._install(request);
}
};
exports.AppActorFront = AppActorFront;

View File

@ -4,5 +4,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'app-actor-front.js',
'Devices.jsm'
)