tenfourfox/devtools/client/debugger/content/actions/sources.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

284 lines
8.5 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 constants = require('../constants');
const promise = require('promise');
const { rdpInvoke } = require('../utils');
const { dumpn } = require("devtools/shared/DevToolsUtils");
const { PROMISE, HISTOGRAM_ID } = require('devtools/client/shared/redux/middleware/promise');
const { getSource, getSourceText } = require('../queries');
const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
function getSourceClient(source) {
return gThreadClient.source(source);
}
/**
* Handler for the debugger client's unsolicited newSource notification.
*/
function newSource(source) {
return dispatch => {
// Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) != -1) {
return;
}
// Signal that a new source has been added.
window.emit(EVENTS.NEW_SOURCE);
return dispatch({
type: constants.ADD_SOURCE,
source: source
});
};
}
function selectSource(source, opts) {
return (dispatch, getState) => {
if (!gThreadClient) {
// No connection, do nothing. This happens when the debugger is
// shut down too fast and it tries to display a default source.
return;
}
source = getSource(getState(), source.actor);
// Make sure to start a request to load the source text.
dispatch(loadSourceText(source));
dispatch({
type: constants.SELECT_SOURCE,
source: source,
opts: opts
});
};
}
function loadSources() {
return {
type: constants.LOAD_SOURCES,
[PROMISE]: Task.spawn(function*() {
const response = yield rdpInvoke(gThreadClient, gThreadClient.getSources);
// Top-level breakpoints may pause the entire loading process
// because scripts are executed as they are loaded, so the
// engine may pause in the middle of loading all the sources.
// This is relatively harmless, as individual `newSource`
// notifications are fired for each script and they will be
// added to the UI through that.
if (!response.sources) {
dumpn(
"Error getting sources, probably because a top-level " +
"breakpoint was hit while executing them"
);
return;
}
// Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
return response.sources.filter(source => {
return NEW_SOURCE_IGNORED_URLS.indexOf(source.url) === -1;
});
})
}
}
/**
* Set the black boxed status of the given source.
*
* @param Object aSource
* The source form.
* @param bool aBlackBoxFlag
* True to black box the source, false to un-black box it.
* @returns Promise
* A promize that resolves to [aSource, isBlackBoxed] or rejects to
* [aSource, error].
*/
function blackbox(source, shouldBlackBox) {
const client = getSourceClient(source);
return {
type: constants.BLACKBOX,
source: source,
[PROMISE]: Task.spawn(function*() {
yield rdpInvoke(client,
shouldBlackBox ? client.blackBox : client.unblackBox);
return {
isBlackBoxed: shouldBlackBox
}
})
};
}
/**
* Toggle the pretty printing of a source's text. All subsequent calls to
* |getText| will return the pretty-toggled text. Nothing will happen for
* non-javascript files.
*
* @param Object aSource
* The source form from the RDP.
* @returns Promise
* A promise that resolves to [aSource, prettyText] or rejects to
* [aSource, error].
*/
function togglePrettyPrint(source) {
return (dispatch, getState) => {
const sourceClient = getSourceClient(source);
const wantPretty = !source.isPrettyPrinted;
return dispatch({
type: constants.TOGGLE_PRETTY_PRINT,
source: source,
[PROMISE]: Task.spawn(function*() {
let response;
// Only attempt to pretty print JavaScript sources.
const sourceText = getSourceText(getState(), source.actor);
const contentType = sourceText ? sourceText.contentType : null;
if (!SourceUtils.isJavaScript(source.url, contentType)) {
throw new Error("Can't prettify non-javascript files.");
}
if (wantPretty) {
response = yield rdpInvoke(sourceClient,
sourceClient.prettyPrint,
Prefs.editorTabSize);
}
else {
response = yield rdpInvoke(sourceClient,
sourceClient.disablePrettyPrint);
}
// Remove the cached source AST from the Parser, to avoid getting
// wrong locations when searching for functions.
DebuggerController.Parser.clearSource(source.url);
return {
isPrettyPrinted: wantPretty,
text: response.source,
contentType: response.contentType
};
})
});
};
}
function loadSourceText(source) {
return (dispatch, getState) => {
// Fetch the source text only once.
let textInfo = getSourceText(getState(), source.actor);
if (textInfo) {
// It's already loaded or is loading
return promise.resolve(textInfo);
}
const sourceClient = getSourceClient(source);
return dispatch({
type: constants.LOAD_SOURCE_TEXT,
source: source,
[PROMISE]: Task.spawn(function*() {
let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE";
let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS";
let histogram = Services.telemetry.getHistogramById(histogramId);
let startTime = Date.now();
const response = yield rdpInvoke(sourceClient, sourceClient.source);
histogram.add(Date.now() - startTime);
// Automatically pretty print if enabled and the test is
// detected to be "minified"
if (Prefs.autoPrettyPrint &&
!source.isPrettyPrinted &&
SourceUtils.isMinified(source.actor, response.source)) {
dispatch(togglePrettyPrint(source));
}
return { text: response.source,
contentType: response.contentType };
})
});
}
}
/**
* Starts fetching all the sources, silently.
*
* @param array aUrls
* The urls for the sources to fetch. If fetching a source's text
* takes too long, it will be discarded.
* @return object
* A promise that is resolved after source texts have been fetched.
*/
function getTextForSources(actors) {
return (dispatch, getState) => {
let deferred = promise.defer();
let pending = new Set(actors);
let fetched = [];
// Can't use promise.all, because if one fetch operation is rejected, then
// everything is considered rejected, thus no other subsequent source will
// be getting fetched. We don't want that. Something like Q's allSettled
// would work like a charm here.
// Try to fetch as many sources as possible.
for (let actor of actors) {
let source = getSource(getState(), actor);
dispatch(loadSourceText(source)).then(({ text, contentType }) => {
onFetch([source, text, contentType]);
}, err => {
onError(source, err);
});
}
setTimeout(onTimeout, FETCH_SOURCE_RESPONSE_DELAY);
/* Called if fetching a source takes too long. */
function onTimeout() {
pending = new Set();
maybeFinish();
}
/* Called if fetching a source finishes successfully. */
function onFetch([aSource, aText, aContentType]) {
// If fetching the source has previously timed out, discard it this time.
if (!pending.has(aSource.actor)) {
return;
}
pending.delete(aSource.actor);
fetched.push([aSource.actor, aText, aContentType]);
maybeFinish();
}
/* Called if fetching a source failed because of an error. */
function onError([aSource, aError]) {
pending.delete(aSource.actor);
maybeFinish();
}
/* Called every time something interesting happens while fetching sources. */
function maybeFinish() {
if (pending.size == 0) {
// Sort the fetched sources alphabetically by their url.
deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
}
}
return deferred.promise;
};
}
module.exports = {
newSource,
selectSource,
loadSources,
blackbox,
togglePrettyPrint,
loadSourceText,
getTextForSources
};