summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/content/actions
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/content/actions')
-rw-r--r--devtools/client/debugger/content/actions/breakpoints.js191
-rw-r--r--devtools/client/debugger/content/actions/event-listeners.js118
-rw-r--r--devtools/client/debugger/content/actions/moz.build10
-rw-r--r--devtools/client/debugger/content/actions/sources.js280
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
+};