diff options
Diffstat (limited to 'devtools/client/debugger/content/actions/sources.js')
-rw-r--r-- | devtools/client/debugger/content/actions/sources.js | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/devtools/client/debugger/content/actions/sources.js b/devtools/client/debugger/content/actions/sources.js new file mode 100644 index 000000000..d7e0728e7 --- /dev/null +++ b/devtools/client/debugger/content/actions/sources.js @@ -0,0 +1,280 @@ +/* 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 Services = require("Services"); +const { dumpn } = require("devtools/shared/DevToolsUtils"); +const { PROMISE, HISTOGRAM_ID } = require("devtools/client/shared/redux/middleware/promise"); +const { getSource, getSourceText } = require("../queries"); +const { Task } = require("devtools/shared/task"); + +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 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 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 sourceClient.prettyPrint(Prefs.editorTabSize); + } + else { + response = yield 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 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 +}; |