blob: c76da239668145658005a6ead0d4e241291a7653 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './strings.m.js';
import {sendWithPromise} from 'chrome://resources/js/cr.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {$} from 'chrome://resources/js/util.m.js';
// Contents of lines that act as delimiters for multi-line values.
const DELIM_START = '---------- START ----------';
const DELIM_END = '---------- END ----------';
// Limit file size to 10 MiB to prevent hanging on accidental upload.
const MAX_FILE_SIZE = 10485760;
// <if expr="chromeos">
// Link to markdown doc with documentation for Chrome OS.
const CROS_MD_DOC_URL =
'https://chromium.googlesource.com/chromiumos/platform2/+/HEAD/debugd/docs/log_entries.md';
// </if>
function getValueDivForButton(button) {
return $(button.id.substr(0, button.id.length - 4));
}
function getButtonForValueDiv(valueDiv) {
return $(valueDiv.id + '-btn');
}
function handleDragOver(e) {
e.dataTransfer.dropEffect = 'copy';
e.preventDefault();
}
function handleDrop(e) {
const file = e.dataTransfer.files[0];
if (file) {
e.preventDefault();
importLog(file);
}
}
function showError(fileName) {
$('status').textContent = loadTimeData.getStringF('parseError', fileName);
}
/**
* Toggles whether an item is collapsed or expanded.
*/
function changeCollapsedStatus() {
const valueDiv = getValueDivForButton(this);
if (valueDiv.parentNode.className == 'number-collapsed') {
valueDiv.parentNode.className = 'number-expanded';
this.textContent = loadTimeData.getString('collapseBtn');
} else {
valueDiv.parentNode.className = 'number-collapsed';
this.textContent = loadTimeData.getString('expandBtn');
}
}
/**
* Collapses all log items.
*/
function collapseAll() {
const valueDivs = document.getElementsByClassName('stat-value');
for (let i = 0; i < valueDivs.length; i++) {
const button = getButtonForValueDiv(valueDivs[i]);
if (button && button.className != 'button-hidden') {
button.textContent = loadTimeData.getString('expandBtn');
valueDivs[i].parentNode.className = 'number-collapsed';
}
}
}
/**
* Expands all log items.
*/
function expandAll() {
const valueDivs = document.getElementsByClassName('stat-value');
for (let i = 0; i < valueDivs.length; i++) {
const button = getButtonForValueDiv(valueDivs[i]);
if (button && button.className != 'button-hidden') {
button.textContent = loadTimeData.getString('collapseBtn');
valueDivs[i].parentNode.className = 'number-expanded';
}
}
}
/**
* Read in a log asynchronously, calling parseSystemLog if successful.
* @param {File} file The file to read.
*/
function importLog(file) {
if (file && file.size <= MAX_FILE_SIZE) {
const reader = new FileReader();
reader.onload = function() {
if (parseSystemLog(this.result)) {
// Reset table title and status
$('tableTitle').textContent =
loadTimeData.getStringF('logFileTableTitle', file.name);
$('status').textContent = '';
} else {
showError(file.name);
}
};
reader.readAsText(file);
} else if (file) {
showError(file.name);
}
}
/**
* For a particular log entry, create the DOM node representing it in the
* log entry table.
* @param{log} A dictionary with the keys statName and statValue
* @return{Element} The DOM node for the given log entry.
*/
function createNodeForLogEntry(log) {
const row = document.createElement('tr');
const nameCell = document.createElement('td');
nameCell.className = 'name';
const nameDiv = document.createElement('div');
nameDiv.className = 'stat-name';
// Add an anchor link that links to the log entry.
const anchor = document.createElement('a');
anchor.href = `#${log.statName}`;
anchor.text = '🔗';
nameDiv.appendChild(anchor);
const a = document.createElement('a');
a.className = 'stat-name-link';
// Let URL be anchor to the section of this page by default.
let urlPrefix = '';
// <if expr="chromeos">
// Link to the markdown doc with documentation for the entry for Chrome OS
// instead.
urlPrefix = CROS_MD_DOC_URL;
// </if>
a.href = `${urlPrefix}#${log.statName}`;
a.name = a.text = log.statName;
nameDiv.appendChild(a);
nameCell.appendChild(nameDiv);
row.appendChild(nameCell);
const buttonCell = document.createElement('td');
buttonCell.className = 'button-cell';
const button = document.createElement('button');
button.id = log.statName + '-value-btn';
button.className = 'expand-status';
button.onclick = changeCollapsedStatus;
buttonCell.appendChild(button);
row.appendChild(buttonCell);
const valueCell = document.createElement('td');
const valueDiv = document.createElement('div');
valueDiv.className = 'stat-value';
valueDiv.id = log.statName + '-value';
valueDiv.textContent = log.statValue;
valueCell.appendChild(valueDiv);
row.appendChild(valueCell);
if (log.statValue.length > 200) {
button.className = '';
button.textContent = loadTimeData.getString('expandBtn');
valueCell.className = 'number-collapsed';
} else {
button.className = 'button-hidden';
valueCell.className = 'number';
}
return row;
}
/**
* Given a list of log entries, replace the contents of the log entry table
* with those entries. The log entries are passed as a list of dictionaries
* containing the keys statName and statValue.
* @param {systemInfo} The log entries to insert into the DOM.
*/
function updateLogEntries(systemInfo) {
const fragment = document.createDocumentFragment();
systemInfo.forEach(logEntry => {
const node = createNodeForLogEntry(logEntry);
fragment.appendChild(node);
});
const table = $('details');
// Delete any existing log entries in the table
table.innerHTML = trustedTypes.emptyHTML;
table.appendChild(fragment);
}
/**
* Callback called when system info has been fetched. The log entries are passed
* as a list of dictionaries containing the keys statName and statValue.
* @param {systemInfo} The fetched log entries.
*/
function returnSystemInfo(systemInfo) {
updateLogEntries(systemInfo);
const spinner = $('loadingIndicator');
spinner.style.display = 'none';
spinner.style.animationPlayState = 'paused';
}
/**
* Convert text-based log into list of name-value pairs.
* @param {string} text The raw text of a log.
* @return {boolean} True if the log was parsed successfully.
*/
function parseSystemLog(text) {
const details = [];
const lines = text.split('\n');
for (let i = 0, len = lines.length; i < len; i++) {
// Skip empty lines.
if (!lines[i]) {
continue;
}
const delimiter = lines[i].indexOf('=');
if (delimiter <= 0) {
if (i == lines.length - 1) {
break;
}
// If '=' is missing here, format is wrong.
return false;
}
const name = lines[i].substring(0, delimiter);
let value = '';
// Set value if non-empty
if (lines[i].length > delimiter + 1) {
value = lines[i].substring(delimiter + 1);
}
// Delimiters are based on kMultilineIndicatorString, kMultilineStartString,
// and kMultilineEndString in components/feedback/feedback_data.cc.
// If these change, we should check for both the old and new versions.
if (value == '<multiline>') {
// Skip start delimiter.
if (i == len - 1 || lines[++i].indexOf(DELIM_START) == -1) {
return false;
}
++i;
value = '';
// Append lines between start and end delimiters.
while (i < len && lines[i] != DELIM_END) {
value += lines[i++] + '\n';
}
// Remove trailing newline.
if (value) {
value = value.substr(0, value.length - 1);
}
}
details.push({'statName': name, 'statValue': value});
}
updateLogEntries(details);
return true;
}
document.addEventListener('DOMContentLoaded', function() {
sendWithPromise('requestSystemInfo').then(returnSystemInfo);
$('collapseAll').onclick = collapseAll;
$('expandAll').onclick = expandAll;
const tp = $('t');
tp.addEventListener('dragover', handleDragOver, false);
tp.addEventListener('drop', handleDrop, false);
});