Bug 1168872 - Support for server side logging; r=panos, ochameau

This commit is contained in:
Jan Odvarko 2015-08-17 12:48:01 +02:00
Родитель a627185642
Коммит 4cf5098ba9
15 изменённых файлов: 1038 добавлений и 27 удалений

Просмотреть файл

@ -1562,6 +1562,10 @@ pref("devtools.webconsole.filter.secwarn", true);
pref("devtools.webconsole.filter.serviceworkers", false);
pref("devtools.webconsole.filter.sharedworkers", false);
pref("devtools.webconsole.filter.windowlessworkers", false);
pref("devtools.webconsole.filter.servererror", false);
pref("devtools.webconsole.filter.serverwarn", false);
pref("devtools.webconsole.filter.serverinfo", false);
pref("devtools.webconsole.filter.serverlog", false);
// Remember the Browser Console filters
pref("devtools.browserconsole.filter.network", true);
@ -1583,6 +1587,10 @@ pref("devtools.browserconsole.filter.secwarn", true);
pref("devtools.browserconsole.filter.serviceworkers", true);
pref("devtools.browserconsole.filter.sharedworkers", true);
pref("devtools.browserconsole.filter.windowlessworkers", true);
pref("devtools.browserconsole.filter.servererror", false);
pref("devtools.browserconsole.filter.serverwarn", false);
pref("devtools.browserconsole.filter.serverinfo", false);
pref("devtools.browserconsole.filter.serverlog", false);
// Text size in the Web Console. Use 0 for the system default size.
pref("devtools.webconsole.fontSize", 0);

Просмотреть файл

@ -44,6 +44,7 @@ const COMPAT = {
INPUT: 4,
OUTPUT: 5,
SECURITY: 6,
SERVER: 7,
},
// The possible message severities.
@ -68,11 +69,12 @@ const COMPAT = {
[ null, null, null, null, ], // Input
[ null, null, null, null, ], // Output
[ "secerror", "secwarn", null, null, ], // Security
[ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging
],
// The fragment of a CSS class name that identifies each category.
CATEGORY_CLASS_FRAGMENTS: [ "network", "cssparser", "exception", "console",
"input", "output", "security" ],
"input", "output", "security", "server" ],
// The fragment of a CSS class name that identifies each severity.
SEVERITY_CLASS_FRAGMENTS: [ "error", "warn", "info", "log" ],
@ -1300,7 +1302,7 @@ Messages.ConsoleGeneric = function(packet)
let options = {
className: "cm-s-mozilla",
timestamp: packet.timeStamp,
category: "webdev",
category: packet.category || "webdev",
severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
prefix: packet.prefix,
private: packet.private,
@ -1571,7 +1573,7 @@ Messages.ConsoleTrace = function(packet)
let options = {
className: "cm-s-mozilla",
timestamp: packet.timeStamp,
category: "webdev",
category: packet.category || "webdev",
severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
private: packet.private,
filterDuplicates: true,
@ -1708,7 +1710,7 @@ Messages.ConsoleTable = function(packet)
let options = {
className: "cm-s-mozilla",
timestamp: packet.timeStamp,
category: "webdev",
category: packet.category || "webdev",
severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
private: packet.private,
filterDuplicates: false,

Просмотреть файл

@ -73,6 +73,7 @@ support-files =
test-console-count-external-file.js
test-console-extras.html
test-console-replaced-api.html
test-console-server-logging.sjs
test-console.html
test-console-workers.html
test-console-table.html
@ -172,6 +173,7 @@ skip-if = buildapp == 'mulet'
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_console_private_browsing.js]
skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests
[browser_console_server_logging.js]
[browser_console_variables_view.js]
skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_console_variables_view_filter.js]

Просмотреть файл

@ -0,0 +1,34 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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";
// Check that server log appears in the console panel - bug 1168872
let test = asyncTest(function* () {
const PREF = "devtools.webconsole.filter.serverlog";
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-server-logging.sjs";
Services.prefs.setBoolPref(PREF, true);
registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
yield loadTab(TEST_URI);
let hud = yield openConsole();
BrowserReload();
// Note that the test is also checking out the (printf like)
// formatters and encoding of UTF8 characters (see the one at the end).
let text = "values: string Object { a: 10 } 123 1.12 \u2713";
yield waitForMessages({
webconsole: hud,
messages: [{
text: text,
category: CATEGORY_SERVER,
severity: SEVERITY_LOG,
}],
})
});

Просмотреть файл

@ -27,12 +27,14 @@ function testFilterButtons() {
testMenuFilterButton("js");
testMenuFilterButton("logging");
testMenuFilterButton("security");
testMenuFilterButton("server");
testIsolateFilterButton("net");
testIsolateFilterButton("css");
testIsolateFilterButton("js");
testIsolateFilterButton("logging");
testIsolateFilterButton("security");
testIsolateFilterButton("server");
}
function testMenuFilterButton(category) {

Просмотреть файл

@ -28,6 +28,7 @@ const CATEGORY_WEBDEV = 3;
const CATEGORY_INPUT = 4;
const CATEGORY_OUTPUT = 5;
const CATEGORY_SECURITY = 6;
const CATEGORY_SERVER = 7;
// The possible message severities.
const SEVERITY_ERROR = 0;

Просмотреть файл

@ -0,0 +1,32 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
function handleRequest(request, response)
{
var page = "<!DOCTYPE html><html>" +
"<head><meta charset='utf-8'></head>" +
"<body><p>hello world!</p></body>" +
"</html>";
var data = {
"version": "4.1.0",
"columns": ["log", "backtrace", "type"],
"rows": [[
["values: %s %o %i %f %s","string",{"a":10,"___class_name":"Object"},123,1.12, "\u2713"],
"C:\\src\\www\\serverlogging\\test7.php:4:1",
""
]]
};
// Put log into headers.
var value = b64EncodeUnicode(JSON.stringify(data));
response.setHeader("X-ChromeLogger-Data", value, false);
response.write(page);
}
function b64EncodeUnicode(str) {
return btoa(unescape(encodeURIComponent(str)));
}

Просмотреть файл

@ -34,6 +34,7 @@ loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/Variabl
loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
loader.lazyGetter(this, "Timers", () => require("sdk/timers"));
const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
@ -76,6 +77,7 @@ const CATEGORY_WEBDEV = 3;
const CATEGORY_INPUT = 4; // always on
const CATEGORY_OUTPUT = 5; // always on
const CATEGORY_SECURITY = 6;
const CATEGORY_SERVER = 7;
// The possible message severities. As before, we start at zero so we can use
// these as indexes into MESSAGE_PREFERENCE_KEYS.
@ -93,6 +95,7 @@ const CATEGORY_CLASS_FRAGMENTS = [
"input",
"output",
"security",
"server",
];
// The fragment of a CSS class name that identifies each severity.
@ -109,14 +112,15 @@ const SEVERITY_CLASS_FRAGMENTS = [
// Most of these rather idiosyncratic names are historical and predate the
// division of message type into "category" and "severity".
const MESSAGE_PREFERENCE_KEYS = [
// Error Warning Info Log
[ "network", "netwarn", "netxhr", "networkinfo", ], // Network
[ "csserror", "cssparser", null, "csslog", ], // CSS
[ "exception", "jswarn", null, "jslog", ], // JS
[ "error", "warn", "info", "log", ], // Web Developer
[ null, null, null, null, ], // Input
[ null, null, null, null, ], // Output
[ "secerror", "secwarn", null, null, ], // Security
// Error Warning Info Log
[ "network", "netwarn", "netxhr", "networkinfo", ], // Network
[ "csserror", "cssparser", null, "csslog", ], // CSS
[ "exception", "jswarn", null, "jslog", ], // JS
[ "error", "warn", "info", "log", ], // Web Developer
[ null, null, null, null, ], // Input
[ null, null, null, null, ], // Output
[ "secerror", "secwarn", null, null, ], // Security
[ "servererror", "serverwarn", "serverinfo", "serverlog", ], // Server Logging
];
// A mapping from the console API log event levels to the Web Console
@ -217,6 +221,7 @@ function WebConsoleFrame(aWebConsoleOwner)
this._onPanelSelected = this._onPanelSelected.bind(this);
this._flushMessageQueue = this._flushMessageQueue.bind(this);
this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
this._onUpdateListeners = this._onUpdateListeners.bind(this);
this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._outputTimerInitialized = false;
@ -652,10 +657,12 @@ WebConsoleFrame.prototype = {
let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
"exception", "jswarn", "jslog", "error", "info", "warn", "log",
"secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
"serviceworkers", "windowlessworkers"];
"serviceworkers", "windowlessworkers", "servererror",
"serverwarn", "serverinfo", "serverlog"];
for (let pref of prefs) {
this.filterPrefs[pref] = Services.prefs
.getBoolPref(this._filterPrefsPrefix + pref);
this.filterPrefs[pref] = Services.prefs.getBoolPref(
this._filterPrefsPrefix + pref);
}
},
@ -666,7 +673,6 @@ WebConsoleFrame.prototype = {
* @param function [aCallback=null]
* Optional function to invoke when the listener has been
* added/removed.
*
*/
_updateReflowActivityListener:
function WCF__updateReflowActivityListener(aCallback)
@ -681,6 +687,38 @@ WebConsoleFrame.prototype = {
}
},
/**
* Attach / detach server logging listener depending on the filter
* preferences. If the user isn't interested in the server logs at
* all the listener is not registered.
*
* @param function [aCallback=null]
* Optional function to invoke when the listener has been
* added/removed.
*/
_updateServerLoggingListener:
function WCF__updateServerLoggingListener(aCallback)
{
if (!this.webConsoleClient) {
return;
}
let startListener = false;
let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"];
for (let i = 0; i < prefs.length; i++) {
if (this.filterPrefs[prefs[i]]) {
startListener = true;
break;
}
}
if (startListener) {
this.webConsoleClient.startListeners(["ServerLogging"], aCallback);
} else {
this.webConsoleClient.stopListeners(["ServerLogging"], aCallback);
}
},
/**
* Sets the events for the filter input field.
* @private
@ -753,6 +791,9 @@ WebConsoleFrame.prototype = {
let logging = this.document.querySelector("toolbarbutton[category=logging]");
logging.removeAttribute("accesskey");
let serverLogging = this.document.querySelector("toolbarbutton[category=server]");
serverLogging.removeAttribute("accesskey");
}
},
@ -970,8 +1011,15 @@ WebConsoleFrame.prototype = {
{
this.filterPrefs[aToggleType] = aState;
this.adjustVisibilityForMessageType(aToggleType, aState);
Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
this._updateReflowActivityListener();
if (this._updateListenersTimeout) {
Timers.clearTimeout(this._updateListenersTimeout);
}
this._updateListenersTimeout = Timers.setTimeout(
this._onUpdateListeners, 200);
},
/**
@ -985,6 +1033,15 @@ WebConsoleFrame.prototype = {
return this.filterPrefs[aToggleType];
},
/**
* Called when a logging filter changes. Allows to stop/start
* listeners according to the current filter state.
*/
_onUpdateListeners: function() {
this._updateReflowActivityListener();
this._updateServerLoggingListener();
},
/**
* Check that the passed string matches the filter arguments.
*
@ -4982,6 +5039,7 @@ function WebConsoleConnectionProxy(aWebConsole, aTarget)
this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
this._onFileActivity = this._onFileActivity.bind(this);
this._onReflowActivity = this._onReflowActivity.bind(this);
this._onServerLogCall = this._onServerLogCall.bind(this);
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onAttachConsole = this._onAttachConsole.bind(this);
this._onCachedMessages = this._onCachedMessages.bind(this);
@ -5087,6 +5145,7 @@ WebConsoleConnectionProxy.prototype = {
client.addListener("consoleAPICall", this._onConsoleAPICall);
client.addListener("fileActivity", this._onFileActivity);
client.addListener("reflowActivity", this._onReflowActivity);
client.addListener("serverLogCall", this._onServerLogCall);
client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
this.target.on("will-navigate", this._onTabNavigated);
this.target.on("navigate", this._onTabNavigated);
@ -5155,7 +5214,7 @@ WebConsoleConnectionProxy.prototype = {
let msgs = ["PageError", "ConsoleAPI"];
this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
this.owner._updateReflowActivityListener();
this.owner._onUpdateListeners();
},
/**
@ -5304,6 +5363,23 @@ WebConsoleConnectionProxy.prototype = {
}
},
/**
* The "serverLogCall" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string aType
* Message type.
* @param object aPacket
* The message received from the server.
*/
_onServerLogCall: function WCCP__onServerLogCall(aType, aPacket)
{
if (this.owner && aPacket.from == this._consoleActor) {
this.owner.handleConsoleAPICall(aPacket.message);
}
},
/**
* The "lastPrivateContextExited" message type handler. When this message is
* received the Web Console UI is cleared.
@ -5378,6 +5454,7 @@ WebConsoleConnectionProxy.prototype = {
this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
this.client.removeListener("fileActivity", this._onFileActivity);
this.client.removeListener("reflowActivity", this._onReflowActivity);
this.client.removeListener("serverLogCall", this._onServerLogCall);
this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
@ -5474,6 +5551,9 @@ ConsoleContextMenu.prototype = {
case CATEGORY_WEBDEV:
selection.add("webdev");
break;
case CATEGORY_SERVER:
selection.add("server");
break;
}
}

Просмотреть файл

@ -172,11 +172,27 @@ function goUpdateConsoleCommands() {
autocheck="false" prefKey="windowlessworkers"/>
</menupopup>
</toolbarbutton>
<toolbarbutton label="&btnServerLogging.label;" type="menu-button"
category="server" class="devtools-toolbarbutton webconsole-filter-button"
tooltiptext="&btnServerLogging.tooltip;"
accesskey="&btnServerLogging.accesskey;"
tabindex="8">
<menupopup id="server-logging-contextmenu">
<menuitem label="&btnServerErrors;" type="checkbox"
autocheck="false" prefKey="servererror"/>
<menuitem label="&btnServerWarnings;" type="checkbox"
autocheck="false" prefKey="serverwarn"/>
<menuitem label="&btnServerInfo;" type="checkbox" autocheck="false"
prefKey="serverinfo"/>
<menuitem label="&btnServerLog;" type="checkbox" autocheck="false"
prefKey="serverlog"/>
</menupopup>
</toolbarbutton>
</hbox>
<toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"
accesskey="&btnClear.accesskey;"
tabindex="8"/>
tabindex="9"/>
<spacer flex="1"/>

Просмотреть файл

@ -76,6 +76,17 @@
<!ENTITY btnConsoleXhr "XHR">
<!ENTITY btnConsoleReflows "Reflows">
<!-- LOCALIZATION NOTE (btnServerLogging): This is used as the text of the
- the toolbar. It shows or hides messages that the web developer inserted on
- the page for debugging purposes, using calls on the HTTP server. -->
<!ENTITY btnServerLogging.label "Server">
<!ENTITY btnServerLogging.tooltip "Log messages received from a web server">
<!ENTITY btnServerLogging.accesskey "S">
<!ENTITY btnServerErrors "Errors">
<!ENTITY btnServerInfo "Info">
<!ENTITY btnServerWarnings "Warnings">
<!ENTITY btnServerLog "Log">
<!-- LOCALIZATION NODE (btnConsoleSharedWorkers) the term "Shared Workers"
- should not be translated. -->
<!ENTITY btnConsoleSharedWorkers "Shared Workers">

Просмотреть файл

@ -319,18 +319,32 @@ a {
}
.message[category=console][severity=error] > .icon::before,
.message[category=output][severity=error] > .icon::before {
.message[category=output][severity=error] > .icon::before,
.message[category=server][severity=error] > .icon::before {
background-position: -12px -36px;
}
.message[category=console][severity=warn] > .icon::before {
.message[category=console][severity=warn] > .icon::before,
.message[category=server][severity=warn] > .icon::before {
background-position: -24px -36px;
}
.message[category=console][severity=info] > .icon::before {
.message[category=console][severity=info] > .icon::before,
.message[category=server][severity=info] > .icon::before {
background-position: -36px -36px;
}
/* Server Logging Styles */
.webconsole-filter-button[category="server"] > .toolbarbutton-menubutton-button:before {
background-image: linear-gradient(rgb(144, 176, 144), rgb(99, 151, 99));
border-color: rgb(76, 143, 76);
}
.message[category=server] > .indent {
-moz-border-end: solid #90B090 6px;
}
/* Input and output styles */
.message[category=input] > .indent,
.message[category=output] > .indent {

Просмотреть файл

@ -31,6 +31,10 @@ XPCOMUtils.defineLazyGetter(this, "ConsoleProgressListener", () => {
XPCOMUtils.defineLazyGetter(this, "events", () => {
return require("sdk/event/core");
});
XPCOMUtils.defineLazyGetter(this, "ServerLoggingListener", () => {
return require("devtools/toolkit/webconsole/server-logger")
.ServerLoggingListener;
});
for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
"ConsoleAPIListener", "addWebConsoleCommands", "JSPropertyProvider",
@ -354,14 +358,22 @@ WebConsoleActor.prototype =
this.consoleReflowListener.destroy();
this.consoleReflowListener = null;
}
events.off(this.parentActor, "changed-toplevel-document", this._onChangedToplevelDocument);
if (this.serverLoggingListener) {
this.serverLoggingListener.destroy();
this.serverLoggingListener = null;
}
events.off(this.parentActor, "changed-toplevel-document",
this._onChangedToplevelDocument);
this.conn.removeActorPool(this._actorPool);
if (this.parentActor.isRootActor) {
Services.obs.removeObserver(this._onObserverNotification,
"last-pb-context-exited");
}
this._actorPool = null;
this._actorPool = null;
this._webConsoleCommandsCache = null;
this._lastConsoleInputEvaluation = null;
this._evalWindow = null;
@ -604,6 +616,13 @@ WebConsoleActor.prototype =
}
startedListeners.push(listener);
break;
case "ServerLogging":
if (!this.serverLoggingListener) {
this.serverLoggingListener =
new ServerLoggingListener(this.window, this);
}
startedListeners.push(listener);
break;
}
}
@ -634,7 +653,7 @@ WebConsoleActor.prototype =
// listeners.
let toDetach = aRequest.listeners ||
["PageError", "ConsoleAPI", "NetworkActivity",
"FileActivity"];
"FileActivity", "ServerLogging"];
while (toDetach.length > 0) {
let listener = toDetach.shift();
@ -675,6 +694,13 @@ WebConsoleActor.prototype =
}
stoppedListeners.push(listener);
break;
case "ServerLogging":
if (this.serverLoggingListener) {
this.serverLoggingListener.destroy();
this.serverLoggingListener = null;
}
stoppedListeners.push(listener);
break;
}
}
@ -1427,6 +1453,40 @@ WebConsoleActor.prototype =
this.conn.send(packet);
},
/**
* Handler for server logging. This method forwards log events to the
* remote Web Console client.
*
* @see ServerLoggingListener
* @param object aMessage
* The console API call on the server we need to send to the remote client.
*/
onServerLogCall: function WCA_onServerLogCall(aMessage)
{
// Clone all data into the content scope (that's where
// passed arguments comes from).
let msg = Cu.cloneInto(aMessage, this.window);
// All arguments within the message need to be converted into
// debuggees to properly send it to the client side.
// Use the default target: this.window as the global object
// since that's the correct scope for data in the message.
// The 'false' argument passed into prepareConsoleMessageForRemote()
// ensures that makeDebuggeeValue uses content debuggee.
// See also:
// * makeDebuggeeValue()
// * prepareConsoleMessageForRemote()
msg = this.prepareConsoleMessageForRemote(msg, false);
let packet = {
from: this.actorID,
type: "serverLogCall",
message: msg,
};
this.conn.send(packet);
},
//////////////////
// End of event handlers for various listeners.
//////////////////
@ -1437,11 +1497,14 @@ WebConsoleActor.prototype =
*
* @param object aMessage
* The original message received from console-api-log-event.
* @param boolean aUseObjectGlobal
* If |true| the object global is determined and added as a debuggee,
* otherwise |this.window| is used when makeDebuggeeValue() is invoked.
* @return object
* The object that can be sent to the remote client.
*/
prepareConsoleMessageForRemote:
function WCA_prepareConsoleMessageForRemote(aMessage)
function WCA_prepareConsoleMessageForRemote(aMessage, aUseObjectGlobal = true)
{
let result = WebConsoleUtils.cloneObject(aMessage);
@ -1454,7 +1517,7 @@ WebConsoleActor.prototype =
delete result.consoleID;
result.arguments = Array.map(aMessage.arguments || [], (aObj) => {
let dbgObj = this.makeDebuggeeValue(aObj, true);
let dbgObj = this.makeDebuggeeValue(aObj, aUseObjectGlobal);
return this.createValueGrip(dbgObj);
});
@ -1462,6 +1525,8 @@ WebConsoleActor.prototype =
return this.createValueGrip(aString);
});
result.category = aMessage.category || "webdev";
return result;
},

Просмотреть файл

@ -12,5 +12,7 @@ EXTRA_JS_MODULES.devtools.toolkit.webconsole += [
'client.js',
'network-helper.js',
'network-monitor.js',
'server-logger-monitor.js',
'server-logger.js',
'utils.js',
]

Просмотреть файл

@ -0,0 +1,211 @@
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Ci} = require("chrome");
const Services = require("Services");
const {DebuggerServer} = require("devtools/server/main");
const {makeInfallible} = require("devtools/toolkit/DevToolsUtils");
loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
// Helper tracer. Should be generic sharable by other modules (bug 1171927)
const trace = {
log: function(...args) {
}
}
const acceptableHeaders = ["x-chromelogger-data"];
/**
* This object represents HTTP events observer. It's intended to be
* used in e10s enabled browser only.
*
* Since child processes can't register HTTP event observer they use
* this module to do the observing in the parent process. This monitor
* is loaded through DebuggerServerConnection.setupInParent() that is executed
* from within the child process. The execution is done by {@ServerLoggingListener}.
* The monitor listens to HTTP events and forwards it into the right child process.
*
* Read more about the architecture:
* https://github.com/mozilla/gecko-dev/blob/fx-team/toolkit/devtools/server/docs/actor-e10s-handling.md
*/
var ServerLoggerMonitor = {
// Initialization
initialize: function() {
this.onChildMessage = this.onChildMessage.bind(this);
this.onDisconnectChild = this.onDisconnectChild.bind(this);
this.onExamineResponse = this.onExamineResponse.bind(this);
// Set of tracked message managers.
this.messageManagers = new Set();
// Set of registered child frames (loggers).
this.targets = new Set();
},
// Parent Child Relationship
attach: makeInfallible(function({mm, prefix}) {
let size = this.messageManagers.size;
trace.log("ServerLoggerMonitor.attach; ", size, arguments);
if (this.messageManagers.has(mm)) {
return;
}
this.messageManagers.add(mm);
// Start listening for messages from the {@ServerLogger} actor
// living in the child process.
mm.addMessageListener("debug:server-logger", this.onChildMessage);
// Listen to the disconnection message to clean-up.
DebuggerServer.once("disconnected-from-child:" + prefix,
this.onDisconnectChild);
}),
detach: function(mm) {
let size = this.messageManagers.size;
trace.log("ServerLoggerMonitor.detach; ", size);
// Unregister message listeners
mm.removeMessageListener("debug:server-logger", this.onChildMessage);
},
onDisconnectChild: function(event, mm) {
let size = this.messageManagers.size;
trace.log("ServerLoggerMonitor.onDisconnectChild; ",
size, arguments);
if (!this.messageManagers.has(mm)) {
return;
}
this.detach(mm);
this.messageManagers.delete(mm);
},
// Child Message Handling
onChildMessage: function(msg) {
let method = msg.data.method;
trace.log("ServerLoggerMonitor.onChildMessage; ", method, msg);
switch (method) {
case "attachChild":
return this.onAttachChild(msg);
case "detachChild":
return this.onDetachChild(msg);
default:
trace.log("Unknown method name: ", method);
}
},
onAttachChild: function(event) {
let target = event.target;
let size = this.targets.size;
trace.log("ServerLoggerMonitor.onAttachChild; size: ", size, target);
// If this is the first child attached, register global HTTP observer.
if (!size) {
trace.log("ServerLoggerMonitor.onAttatchChild; Add HTTP Observer");
Services.obs.addObserver(this.onExamineResponse,
"http-on-examine-response", false);
}
// Collect child loggers. The frame element where the
// window/document lives.
this.targets.add(target);
},
onDetachChild: function(event) {
let target = event.target;
this.targets.delete(target);
let size = this.targets.size;
trace.log("ServerLoggerMonitor.onDetachChild; size: ", size, target);
// If this is the last child process attached, unregister
// the global HTTP observer.
if (!size) {
trace.log("ServerLoggerMonitor.onDetachChild; Remove HTTP Observer");
Services.obs.removeObserver(this.onExamineResponse,
"http-on-examine-response", false);
}
},
// HTTP Observer
onExamineResponse: makeInfallible(function(subject, topic) {
let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
trace.log("ServerLoggerMonitor.onExamineResponse; ", httpChannel.name,
this.targets);
// Ignore requests from chrome or add-on code when we are monitoring
// content.
if (!httpChannel.loadInfo &&
httpChannel.loadInfo.loadingDocument === null &&
httpChannel.loadInfo.loadingPrincipal === Services.scriptSecurityManager.getSystemPrincipal()) {
return;
}
let requestFrame = NetworkHelper.getTopFrameForRequest(httpChannel);
if (!requestFrame) {
return;
}
// Ignore requests from parent frames that aren't registered.
if (!this.targets.has(requestFrame)) {
return;
}
let headers = [];
httpChannel.visitResponseHeaders((header, value) => {
header = header.toLowerCase();
if (acceptableHeaders.indexOf(header) !== -1) {
headers.push({header: header, value: value});
}
});
if (!headers.length) {
return;
}
let { messageManager } = requestFrame;
messageManager.sendAsyncMessage("debug:server-logger", {
method: "examineHeaders",
headers: headers,
});
trace.log("ServerLoggerMonitor.onExamineResponse; headers ",
headers.length, ", ", headers);
}),
};
/**
* Executed automatically by the framework.
*/
function setupParentProcess(event) {
ServerLoggerMonitor.attach(event);
}
// Monitor initialization.
ServerLoggerMonitor.initialize();
// Exports from this module
exports.setupParentProcess = setupParentProcess;

