Commit 44d33b48 authored by mmenke@google.com's avatar mmenke@google.com

Adds the ability to load JSON log files to about:net-internals.

Only works with logs created by the new "--log-net-log=file",
which writes NetLog events to the specified file, regardless
of other logging command line parameters.

Using "--log-net-log" without a file name will just write
NetLog events to VLOG(1), as before.

BUG=63687
TEST=None

Review URL: http://codereview.chromium.org/6025017

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@72567 0039d316-1c4b-4281-b951-d872f2087c98
parent a1be9ffd
......@@ -28,6 +28,9 @@
#include "chrome/browser/net/url_fixer_upper.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shell_dialogs.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/tab_contents/tab_contents_view.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_version_info.h"
#include "chrome/common/jstemplate_builder.h"
......@@ -136,6 +139,7 @@ class NetInternalsHTMLSource : public ChromeURLDataManager::DataSource {
// TODO(eroman): Can we start on the IO thread to begin with?
class NetInternalsMessageHandler
: public DOMMessageHandler,
public SelectFileDialog::Listener,
public base::SupportsWeakPtr<NetInternalsMessageHandler> {
public:
NetInternalsMessageHandler();
......@@ -150,12 +154,43 @@ class NetInternalsMessageHandler
void CallJavascriptFunction(const std::wstring& function_name,
const Value* value);
// SelectFileDialog::Listener implementation
virtual void FileSelected(const FilePath& path, int index, void* params);
virtual void FileSelectionCanceled(void* params);
// The only callback handled on the UI thread. As it needs to access fields
// from |dom_ui_|, it can't be called on the IO thread.
void OnLoadLogFile(const ListValue* list);
private:
class IOThreadImpl;
// Task run on the FILE thread to read the contents of a log file. The result
// is then passed to IOThreadImpl's CallJavascriptFunction, which sends it
// back to the web page. IOThreadImpl is used instead of the
// NetInternalsMessageHandler directly because it checks if the message
// handler has been destroyed in the meantime.
class ReadLogFileTask : public Task {
public:
ReadLogFileTask(IOThreadImpl* proxy, const FilePath& path);
virtual void Run();
private:
// IOThreadImpl implements existence checks already. Simpler to reused them
// then to reimplement them.
scoped_refptr<IOThreadImpl> proxy_;
// Path of the file to open.
const FilePath path_;
};
// This is the "real" message handler, which lives on the IO thread.
scoped_refptr<IOThreadImpl> proxy_;
// Used for loading log files.
scoped_refptr<SelectFileDialog> select_log_file_dialog_;
DISALLOW_COPY_AND_ASSIGN(NetInternalsMessageHandler);
};
......@@ -243,17 +278,17 @@ class NetInternalsMessageHandler::IOThreadImpl
int result);
virtual void OnCompletedConnectionTestSuite();
// Helper that executes |function_name| in the attached renderer.
// The function takes ownership of |arg|. Note that this can be called from
// any thread.
void CallJavascriptFunction(const std::wstring& function_name, Value* arg);
private:
class CallbackHelper;
// Helper that runs |method| with |arg|, and deletes |arg| on completion.
void DispatchToMessageHandler(ListValue* arg, MessageHandler method);
// Helper that executes |function_name| in the attached renderer.
// The function takes ownership of |arg|. Note that this can be called from
// any thread.
void CallJavascriptFunction(const std::wstring& function_name, Value* arg);
// Adds |entry| to the queue of pending log entries to be sent to the page via
// Javascript. Must be called on the IO Thread. Also creates a delayed task
// that will call PostPendingEntries, if there isn't one already.
......@@ -398,6 +433,8 @@ NetInternalsMessageHandler::~NetInternalsMessageHandler() {
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
NewRunnableMethod(proxy_.get(), &IOThreadImpl::Detach));
}
if (select_log_file_dialog_)
select_log_file_dialog_->ListenerDestroyed();
}
DOMMessageHandler* NetInternalsMessageHandler::Attach(DOMUI* dom_ui) {
......@@ -408,8 +445,35 @@ DOMMessageHandler* NetInternalsMessageHandler::Attach(DOMUI* dom_ui) {
return result;
}
void NetInternalsMessageHandler::FileSelected(
const FilePath& path, int index, void* params) {
select_log_file_dialog_.release();
BrowserThread::PostTask(
BrowserThread::FILE, FROM_HERE,
new ReadLogFileTask(proxy_.get(), path));
}
void NetInternalsMessageHandler::FileSelectionCanceled(void* params) {
select_log_file_dialog_.release();
}
void NetInternalsMessageHandler::OnLoadLogFile(const ListValue* list) {
// Only allow a single dialog at a time.
if (select_log_file_dialog_.get())
return;
select_log_file_dialog_ = SelectFileDialog::Create(this);
select_log_file_dialog_->SelectFile(
SelectFileDialog::SELECT_OPEN_FILE, string16(), FilePath(), NULL, 0,
FILE_PATH_LITERAL(""),
dom_ui_->tab_contents()->view()->GetTopLevelNativeWindow(), NULL);
}
void NetInternalsMessageHandler::RegisterMessages() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Only callback handled on UI thread.
dom_ui_->RegisterMessageCallback(
"loadLogFile",
NewCallback(this, &NetInternalsMessageHandler::OnLoadLogFile));
dom_ui_->RegisterMessageCallback(
"notifyReady",
......@@ -469,6 +533,25 @@ void NetInternalsMessageHandler::CallJavascriptFunction(
}
}
////////////////////////////////////////////////////////////////////////////////
//
// NetInternalsMessageHandler::ReadLogFileTask
//
////////////////////////////////////////////////////////////////////////////////
NetInternalsMessageHandler::ReadLogFileTask::ReadLogFileTask(
IOThreadImpl* proxy, const FilePath& path)
: proxy_(proxy), path_(path) {
}
void NetInternalsMessageHandler::ReadLogFileTask::Run() {
std::string file_contents;
if (!file_util::ReadFileToString(path_, &file_contents))
return;
proxy_->CallJavascriptFunction(L"g_browser.loadedLogFile",
new StringValue(file_contents));
}
////////////////////////////////////////////////////////////////////////////////
//
// NetInternalsMessageHandler::IOThreadImpl
......
......@@ -67,7 +67,8 @@ ChromeNetLog::ChromeNetLog()
const CommandLine& command_line = *CommandLine::ForCurrentProcess();
if (command_line.HasSwitch(switches::kLogNetLog)) {
net_log_logger_.reset(new NetLogLogger());
net_log_logger_.reset(new NetLogLogger(
command_line.GetSwitchValuePath(switches::kLogNetLog)));
AddObserver(net_log_logger_.get());
}
}
......
......@@ -4,11 +4,19 @@
#include "chrome/browser/net/net_log_logger.h"
#include <stdio.h>
#include "base/file_util.h"
#include "base/json/json_writer.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
NetLogLogger::NetLogLogger()
NetLogLogger::NetLogLogger(const FilePath &log_path)
: ThreadSafeObserver(net::NetLog::LOG_ALL_BUT_BYTES) {
if (!log_path.empty()) {
base::ThreadRestrictions::ScopedAllowIO allow_io;
file_.Set(file_util::OpenFile(log_path, "w"));
}
}
NetLogLogger::~NetLogLogger() {}
......@@ -21,8 +29,16 @@ void NetLogLogger::OnAddEntry(net::NetLog::EventType type,
scoped_ptr<Value> value(net::NetLog::EntryToDictionaryValue(type, time,
source, phase,
params, true));
// Don't pretty print, so each JSON value occupies a single line, with no
// breaks (Line breaks in any text field will be escaped). Using strings
// instead of integer identifiers allows logs from older versions to be
// loaded, though a little extra parsing has to be done when loading a log.
std::string json;
base::JSONWriter::Write(value.get(), true, &json);
VLOG(1) << json;
base::JSONWriter::Write(value.get(), false, &json);
if (!file_.get()) {
VLOG(1) << json;
} else {
fprintf(file_.get(), "%s\n", json.c_str());
}
}
......@@ -6,14 +6,23 @@
#define CHROME_BROWSER_NET_NET_LOG_LOGGER_H_
#pragma once
#include "base/scoped_handle.h"
#include "chrome/browser/net/chrome_net_log.h"
class FilePath;
// NetLogLogger watches the NetLog event stream, and sends all entries to
// VLOG(1). This is to debug errors that prevent getting to the
// about:net-internals page.
// VLOG(1) or a path specified on creation. This is to debug errors that
// prevent getting to the about:net-internals page.
//
// Relies on ChromeNetLog only calling an Observer once at a time for
// thread-safety.
class NetLogLogger : public ChromeNetLog::ThreadSafeObserver {
public:
NetLogLogger();
// If |log_path| is empty or file creation fails, writes to VLOG(1).
// Otherwise, writes to |log_path|. Uses one line per entry, for
// easy parsing.
explicit NetLogLogger(const FilePath &log_path);
~NetLogLogger();
// ThreadSafeObserver implementation:
......@@ -24,6 +33,8 @@ class NetLogLogger : public ChromeNetLog::ThreadSafeObserver {
net::NetLog::EventParameters* params);
private:
ScopedStdioHandle file_;
DISALLOW_COPY_AND_ASSIGN(NetLogLogger);
};
......
......@@ -19,7 +19,12 @@ function DataView(mainBoxId,
byteLoggingCheckboxId,
passivelyCapturedCountId,
activelyCapturedCountId,
deleteAllId) {
deleteAllId,
dumpDataDivId,
loadDataDivId,
loadLogFileId,
capturingTextSpanId,
loggingTextSpanId) {
DivView.call(this, mainBoxId);
this.textPre_ = document.getElementById(outputTextBoxId);
......@@ -40,6 +45,14 @@ function DataView(mainBoxId,
document.getElementById(deleteAllId).onclick =
g_browser.deleteAllEvents.bind(g_browser);
this.dumpDataDiv_ = document.getElementById(dumpDataDivId);
this.loadDataDiv_ = document.getElementById(loadDataDivId);
this.capturingTextSpan_ = document.getElementById(capturingTextSpanId);
this.loggingTextSpan_ = document.getElementById(loggingTextSpanId);
document.getElementById(loadLogFileId).onclick =
g_browser.loadLogFile.bind(g_browser);
this.updateEventCounts_();
this.waitingForUpdate_ = false;
......@@ -70,6 +83,18 @@ DataView.prototype.onAllLogEntriesDeleted = function() {
this.updateEventCounts_();
};
/**
* Called when either a log file is loaded or when going back to actively
* logging events. In either case, called after clearing the old entries,
* but before getting any new ones.
*/
DataView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
setNodeDisplay(this.dumpDataDiv_, !isViewingLogFile);
setNodeDisplay(this.capturingTextSpan_, !isViewingLogFile);
setNodeDisplay(this.loggingTextSpan_, isViewingLogFile);
this.setText_('');
};
/**
* Updates the counters showing how many events have been captured.
*/
......@@ -108,6 +133,10 @@ DataView.prototype.onExportToText_ = function() {
* Presents the captured data as formatted text.
*/
DataView.prototype.onUpdateAllCompleted = function(data) {
// It's possible for a log file to be loaded while a dump is being generated.
// When that happens, don't display the log dump, to avoid any confusion.
if (g_browser.isViewingLogFile())
return;
this.waitingForUpdate_ = false;
var text = [];
......
......@@ -475,10 +475,20 @@ EventsView.prototype.onLogEntriesDeleted = function(sourceIds) {
/**
* Called whenever all log events are deleted.
*/
EventsView.prototype.onAllLogEntriesDeleted = function(offset) {
EventsView.prototype.onAllLogEntriesDeleted = function() {
this.initializeSourceList_();
};
/**
* Called when either a log file is loaded or when going back to actively
* logging events. In either case, called after clearing the old entries,
* but before getting any new ones.
*/
EventsView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
// Needed to sort new sourceless entries correctly.
this.maxReceivedSourceId_ = 0;
};
EventsView.prototype.incrementPrefilterCount = function(offset) {
this.numPrefilter_ += offset;
this.invalidateFilterCounter_();
......
......@@ -202,23 +202,41 @@ found in the LICENSE file.
<table width=100%>
<tr>
<td valign=top>
<h2>Dump data</h2>
<div style="margin: 8px">
<p><input id=securityStrippingCheckbox type=checkbox checked=yes>
Strip private information (cookies and credentials).
</p>
<p>
<a href="javascript:displayHelpForBugDump()">
Help: How to get data for bug reports?
</a>
</p>
<button id=exportToText class=bigButton>Dump to text</button>
<div id=dataViewDumpDataDiv>
<h2>Dump data</h2>
<div style="margin: 8px">
<p><input id=securityStrippingCheckbox type=checkbox checked=yes>
Strip private information (cookies and credentials).
</p>
<p>
<a href="javascript:displayHelpForBugDump()">
Help: How to get data for bug reports?
</a>
</p>
<button id=exportToText class=bigButton>Dump to text</button>
</div>
</div>
<div id=dataViewLoadDataDiv>
<h2>Load data</h2>
<div style="margin: 8px">
<p><input type=button value="Load log from file" id=dataViewLoadLogFile /></p>
<p>Only works with log files created with "--log-net-log=file_name".</p>
<p>Once a log is loaded, this page will stop collecting data, and will
only start gathering data again when the page is
<a href="javascript:history.go(0);">reloaded</a>.<BR>
</p>
</div>
</div>
</td>
<td align=right valign=top>
<div class="capturingBox">
<b>Capturing all events...</b>
<div class="capturingBox" id=dataViewCapturingBox>
<span id=dataViewCapturingTextSpan>
<b>Capturing all events...</b>
</span>
<span id=dataViewLoggingTextSpan style="display: none;">
<b>Viewing loaded log file.</b>
</span>
<table style="margin: 8px">
<tr>
<td>Passively captured:</td>
......
......@@ -75,10 +75,12 @@ function onLoaded() {
// captured data.
var dataView = new DataView('dataTabContent', 'exportedDataText',
'exportToText', 'securityStrippingCheckbox',
'byteLoggingCheckbox',
'passivelyCapturedCount',
'activelyCapturedCount',
'dataViewDeleteAll');
'byteLoggingCheckbox', 'passivelyCapturedCount',
'activelyCapturedCount', 'dataViewDeleteAll',
'dataViewDumpDataDiv', 'dataViewLoadDataDiv',
'dataViewLoadLogFile',
'dataViewCapturingTextSpan',
'dataViewLoggingTextSpan');
// Create a view which will display the results and controls for connection
// tests.
......@@ -108,6 +110,7 @@ function onLoaded() {
// Create a view which lets you tab between the different sub-views.
var categoryTabSwitcher = new TabSwitcherView('categoryTabHandles');
g_browser.setTabSwitcher(categoryTabSwitcher);
// Populate the main tabs.
categoryTabSwitcher.addTab('eventsTab', eventsView, false);
......@@ -144,6 +147,9 @@ function onLoaded() {
// Select the initial view based on the current URL.
window.onhashchange();
// Inform observers a log file is not currently being displayed.
g_browser.setIsViewingLogFile_(false);
// Tell the browser that we are ready to start receiving log events.
g_browser.sendReady();
}
......@@ -191,6 +197,11 @@ function BrowserBridge() {
// Next unique id to be assigned to a log entry without a source.
// Needed to simplify deletion, identify associated GUI elements, etc.
this.nextSourcelessEventId_ = -1;
// True when viewing a log file rather than actively logged events.
// When viewing a log file, all tabs are hidden except the event view,
// and all received events are ignored.
this.isViewingLogFile_ = false;
}
/*
......@@ -302,23 +313,19 @@ BrowserBridge.prototype.setLogLevel = function(logLevel) {
chrome.send('setLogLevel', ['' + logLevel]);
}
BrowserBridge.prototype.loadLogFile = function() {
chrome.send('loadLogFile');
}
//------------------------------------------------------------------------------
// Messages received from the browser
//------------------------------------------------------------------------------
BrowserBridge.prototype.receivedLogEntries = function(logEntries) {
for (var e = 0; e < logEntries.length; ++e) {
var logEntry = logEntries[e];
// Assign unique ID, if needed.
if (logEntry.source.id == 0) {
logEntry.source.id = this.nextSourcelessEventId_;
--this.nextSourcelessEventId_;
}
this.capturedEvents_.push(logEntry);
for (var i = 0; i < this.logObservers_.length; ++i)
this.logObservers_[i].onLogEntryAdded(logEntry);
}
// Does nothing if viewing a log file.
if (this.isViewingLogFile_)
return;
this.addLogEntries(logEntries);
};
BrowserBridge.prototype.receivedLogEventTypeConstants = function(constantsMap) {
......@@ -439,8 +446,90 @@ BrowserBridge.prototype.receivedHttpCacheInfo = function(info) {
this.pollableDataHelpers_.httpCacheInfo.update(info);
};
BrowserBridge.prototype.loadedLogFile = function(logFileContents) {
var match;
// Replace carriage returns with linebreaks and then split around linebreaks.
var lines = logFileContents.replace(/\r/g, '\n').split('\n');
var entries = [];
var numInvalidLines = 0;
for (var i = 0; i < lines.length; ++i) {
if (lines[i].trim().length == 0)
continue;
// Parse all valid lines, skipping any others.
try {
var entry = JSON.parse(lines[i]);
if (entry &&
typeof(entry) == 'object' &&
entry.phase != undefined &&
entry.source != undefined &&
entry.time != undefined &&
entry.type != undefined) {
entries.push(entry);
continue;
}
} catch (err) {
}
++numInvalidLines;
console.log('Unable to parse log line: ' + lines[i]);
}
if (entries.length == 0) {
window.alert('Loading log file failed.');
return;
}
this.deleteAllEvents();
this.setIsViewingLogFile_(true);
var validEntries = [];
for (var i = 0; i < entries.length; ++i) {
entries[i].wasPassivelyCaptured = true;
if (LogEventType[entries[i].type] != undefined &&
LogSourceType[entries[i].source.type] != undefined &&
LogEventPhase[entries[i].phase] != undefined) {
entries[i].type = LogEventType[entries[i].type];
entries[i].source.type = LogSourceType[entries[i].source.type];
entries[i].phase = LogEventPhase[entries[i].phase];
validEntries.push(entries[i]);
} else {
// TODO(mmenke): Do something reasonable when the event type isn't
// found, which could happen when event types are
// removed or added between versions. Could also happen
// with source types, but less likely.
console.log(
'Unrecognized values in log entry: ' + JSON.stringify(entry));
}
}
this.numPassivelyCapturedEvents_ = validEntries.length;
this.addLogEntries(validEntries);
var numInvalidEntries = entries.length - validEntries.length;
if (numInvalidEntries > 0 || numInvalidLines > 0) {
window.alert(
numInvalidLines.toString() +
' could not be parsed as JSON strings, and ' +
numInvalidEntries.toString() +
' entries don\'t have valid data.\n\n' +
'Unparseable lines may indicate log file corruption.\n' +
'Entries with invalid data may be caused by version differences.\n\n' +
'See console for more information.');
}
}
//------------------------------------------------------------------------------
/**
* Sets the |categoryTabSwitcher_| of BrowserBridge. Since views depend on
* g_browser being initialized, have to have a BrowserBridge prior to tab
* construction.
*/
BrowserBridge.prototype.setTabSwitcher = function(categoryTabSwitcher) {
this.categoryTabSwitcher_ = categoryTabSwitcher;
};
/**
* Adds a listener of log entries. |observer| will be called back when new log
* data arrives, through:
......@@ -591,6 +680,25 @@ BrowserBridge.prototype.getNumPassivelyCapturedEvents = function() {
return this.numPassivelyCapturedEvents_;
};
/**
* Sends each entry to all log observers, and updates |capturedEvents_|.
* Also assigns unique ids to log entries without a source.
*/
BrowserBridge.prototype.addLogEntries = function(logEntries) {
for (var e = 0; e < logEntries.length; ++e) {
var logEntry = logEntries[e];
// Assign unique ID, if needed.
if (logEntry.source.id == 0) {
logEntry.source.id = this.nextSourcelessEventId_;
--this.nextSourcelessEventId_;
}
this.capturedEvents_.push(logEntry);
for (var i = 0; i < this.logObservers_.length; ++i)
this.logObservers_[i].onLogEntryAdded(logEntry);
}
};
/**
* Deletes captured events with source IDs in |sourceIds|.
*/
......@@ -625,6 +733,39 @@ BrowserBridge.prototype.deleteAllEvents = function() {
this.logObservers_[i].onAllLogEntriesDeleted();
};
/**
* Informs log observers whether or not future events will be from a log file.
* Hides all tabs except the events and data tabs when viewing a log file, shows
* them all otherwise.
*/
BrowserBridge.prototype.setIsViewingLogFile_ = function(isViewingLogFile) {
this.isViewingLogFile_ = isViewingLogFile;
var tabIds = this.categoryTabSwitcher_.getAllTabIds();
for (var i = 0; i < this.logObservers_.length; ++i)
this.logObservers_[i].onSetIsViewingLogFile(isViewingLogFile);
// Shows/hides tabs not used when viewing a log file.
for (var i = 0; i < tabIds.length; ++i) {
if (tabIds[i] == 'eventsTab' || tabIds[i] == 'dataTab')
continue;
this.categoryTabSwitcher_.showTabHandleNode(tabIds[i], !isViewingLogFile);
}
if (isViewingLogFile) {
var activeTab = this.categoryTabSwitcher_.findActiveTab();
if (activeTab.id != 'eventsTab')
this.categoryTabSwitcher_.switchToTab('dataTab', null);
}
};
/**
* Returns true if a log file is currently being viewed.
*/
BrowserBridge.prototype.isViewingLogFile = function() {
return this.isViewingLogFile_;
};
/**
* If |force| is true, calls all startUpdate functions. Otherwise, just
* runs updates with active observers.
......
......@@ -114,3 +114,7 @@ ProxyView.prototype.onLogEntriesDeleted = function(sourceIds) {
ProxyView.prototype.onAllLogEntriesDeleted = function() {
this.clearLog_();
};
ProxyView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
};
......@@ -136,6 +136,13 @@ TabSwitcherView.prototype.getAllTabIds = function() {
return ids;
};
// Shows/hides the DOM node that is used to select the tab. Will not change
// the active tab.
TabSwitcherView.prototype.showTabHandleNode = function(id, isVisible) {
var tab = this.findTabById(id);
setNodeDisplay(tab.getTabHandleNode(), isVisible);
};
//-----------------------------------------------------------------------------
/**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment