705 lines
22 KiB
JavaScript
705 lines
22 KiB
JavaScript
/* 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 { AppConstants } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AppConstants.sys.mjs"
|
|
);
|
|
const { ProcessType } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ProcessType.sys.mjs"
|
|
);
|
|
|
|
let AboutThirdParty = null;
|
|
let CrashModuleSet = null;
|
|
let gBackgroundTasksDone = false;
|
|
|
|
function moduleCompareForDisplay(a, b) {
|
|
// First, show blocked modules that were blocked at launch - this will keep the ordering
|
|
// consistent when the user blocks/unblocks things.
|
|
const bBlocked =
|
|
b.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
|
|
? 1
|
|
: 0;
|
|
const aBlocked =
|
|
a.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
|
|
? 1
|
|
: 0;
|
|
|
|
let diff = bBlocked - aBlocked;
|
|
if (diff) {
|
|
return diff;
|
|
}
|
|
|
|
// Next, show crasher modules
|
|
diff = b.isCrasher - a.isCrasher;
|
|
if (diff) {
|
|
return diff;
|
|
}
|
|
|
|
// Then unknown-type modules
|
|
diff = a.typeFlags - b.typeFlags;
|
|
if (diff) {
|
|
return diff;
|
|
}
|
|
|
|
// Lastly sort the remaining modules in descending order
|
|
// of duration to move up slower modules.
|
|
return b.loadingOnMain - a.loadingOnMain;
|
|
}
|
|
|
|
async function fetchData() {
|
|
let data = null;
|
|
try {
|
|
// Wait until the module load events are ready (bug 1833152)
|
|
const sleep = delayInMs =>
|
|
new Promise(resolve => setTimeout(resolve, delayInMs));
|
|
let loadEventsReady = Services.telemetry.areUntrustedModuleLoadEventsReady;
|
|
let numberOfAttempts = 0;
|
|
// Just to make sure we don't infinite loop here. (this is normally quite
|
|
// quick) If we do hit this limit, the page will return an empty list of
|
|
// modules.
|
|
const MAX_ATTEMPTS = 30;
|
|
while (!loadEventsReady && numberOfAttempts < MAX_ATTEMPTS) {
|
|
await sleep(1000);
|
|
numberOfAttempts++;
|
|
loadEventsReady = Services.telemetry.areUntrustedModuleLoadEventsReady;
|
|
}
|
|
|
|
data = await Services.telemetry.getUntrustedModuleLoadEvents(
|
|
Services.telemetry.INCLUDE_OLD_LOADEVENTS |
|
|
Services.telemetry.KEEP_LOADEVENTS_NEW |
|
|
Services.telemetry.INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS |
|
|
Services.telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS
|
|
);
|
|
} catch (e) {
|
|
// No error report in case of NS_ERROR_NOT_AVAILABLE
|
|
// because the method throws it when data is empty.
|
|
if (
|
|
!(e instanceof Components.Exception) ||
|
|
e.result != Cr.NS_ERROR_NOT_AVAILABLE
|
|
) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
if (!data || !data.modules || !data.processes) {
|
|
return null;
|
|
}
|
|
|
|
// The original telemetry data structure has an array of modules
|
|
// and an array of loading events referring to the module array's
|
|
// item via its index.
|
|
// To easily display data per module, we put loading events into
|
|
// a corresponding module object and return the module array.
|
|
|
|
for (const module of data.modules) {
|
|
module.events = [];
|
|
module.loadingOnMain = { count: 0, sum: 0 };
|
|
|
|
const moduleName = module.dllFile?.leafName;
|
|
module.typeFlags = AboutThirdParty.lookupModuleType(moduleName);
|
|
module.isCrasher = CrashModuleSet?.has(moduleName);
|
|
|
|
module.application = AboutThirdParty.lookupApplication(
|
|
module.dllFile?.path
|
|
);
|
|
module.moduleName = module.dllFile?.leafName;
|
|
module.hasLoadInformation = true;
|
|
}
|
|
|
|
let blockedModules = data.blockedModules.map(blockedModuleName => {
|
|
return {
|
|
moduleName: blockedModuleName,
|
|
typeFlags: AboutThirdParty.lookupModuleType(blockedModuleName),
|
|
isCrasher: CrashModuleSet?.has(blockedModuleName),
|
|
hasLoadInformation: false,
|
|
};
|
|
});
|
|
|
|
for (const [proc, perProc] of Object.entries(data.processes)) {
|
|
for (const event of perProc.events) {
|
|
// The expected format of |proc| is <type>.<pid> like "browser.0x1234"
|
|
const [ptype, pidHex] = proc.split(".");
|
|
event.processType = ptype;
|
|
event.processID = parseInt(pidHex, 16);
|
|
|
|
event.mainThread =
|
|
event.threadName == "MainThread" || event.threadName == "Main Thread";
|
|
|
|
const module = data.modules[event.moduleIndex];
|
|
if (event.mainThread) {
|
|
++module.loadingOnMain.count;
|
|
module.loadingOnMain.sum += event.loadDurationMS;
|
|
}
|
|
|
|
module.events.push(event);
|
|
}
|
|
}
|
|
|
|
for (const module of data.modules) {
|
|
const avg = module.loadingOnMain.count
|
|
? module.loadingOnMain.sum / module.loadingOnMain.count
|
|
: 0;
|
|
module.loadingOnMain = avg;
|
|
module.events.sort((a, b) => {
|
|
const diff = a.processType.localeCompare(b.processType);
|
|
return diff ? diff : a.processID - b.processID;
|
|
});
|
|
// If this module was blocked but not by the user, it must have been blocked
|
|
// by the static blocklist.
|
|
// But we don't know this for sure unless the background tasks were done
|
|
// by the time we gathered data about the module above.
|
|
if (gBackgroundTasksDone) {
|
|
module.isBlockedByBuiltin =
|
|
!(
|
|
module.typeFlags &
|
|
Ci.nsIAboutThirdParty.ModuleType_BlockedByUserAtLaunch
|
|
) &&
|
|
!!module.events.length &&
|
|
module.events.every(e => e.loadStatus !== 0);
|
|
} else {
|
|
module.isBlockedByBuiltin = false;
|
|
}
|
|
}
|
|
|
|
data.modules.sort(moduleCompareForDisplay);
|
|
|
|
return { modules: data.modules, blocked: blockedModules };
|
|
}
|
|
|
|
function setContent(element, text, l10n) {
|
|
if (text) {
|
|
element.textContent = text;
|
|
} else if (l10n) {
|
|
document.l10n.setAttributes(element, l10n);
|
|
}
|
|
}
|
|
|
|
function onClickOpenDir(event) {
|
|
const module = event.target.closest(".card").module;
|
|
if (!module?.dllFile) {
|
|
return;
|
|
}
|
|
module.dllFile.reveal();
|
|
}
|
|
|
|
// Returns whether we should restart.
|
|
async function confirmRestartPrompt() {
|
|
let [msg, title, restartButtonText, restartLaterButtonText] =
|
|
await document.l10n.formatValues([
|
|
{ id: "third-party-blocking-requires-restart" },
|
|
{ id: "third-party-should-restart-title" },
|
|
{ id: "third-party-restart-now" },
|
|
{ id: "third-party-restart-later" },
|
|
]);
|
|
let buttonFlags =
|
|
Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
|
|
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING +
|
|
Services.prompt.BUTTON_POS_1_DEFAULT;
|
|
let buttonIndex = Services.prompt.confirmEx(
|
|
window.browsingContext.topChromeWindow,
|
|
title,
|
|
msg,
|
|
buttonFlags,
|
|
restartButtonText,
|
|
restartLaterButtonText,
|
|
null,
|
|
null,
|
|
{}
|
|
);
|
|
return buttonIndex === 0;
|
|
}
|
|
|
|
let processingBlockRequest = false;
|
|
async function onBlock(event) {
|
|
const module = event.target.closest(".card").module;
|
|
if (!module?.moduleName) {
|
|
return;
|
|
}
|
|
// To avoid race conditions, don't allow any modules to be blocked/unblocked
|
|
// until we've updated and written the blocklist.
|
|
if (processingBlockRequest) {
|
|
return;
|
|
}
|
|
processingBlockRequest = true;
|
|
|
|
let updatedBlocklist = false;
|
|
try {
|
|
const wasBlocked = event.target.classList.contains("module-blocked");
|
|
await AboutThirdParty.updateBlocklist(module.moduleName, !wasBlocked);
|
|
|
|
event.target.classList.toggle("module-blocked");
|
|
let blockButtonL10nId;
|
|
if (wasBlocked) {
|
|
blockButtonL10nId = "third-party-button-to-block-module";
|
|
} else {
|
|
blockButtonL10nId = AboutThirdParty.isDynamicBlocklistDisabled
|
|
? "third-party-button-to-unblock-module-disabled"
|
|
: "third-party-button-to-unblock-module";
|
|
}
|
|
document.l10n.setAttributes(event.target, blockButtonL10nId);
|
|
updatedBlocklist = true;
|
|
} catch (ex) {
|
|
console.error("Failed to update the blocklist file - ", ex.result);
|
|
} finally {
|
|
processingBlockRequest = false;
|
|
}
|
|
if (updatedBlocklist && (await confirmRestartPrompt())) {
|
|
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
|
|
Ci.nsISupportsPRBool
|
|
);
|
|
Services.obs.notifyObservers(
|
|
cancelQuit,
|
|
"quit-application-requested",
|
|
"restart"
|
|
);
|
|
if (!cancelQuit.data) {
|
|
// restart was not cancelled.
|
|
// Note that even if we're in safe mode, we don't restart
|
|
// into safe mode, because it's likely the user is trying to
|
|
// fix a crash or something, and they'd probably like to
|
|
// see if it works.
|
|
Services.startup.quit(
|
|
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onClickExpand(event) {
|
|
const card = event.target.closest(".card");
|
|
const button = event.target.closest("button");
|
|
|
|
const table = card.querySelector(".event-table");
|
|
if (!table) {
|
|
return;
|
|
}
|
|
|
|
if (table.hidden) {
|
|
table.hidden = false;
|
|
button.classList.add("button-collapse");
|
|
button.classList.remove("button-expand");
|
|
setContent(button, null, "third-party-button-collapse");
|
|
} else {
|
|
table.hidden = true;
|
|
button.classList.add("button-expand");
|
|
button.classList.remove("button-collapse");
|
|
setContent(button, null, "third-party-button-expand");
|
|
}
|
|
}
|
|
|
|
function createDetailRow(label, value) {
|
|
if (!document.templateDetailRow) {
|
|
document.templateDetailRow = document.querySelector(
|
|
"template[name=module-detail-row]"
|
|
);
|
|
}
|
|
|
|
const fragment = document.templateDetailRow.content.cloneNode(true);
|
|
setContent(fragment.querySelector("div > label"), null, label);
|
|
setContent(fragment.querySelector("div > span"), value);
|
|
return fragment;
|
|
}
|
|
|
|
function copyDataToClipboard(aData) {
|
|
const modulesData = aData.modules.map(module => {
|
|
const copied = {
|
|
name: module.moduleName,
|
|
fileVersion: module.fileVersion,
|
|
};
|
|
|
|
// We include the typeFlags field only when it's not 0 because
|
|
// typeFlags == 0 means system info is not yet collected.
|
|
if (module.typeFlags) {
|
|
copied.typeFlags = module.typeFlags;
|
|
}
|
|
if (module.signedBy) {
|
|
copied.signedBy = module.signedBy;
|
|
}
|
|
if (module.isCrasher) {
|
|
copied.isCrasher = module.isCrasher;
|
|
}
|
|
if (module.companyName) {
|
|
copied.companyName = module.companyName;
|
|
}
|
|
if (module.application) {
|
|
copied.applicationName = module.application.name;
|
|
copied.applicationPublisher = module.application.publisher;
|
|
}
|
|
|
|
if (Array.isArray(module.events)) {
|
|
copied.events = module.events.map(event => {
|
|
return {
|
|
processType: event.processType,
|
|
processID: event.processID,
|
|
threadID: event.threadID,
|
|
loadStatus: event.loadStatus,
|
|
loadDurationMS: event.loadDurationMS,
|
|
};
|
|
});
|
|
}
|
|
|
|
return copied;
|
|
});
|
|
const blockedData = aData.blocked.map(blockedModule => {
|
|
const copied = {
|
|
name: blockedModule.moduleName,
|
|
};
|
|
// We include the typeFlags field only when it's not 0 because
|
|
// typeFlags == 0 means system info is not yet collected.
|
|
if (blockedModule.typeFlags) {
|
|
copied.typeFlags = blockedModule.typeFlags;
|
|
}
|
|
if (blockedModule.isCrasher) {
|
|
copied.isCrasher = blockedModule.isCrasher;
|
|
}
|
|
return copied;
|
|
});
|
|
let clipboardData = { modules: modulesData, blocked: blockedData };
|
|
|
|
return navigator.clipboard.writeText(JSON.stringify(clipboardData, null, 2));
|
|
}
|
|
|
|
function correctProcessTypeForFluent(type) {
|
|
// GetProcessTypeString() in UntrustedModulesDataSerializer.cpp converted
|
|
// the "default" process type to "browser" to send as telemetry. We revert
|
|
// it to pass to ProcessType API.
|
|
const geckoType = type == "browser" ? "default" : type;
|
|
return ProcessType.fluentNameFromProcessTypeString(geckoType);
|
|
}
|
|
|
|
function setUpBlockButton(aCard, isBlocklistDisabled, aModule) {
|
|
const blockButton = aCard.querySelector(".button-block");
|
|
if (aModule.hasLoadInformation) {
|
|
if (!aModule.isBlockedByBuiltin) {
|
|
blockButton.hidden = aModule.typeFlags == 0;
|
|
}
|
|
} else {
|
|
// This means that this is an entry in the dynamic blocklist that
|
|
// has not attempted to load, thus we have very little information
|
|
// about it (just its name). So this should always show up.
|
|
blockButton.hidden = false;
|
|
// Bug 1808904 - don't allow unblocking this module before we've loaded
|
|
// the list of blocked modules in the background task.
|
|
blockButton.disabled = !gBackgroundTasksDone;
|
|
}
|
|
// If we haven't loaded the typeFlags yet and we don't have any load information for this
|
|
// module, default to showing that the module is blocked (because we must have gotten this
|
|
// module's info from the dynamic blocklist)
|
|
if (
|
|
aModule.typeFlags & Ci.nsIAboutThirdParty.ModuleType_BlockedByUser ||
|
|
(aModule.typeFlags == 0 && !aModule.hasLoadInformation)
|
|
) {
|
|
blockButton.classList.add("module-blocked");
|
|
}
|
|
|
|
if (isBlocklistDisabled) {
|
|
blockButton.classList.add("blocklist-disabled");
|
|
}
|
|
if (blockButton.classList.contains("module-blocked")) {
|
|
document.l10n.setAttributes(
|
|
blockButton,
|
|
isBlocklistDisabled
|
|
? "third-party-button-to-unblock-module-disabled"
|
|
: "third-party-button-to-unblock-module"
|
|
);
|
|
}
|
|
}
|
|
|
|
function visualizeData(aData) {
|
|
const templateCard = document.querySelector("template[name=card]");
|
|
const templateBlockedCard = document.querySelector(
|
|
"template[name=card-blocked]"
|
|
);
|
|
const templateTableRow = document.querySelector(
|
|
"template[name=event-table-row]"
|
|
);
|
|
|
|
// These correspond to the enum ModuleLoadInfo::Status
|
|
const labelLoadStatus = [
|
|
"third-party-status-loaded",
|
|
"third-party-status-blocked",
|
|
"third-party-status-redirected",
|
|
"third-party-status-blocked",
|
|
];
|
|
|
|
const isBlocklistAvailable =
|
|
AboutThirdParty.isDynamicBlocklistAvailable &&
|
|
Services.policies.isAllowed("thirdPartyModuleBlocking");
|
|
const isBlocklistDisabled = AboutThirdParty.isDynamicBlocklistDisabled;
|
|
|
|
const mainContentFragment = new DocumentFragment();
|
|
|
|
// Blocklist entries are case-insensitive
|
|
let lowercaseModuleNames = new Set(
|
|
aData.modules.map(module => module.moduleName.toLowerCase())
|
|
);
|
|
for (const module of aData.blocked) {
|
|
if (lowercaseModuleNames.has(module.moduleName.toLowerCase())) {
|
|
// Only show entries that we haven't already tried to load,
|
|
// because those will already show up in the page
|
|
continue;
|
|
}
|
|
const newCard = templateBlockedCard.content.cloneNode(true);
|
|
setContent(newCard.querySelector(".module-name"), module.moduleName);
|
|
// Referred by the button click handlers
|
|
newCard.querySelector(".card").module = {
|
|
moduleName: module.moduleName,
|
|
};
|
|
|
|
if (isBlocklistAvailable) {
|
|
setUpBlockButton(newCard, isBlocklistDisabled, module);
|
|
}
|
|
if (module.isCrasher) {
|
|
newCard.querySelector(".image-warning").hidden = false;
|
|
}
|
|
mainContentFragment.appendChild(newCard);
|
|
}
|
|
|
|
for (const module of aData.modules) {
|
|
const newCard = templateCard.content.cloneNode(true);
|
|
const moduleName = module.moduleName;
|
|
|
|
// Referred by the button click handlers
|
|
newCard.querySelector(".card").module = {
|
|
dllFile: module.dllFile,
|
|
moduleName: module.moduleName,
|
|
fileVersion: module.fileVersion,
|
|
};
|
|
|
|
setContent(newCard.querySelector(".module-name"), moduleName);
|
|
|
|
const modTagsContainer = newCard.querySelector(".module-tags");
|
|
if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_IME) {
|
|
modTagsContainer.querySelector(".tag-ime").hidden = false;
|
|
}
|
|
if (module.typeFlags & Ci.nsIAboutThirdParty.ModuleType_ShellExtension) {
|
|
modTagsContainer.querySelector(".tag-shellex").hidden = false;
|
|
}
|
|
|
|
newCard.querySelector(".blocked-by-builtin").hidden =
|
|
!module.isBlockedByBuiltin;
|
|
if (isBlocklistAvailable) {
|
|
setUpBlockButton(newCard, isBlocklistDisabled, module);
|
|
}
|
|
|
|
if (module.isCrasher) {
|
|
newCard.querySelector(".image-warning").hidden = false;
|
|
}
|
|
|
|
if (!module.signedBy) {
|
|
newCard.querySelector(".image-unsigned").hidden = false;
|
|
}
|
|
|
|
const modDetailContainer = newCard.querySelector(".module-details");
|
|
|
|
if (module.application) {
|
|
modDetailContainer.appendChild(
|
|
createDetailRow("third-party-detail-app", module.application.name)
|
|
);
|
|
modDetailContainer.appendChild(
|
|
createDetailRow(
|
|
"third-party-detail-publisher",
|
|
module.application.publisher
|
|
)
|
|
);
|
|
}
|
|
|
|
if (module.fileVersion) {
|
|
modDetailContainer.appendChild(
|
|
createDetailRow("third-party-detail-version", module.fileVersion)
|
|
);
|
|
}
|
|
|
|
const vendorInfo = module.signedBy || module.companyName;
|
|
if (vendorInfo) {
|
|
modDetailContainer.appendChild(
|
|
createDetailRow("third-party-detail-vendor", vendorInfo)
|
|
);
|
|
}
|
|
|
|
modDetailContainer.appendChild(
|
|
createDetailRow("third-party-detail-occurrences", module.events.length)
|
|
);
|
|
modDetailContainer.appendChild(
|
|
createDetailRow(
|
|
"third-party-detail-duration",
|
|
module.loadingOnMain || "-"
|
|
)
|
|
);
|
|
|
|
const eventTable = newCard.querySelector(".event-table > tbody");
|
|
for (const event of module.events) {
|
|
const fragment = templateTableRow.content.cloneNode(true);
|
|
|
|
const row = fragment.querySelector("tr");
|
|
|
|
setContent(
|
|
row.children[0].querySelector(".process-type"),
|
|
null,
|
|
correctProcessTypeForFluent(event.processType)
|
|
);
|
|
setContent(row.children[0].querySelector(".process-id"), event.processID);
|
|
|
|
// Use setContent() instead of simple assignment because
|
|
// loadDurationMS can be empty (not zero) when a module is
|
|
// loaded very early in the process and we need to show
|
|
// a text in that case.
|
|
setContent(
|
|
row.children[1].querySelector(".event-duration"),
|
|
event.loadDurationMS,
|
|
"third-party-message-no-duration"
|
|
);
|
|
row.querySelector(".tag-background").hidden = event.mainThread;
|
|
|
|
setContent(row.children[2], null, labelLoadStatus[event.loadStatus]);
|
|
eventTable.appendChild(fragment);
|
|
}
|
|
|
|
mainContentFragment.appendChild(newCard);
|
|
}
|
|
|
|
const main = document.getElementById("main");
|
|
main.appendChild(mainContentFragment);
|
|
main.addEventListener("click", onClickInMain);
|
|
}
|
|
|
|
function onClickInMain(event) {
|
|
const classList = event.target.classList;
|
|
if (classList.contains("button-open-dir")) {
|
|
onClickOpenDir(event);
|
|
} else if (classList.contains("button-block")) {
|
|
onBlock(event);
|
|
} else if (
|
|
classList.contains("button-expand") ||
|
|
classList.contains("button-collapse")
|
|
) {
|
|
onClickExpand(event);
|
|
}
|
|
}
|
|
|
|
function clearVisualizedData() {
|
|
const mainDiv = document.getElementById("main");
|
|
while (mainDiv.firstChild) {
|
|
mainDiv.firstChild.remove();
|
|
}
|
|
}
|
|
|
|
async function collectCrashInfo() {
|
|
const parseBigInt = maybeBigInt => {
|
|
try {
|
|
return BigInt(maybeBigInt);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
return NaN;
|
|
};
|
|
|
|
if (CrashModuleSet || !AppConstants.MOZ_CRASHREPORTER) {
|
|
return;
|
|
}
|
|
|
|
const { getCrashManager } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/CrashManager.sys.mjs"
|
|
);
|
|
const crashes = await getCrashManager().getCrashes();
|
|
CrashModuleSet = new Set(
|
|
crashes.map(crash => {
|
|
const stackInfo = crash.metadata?.StackTraces;
|
|
if (!stackInfo) {
|
|
return null;
|
|
}
|
|
|
|
const crashAddr = parseBigInt(stackInfo.crash_info?.address);
|
|
if (typeof crashAddr !== "bigint") {
|
|
return null;
|
|
}
|
|
|
|
// Find modules whose address range includes the crashing address.
|
|
// No need to check the type of the return value from parseBigInt
|
|
// because comparing BigInt with NaN returns false.
|
|
return stackInfo.modules?.find(
|
|
module =>
|
|
crashAddr >= parseBigInt(module.base_addr) &&
|
|
crashAddr < parseBigInt(module.end_addr)
|
|
)?.filename;
|
|
})
|
|
);
|
|
}
|
|
|
|
async function onLoad() {
|
|
document
|
|
.getElementById("button-copy-to-clipboard")
|
|
.addEventListener("click", async e => {
|
|
e.target.disabled = true;
|
|
|
|
const data = await fetchData();
|
|
await copyDataToClipboard(data || []).catch(console.error);
|
|
|
|
e.target.disabled = false;
|
|
});
|
|
|
|
const backgroundTasks = [
|
|
AboutThirdParty.collectSystemInfo(),
|
|
collectCrashInfo(),
|
|
];
|
|
|
|
let hasData = false;
|
|
Promise.all(backgroundTasks)
|
|
.then(() => {
|
|
gBackgroundTasksDone = true;
|
|
// Reload button will either show or is not needed, so we can hide the
|
|
// loading indicator.
|
|
document.getElementById("background-data-loading").hidden = true;
|
|
if (!hasData) {
|
|
// If all async tasks were completed before fetchData,
|
|
// or there was no data available, visualizeData shows
|
|
// full info and the reload button is not needed.
|
|
return;
|
|
}
|
|
|
|
// Add {once: true} to prevent multiple listeners from being scheduled
|
|
const button = document.getElementById("button-reload");
|
|
button.addEventListener(
|
|
"click",
|
|
async event => {
|
|
// Update the content with data we've already collected.
|
|
clearVisualizedData();
|
|
visualizeData(await fetchData());
|
|
event.target.hidden = true;
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
// Coming here means visualizeData is completed before the background
|
|
// tasks are completed. Because the page does not show full information,
|
|
// we show the reload button to call visualizeData again.
|
|
button.hidden = false;
|
|
})
|
|
.catch(console.error);
|
|
|
|
const data = await fetchData();
|
|
// Used for testing purposes
|
|
window.fetchDataDone = true;
|
|
|
|
hasData = !!data?.modules.length || !!data?.blocked.length;
|
|
if (!hasData) {
|
|
document.getElementById("no-data").hidden = false;
|
|
return;
|
|
}
|
|
|
|
visualizeData(data);
|
|
}
|
|
|
|
try {
|
|
AboutThirdParty = Cc["@mozilla.org/about-thirdparty;1"].getService(
|
|
Ci.nsIAboutThirdParty
|
|
);
|
|
document.addEventListener("DOMContentLoaded", onLoad, { once: true });
|
|
} catch (ex) {
|
|
// Do nothing if we fail to create a singleton instance,
|
|
// showing the default no-module message.
|
|
console.error(ex);
|
|
}
|