Просмотреть файл

@ -0,0 +1,531 @@
/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cu, Ci} = require("chrome");
const {Class} = require("sdk/core/heritage");
const Services = require("Services");
const {DebuggerServer} = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
Cu.importGlobalProperties(["atob"]);
loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
// Helper tracer. Should be generic sharable by other modules (bug 1171927)
const trace = {
log: function(...args) {
}
}
// Constants
const makeInfallible = DevToolsUtils.makeInfallible;
const acceptableHeaders = ["x-chromelogger-data"];
/**
* The listener is responsible for detecting server side logs
* within HTTP headers and sending them to the client.
*
* The logic is based on "http-on-examine-response" event that is
* sent when a response from the server is received. Consequently HTTP
* headers are parsed to find server side logs.
*
* A listeners for "http-on-examine-response" is registered when
* the listener starts and removed when destroy is executed.
*/
var ServerLoggingListener = Class({
/**
* Initialization of the listener. The main step during the initialization
* process is registering a listener for "http-on-examine-response" event.
*
* @param {Object} win (nsIDOMWindow):
* filter network requests by the associated window object.
* If null (i.e. in the browser context) log everything
* @param {Object} owner
* The {@WebConsoleActor} instance
*/
initialize: function(win, owner) {
trace.log("ServerLoggingListener.initialize; ", owner.actorID,
", child process: ", DebuggerServer.isInChildProcess);
this.owner = owner;
this.window = win;
this.onExamineResponse = this.onExamineResponse.bind(this);
this.onExamineHeaders = this.onExamineHeaders.bind(this);
this.onParentMessage = this.onParentMessage.bind(this);
this.attach();
},
/**
* The destroy is called by the parent WebConsoleActor actor.
*/
destroy: function() {
trace.log("ServerLoggingListener.destroy; ", this.owner.actorID,
", child process: ", DebuggerServer.isInChildProcess);
this.detach();
},
/**
* The main responsibility of this method is registering a listener for
* "http-on-examine-response" events.
*/
attach: makeInfallible(function() {
trace.log("ServerLoggingListener.attach; child process: ",
DebuggerServer.isInChildProcess);
// Setup the child <-> parent communication if this actor module
// is running in a child process. If e10s is disabled (this actor
// running in the same process as everything else) register observer
// listener just like in good old pre e10s days.
if (DebuggerServer.isInChildProcess) {
this.attachParentProcess();
} else {
Services.obs.addObserver(this.onExamineResponse,
"http-on-examine-response", false);
}
}),
/**
* Remove the "http-on-examine-response" listener.
*/
detach: makeInfallible(function() {
trace.log("ServerLoggingListener.detach; ", this.owner.actorID);
if (DebuggerServer.isInChildProcess) {
this.detachParentProcess();
} else {
Services.obs.removeObserver(this.onExamineResponse,
"http-on-examine-response", false);
}
}),
// Parent Child Relationship
attachParentProcess: function() {
trace.log("ServerLoggingListener.attachParentProcess;");
this.owner.conn.setupInParent({
module: "devtools/toolkit/webconsole/server-logger-monitor",
setupParent: "setupParentProcess"
});
let mm = this.owner.conn.parentMessageManager;
let { addMessageListener, sendSyncMessage } = mm;
// It isn't possible to register HTTP-* event observer inside
// a child process (in case of e10s), so listen for messages
// coming from the {@ServerLoggerMonitor} that lives inside
// the parent process.
addMessageListener("debug:server-logger", this.onParentMessage);
// Attach to the {@ServerLoggerMonitor} object to subscribe events.
sendSyncMessage("debug:server-logger", {
method: "attachChild"
});
},
detachParentProcess: makeInfallible(function() {
trace.log("ServerLoggingListener.detachParentProcess;");
let mm = this.owner.conn.parentMessageManager;
let { removeMessageListener, sendSyncMessage } = mm;
sendSyncMessage("debug:server-logger", {
method: "detachChild",
});
removeMessageListener("debug:server-logger", this.onParentMessage);
}),
onParentMessage: makeInfallible(function(msg) {
if (!msg.data) {
return;
}
let method = msg.data.method;
trace.log("ServerLogger.onParentMessage; ", method, msg.data);
switch (method) {
case "examineHeaders":
return this.onExamineHeaders(msg);
default:
trace.log("Unknown method name: ", method);
}
}),
// HTTP Observer
onExamineHeaders: function(event) {
let headers = event.data.headers;
trace.log("ServerLoggingListener.onExamineHeaders;", headers);
let parsedMessages = [];
for (let item of headers) {
let header = item.header;
let value = item.value;
let messages = this.parse(header, value);
if (messages) {
parsedMessages.push(...messages);
}
}
if (!parsedMessages.length) {
return;
}
for (let message of parsedMessages) {
this.sendMessage(message);
}
},
onExamineResponse: makeInfallible(function(subject) {
let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
trace.log("ServerLoggingListener.onExamineResponse; ", httpChannel.name,
", ", this.owner.actorID, httpChannel);
if (!this._matchRequest(httpChannel)) {
trace.log("ServerLoggerMonitor.onExamineResponse; No matching request!");
return;
}
let headers = [];
httpChannel.visitResponseHeaders((header, value) => {
header = header.toLowerCase();
if (acceptableHeaders.indexOf(header) !== -1) {
headers.push({header: header, value: value});
}
});
this.onExamineHeaders({
data: {
headers: headers,
}
});
}),
/**
* Check if a given network request should be logged by this network monitor
* instance based on the current filters.
*
* @private
* @param nsIHttpChannel aChannel
* Request to check.
* @return boolean
* True if the network request should be logged, false otherwise.
*/
_matchRequest: function(aChannel) {
trace.log("_matchRequest ", this.window, ", ", this.topFrame);
// Log everything if the window is null (it's null in the browser context)
if (!this.window) {
return true;
}
// Ignore requests from chrome or add-on code when we are monitoring
// content.
if (!aChannel.loadInfo &&
aChannel.loadInfo.loadingDocument === null &&
aChannel.loadInfo.loadingPrincipal === Services.scriptSecurityManager.getSystemPrincipal()) {
return false;
}
// Since frames support, this.window may not be the top level content
// frame, so that we can't only compare with win.top.
let win = NetworkHelper.getWindowForRequest(aChannel);
while(win) {
if (win == this.window) {
return true;
}
if (win.parent == win) {
break;
}
win = win.parent;
}
return false;
},
// Server Logs
/**
* Search through HTTP headers to catch all server side logs.
* Learn more about the data structure:
* https://craig.is/writing/chrome-logger/techspecs
*/
parse: function(header, value) {
let data;
try {
let result = decodeURIComponent(escape(atob(value)));
data = JSON.parse(result);
} catch (err) {
Cu.reportError("Failed to parse HTTP log data! " + err);
return;
}
let parsedMessage = [];
let columnMap = this.getColumnMap(data);
trace.log("ServerLoggingListener.parse; ColumnMap", columnMap);
trace.log("ServerLoggingListener.parse; data", data);
let lastLocation;
for (let row of data.rows) {
let backtrace = row[columnMap.get("backtrace")];
let rawLogs = row[columnMap.get("log")];
let type = row[columnMap.get("type")] || "log";
// Old version of the protocol includes a label.
// If this is the old version do some converting.
if (data.columns.indexOf("label") != -1) {
let label = row[columnMap.get("label")];
let showLabel = label && typeof label === "string";
rawLogs = [rawLogs];
if (showLabel) {
rawLogs.unshift(label);
}
}
// If multiple logs come from the same line only the first log
// has info about the backtrace. So, remember the last valid
// location and use it for those that not set.
let location = this.parseBacktrace(backtrace);
if (location) {
lastLocation = location;
} else {
location = lastLocation;
}
parsedMessage.push({
logs: rawLogs,
location: location,
type: type
});
}
return parsedMessage;
},
parseBacktrace: function(backtrace) {
if (!backtrace) {
return null;
}
let result = backtrace.match(/\s*(\d+)$/);
if (!result || result.length < 2) {
return backtrace;
}
return {
url: backtrace.slice(0, -result[0].length),
line: result[1]
};
},
getColumnMap: function(data) {
let columnMap = new Map();
let columnName;
for (let key in data.columns) {
columnName = data.columns[key];
columnMap.set(columnName, key);
}
return columnMap;
},
sendMessage: function(msg) {
trace.log("ServerLoggingListener.sendMessage; message", msg);
let formatted = format(msg);
trace.log("ServerLoggingListener.sendMessage; formatted", formatted);
let win = this.window;
let innerID = win ? getInnerId(win) : null;
let location = msg.location;
let message = {
category: "server",
innerID: innerID,
level: msg.type,
filename: location ? location.url : null,
lineNumber: location ? location.line : null,
columnNumber: 0,
private: false,
timeStamp: Date.now(),
arguments: formatted ? formatted.logs : null,
styles: formatted ? formatted.styles : null,
};
// Make sure to set the group name.
if (msg.type == "group" && formatted && formatted.logs) {
message.groupName = formatted ? formatted.logs[0] : null;
}
// A message for console.table() method (passed in as the first
// argument) isn't supported. But, it's passed in by some server
// side libraries that implement console.* API - let's just remove it.
let args = message.arguments;
if (msg.type == "table" && args) {
if (typeof args[0] == "string") {
args.shift();
}
}
trace.log("ServerLoggingListener.sendMessage; raw: ",
msg.logs.join(", "), message);
this.owner.onServerLogCall(message);
},
});
// Helpers
/**
* Parse printf-like specifiers ("%f", "%d", ...) and
* format the logs according to them.
*/
function format(msg) {
if (!msg.logs || !msg.logs[0]) {
return;
}
// Initialize the styles array (used for the "%c" specifier).
msg.styles = [];
// Remove and get the first log (in which the specifiers are).
let firstString = msg.logs.shift();
// Contains all the strings split by the specifiers
// (i.e. "a %f b" => ["a ", " b"]).
let splitLog = [];
// All the specifiers present in the first string.
let specifiers = [];
let specifierIndex = -1;
let splitLogRegExp = /(.*?)(%[oOcsdif]|$)/g;
let splitLogRegExpRes;
// Get the strings before the specifiers (or the last chunk before the end
// of the string).
while ((splitLogRegExpRes = splitLogRegExp.exec(firstString)) !== null) {
let [_, log, specifier] = splitLogRegExpRes;
// We can add an empty string if there is a specifier after (which
// means we haven't reached the end of the string). This empty string is
// necessary when rebuilding the logs after the formatting (we should ensure
// to alternate a log + a specifier to replace to make this loop work).
//
// Example: "%ctest" => first iteration: log = "", specifier = "%c".
// => second iteration: log = "test", specifier = "".
if (log || specifier) {
splitLog.push(log);
}
// Break now if there is no specifier anymore
// (means that we have reached the end of the string).
if (!specifier) {
break;
}
specifiers.push(specifier);
}
// This array represents the string of the log, in which the specifiers
// are replaced. It alternates strings and objects (%o;%O).
let rebuiltLogArray = [];
let concatString = "";
let pushConcatString = () => {
if (concatString) {
rebuiltLogArray.push(concatString);
}
concatString = "";
};
// Merge the split first string and the values associated to the specifiers.
splitLog.forEach((string, index) => {
// Concatenate the string in any case.
concatString += string;
if (specifiers.length === 0) {
return;
}
let argument = msg.logs.shift();
switch (specifiers[index]) {
case "%i":
case "%d":
// Parse into integer.
argument |= 0;
concatString += argument;
break;
case "%f":
// Parse into float.
argument =+ argument;
concatString += argument;
break;
case "%o":
case "%O":
// Push the concatenated string and reinitialize concatString.
pushConcatString();
// Push the object.
rebuiltLogArray.push(argument);
break;
case "%s":
concatString += argument;
break;
case "%c":
pushConcatString();
for (let j = msg.styles.length; j < rebuiltLogArray.length; j++) {
msg.styles.push(null);
}
msg.styles.push(argument);
break;
default:
// Should never happen.
return;
}
});
if (concatString) {
rebuiltLogArray.push(concatString);
}
// Append the rest of arguments that don't have corresponding
// specifiers to the message logs.
msg.logs = rebuiltLogArray.concat(msg.logs);
// Remove special ___class_name property that isn't supported
// by the current implementation. This property represents object class
// allowing custom rendering in the console panel.
for (let log of msg.logs) {
if (typeof log == "object") {
delete log.___class_name;
}
}
return msg;
}
// These helper are cloned from SDK to avoid loading to
// much SDK modules just because of two functions.
function getInnerId(win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
};
// Exports from this module
exports.ServerLoggingListener = ServerLoggingListener;