diff options
Diffstat (limited to 'devtools/client/debugger/content/actions')
-rw-r--r-- | devtools/client/debugger/content/actions/breakpoints.js | 191 | ||||
-rw-r--r-- | devtools/client/debugger/content/actions/event-listeners.js | 118 | ||||
-rw-r--r-- | devtools/client/debugger/content/actions/moz.build | 10 | ||||
-rw-r--r-- | devtools/client/debugger/content/actions/sources.js | 280 |
4 files changed, 599 insertions, 0 deletions
diff --git a/devtools/client/debugger/content/actions/breakpoints.js b/devtools/client/debugger/content/actions/breakpoints.js new file mode 100644 index 000000000..5c5552d78 --- /dev/null +++ b/devtools/client/debugger/content/actions/breakpoints.js @@ -0,0 +1,191 @@ +/* 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 { asPaused } = require("../utils"); +const { PROMISE } = require("devtools/client/shared/redux/middleware/promise"); +const { + getSource, getBreakpoint, getBreakpoints, makeLocationId +} = require("../queries"); +const { Task } = require("devtools/shared/task"); + +// Because breakpoints are just simple data structures, we still need +// a way to lookup the actual client instance to talk to the server. +// We keep an internal database of clients based off of actor ID. +const BREAKPOINT_CLIENT_STORE = new Map(); + +function setBreakpointClient(actor, client) { + BREAKPOINT_CLIENT_STORE.set(actor, client); +} + +function getBreakpointClient(actor) { + return BREAKPOINT_CLIENT_STORE.get(actor); +} + +function enableBreakpoint(location) { + // Enabling is exactly the same as adding. It will use the existing + // breakpoint that still stored. + return addBreakpoint(location); +} + +function _breakpointExists(state, location) { + const currentBp = getBreakpoint(state, location); + return currentBp && !currentBp.disabled; +} + +function _getOrCreateBreakpoint(state, location, condition) { + return getBreakpoint(state, location) || { location, condition }; +} + +function addBreakpoint(location, condition) { + return (dispatch, getState) => { + if (_breakpointExists(getState(), location)) { + return; + } + + const bp = _getOrCreateBreakpoint(getState(), location, condition); + + return dispatch({ + type: constants.ADD_BREAKPOINT, + breakpoint: bp, + condition: condition, + [PROMISE]: Task.spawn(function* () { + const sourceClient = gThreadClient.source( + getSource(getState(), bp.location.actor) + ); + const [response, bpClient] = yield sourceClient.setBreakpoint({ + line: bp.location.line, + column: bp.location.column, + condition: bp.condition + }); + const { isPending, actualLocation } = response; + + // Save the client instance + setBreakpointClient(bpClient.actor, bpClient); + + return { + text: DebuggerView.editor.getText( + (actualLocation ? actualLocation.line : bp.location.line) - 1 + ).trim(), + + // If the breakpoint response has an "actualLocation" attached, then + // the original requested placement for the breakpoint wasn't + // accepted. + actualLocation: isPending ? null : actualLocation, + actor: bpClient.actor + }; + }) + }); + }; +} + +function disableBreakpoint(location) { + return _removeOrDisableBreakpoint(location, true); +} + +function removeBreakpoint(location) { + return _removeOrDisableBreakpoint(location); +} + +function _removeOrDisableBreakpoint(location, isDisabled) { + return (dispatch, getState) => { + let bp = getBreakpoint(getState(), location); + if (!bp) { + throw new Error("attempt to remove breakpoint that does not exist"); + } + if (bp.loading) { + // TODO(jwl): make this wait until the breakpoint is saved if it + // is still loading + throw new Error("attempt to remove unsaved breakpoint"); + } + + const bpClient = getBreakpointClient(bp.actor); + const action = { + type: constants.REMOVE_BREAKPOINT, + breakpoint: bp, + disabled: isDisabled + }; + + // If the breakpoint is already disabled, we don't need to remove + // it from the server. We just need to dispatch an action + // simulating a successful server request to remove it, and it + // will be removed completely from the state. + if (!bp.disabled) { + return dispatch(Object.assign({}, action, { + [PROMISE]: bpClient.remove() + })); + } else { + return dispatch(Object.assign({}, action, { status: "done" })); + } + }; +} + +function removeAllBreakpoints() { + return (dispatch, getState) => { + const breakpoints = getBreakpoints(getState()); + const activeBreakpoints = breakpoints.filter(bp => !bp.disabled); + activeBreakpoints.forEach(bp => removeBreakpoint(bp.location)); + }; +} + +/** + * Update the condition of a breakpoint. + * + * @param object aLocation + * @see DebuggerController.Breakpoints.addBreakpoint + * @param string aClients + * The condition to set on the breakpoint + * @return object + * A promise that will be resolved with the breakpoint client + */ +function setBreakpointCondition(location, condition) { + return (dispatch, getState) => { + const bp = getBreakpoint(getState(), location); + if (!bp) { + throw new Error("Breakpoint does not exist at the specified location"); + } + if (bp.loading) { + // TODO(jwl): when this function is called, make sure the action + // creator waits for the breakpoint to exist + throw new Error("breakpoint must be saved"); + } + + const bpClient = getBreakpointClient(bp.actor); + const action = { + type: constants.SET_BREAKPOINT_CONDITION, + breakpoint: bp, + condition: condition + }; + + // If it's not disabled, we need to update the condition on the + // server. Otherwise, just dispatch a non-remote action that + // updates the condition locally. + if (!bp.disabled) { + return dispatch(Object.assign({}, action, { + [PROMISE]: Task.spawn(function* () { + const newClient = yield bpClient.setCondition(gThreadClient, condition); + + // Remove the old instance and save the new one + setBreakpointClient(bpClient.actor, null); + setBreakpointClient(newClient.actor, newClient); + + return { actor: newClient.actor }; + }) + })); + } else { + return dispatch(action); + } + }; +} + +module.exports = { + enableBreakpoint, + addBreakpoint, + disableBreakpoint, + removeBreakpoint, + removeAllBreakpoints, + setBreakpointCondition +}; diff --git a/devtools/client/debugger/content/actions/event-listeners.js b/devtools/client/debugger/content/actions/event-listeners.js new file mode 100644 index 000000000..4bca557fe --- /dev/null +++ b/devtools/client/debugger/content/actions/event-listeners.js @@ -0,0 +1,118 @@ +/* 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 { asPaused } = require("../utils"); +const { reportException } = require("devtools/shared/DevToolsUtils"); +const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers"); +const { Task } = require("devtools/shared/task"); + +const FETCH_EVENT_LISTENERS_DELAY = 200; // ms + +function fetchEventListeners() { + return (dispatch, getState) => { + // Make sure we"re not sending a batch of closely repeated requests. + // This can easily happen whenever new sources are fetched. + setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => { + // In case there is still a request of listeners going on (it + // takes several RDP round trips right now), make sure we wait + // on a currently running request + if (getState().eventListeners.fetchingListeners) { + dispatch({ + type: services.WAIT_UNTIL, + predicate: action => ( + action.type === constants.FETCH_EVENT_LISTENERS && + action.status === "done" + ), + run: dispatch => dispatch(fetchEventListeners()) + }); + return; + } + + dispatch({ + type: constants.FETCH_EVENT_LISTENERS, + status: "begin" + }); + + asPaused(gThreadClient, _getListeners).then(listeners => { + // Notify that event listeners were fetched and shown in the view, + // and callback to resume the active thread if necessary. + window.emit(EVENTS.EVENT_LISTENERS_FETCHED); + + dispatch({ + type: constants.FETCH_EVENT_LISTENERS, + status: "done", + listeners: listeners + }); + }); + }); + }; +} + +const _getListeners = Task.async(function* () { + const response = yield gThreadClient.eventListeners(); + + // Make sure all the listeners are sorted by the event type, since + // they"re not guaranteed to be clustered together. + response.listeners.sort((a, b) => a.type > b.type ? 1 : -1); + + // Add all the listeners in the debugger view event linsteners container. + let fetchedDefinitions = new Map(); + let listeners = []; + for (let listener of response.listeners) { + let definitionSite; + if (fetchedDefinitions.has(listener.function.actor)) { + definitionSite = fetchedDefinitions.get(listener.function.actor); + } else if (listener.function.class == "Function") { + definitionSite = yield _getDefinitionSite(listener.function); + if (!definitionSite) { + // We don"t know where this listener comes from so don"t show it in + // the UI as breaking on it doesn"t work (bug 942899). + continue; + } + + fetchedDefinitions.set(listener.function.actor, definitionSite); + } + listener.function.url = definitionSite; + listeners.push(listener); + } + fetchedDefinitions.clear(); + + return listeners; +}); + +const _getDefinitionSite = Task.async(function* (aFunction) { + const grip = gThreadClient.pauseGrip(aFunction); + let response; + + try { + response = yield grip.getDefinitionSite(); + } + catch (e) { + // Don't make this error fatal, because it would break the entire events pane. + reportException("_getDefinitionSite", e); + return null; + } + + return response.source.url; +}); + +function updateEventBreakpoints(eventNames) { + return dispatch => { + setNamedTimeout("event-breakpoints-update", 0, () => { + gThreadClient.pauseOnDOMEvents(eventNames, function () { + // Notify that event breakpoints were added/removed on the server. + window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED); + + dispatch({ + type: constants.UPDATE_EVENT_BREAKPOINTS, + eventNames: eventNames + }); + }); + }); + }; +} + +module.exports = { updateEventBreakpoints, fetchEventListeners }; diff --git a/devtools/client/debugger/content/actions/moz.build b/devtools/client/debugger/content/actions/moz.build new file mode 100644 index 000000000..13a2dd9ad --- /dev/null +++ b/devtools/client/debugger/content/actions/moz.build @@ -0,0 +1,10 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'breakpoints.js', + 'event-listeners.js', + 'sources.js' +) 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 +}; |