tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

modify.js (11399B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import { createBreakpoint } from "../../client/firefox/create";
      6 import {
      7  makeBreakpointServerLocation,
      8  makeBreakpointId,
      9 } from "../../utils/breakpoint/index";
     10 import {
     11  getBreakpoint,
     12  getBreakpointPositionsForLocation,
     13  getFirstBreakpointPosition,
     14  getSettledSourceTextContent,
     15  getBreakpointsList,
     16  getPendingBreakpointList,
     17  isMapScopesEnabled,
     18  getBlackBoxRanges,
     19  isSourceMapIgnoreListEnabled,
     20  isSourceOnSourceMapIgnoreList,
     21 } from "../../selectors/index";
     22 
     23 import { setBreakpointPositions } from "./breakpointPositions";
     24 import { setSkipPausing } from "../pause/skipPausing";
     25 
     26 const {
     27  PROMISE,
     28 } = require("resource://devtools/client/shared/redux/middleware/promise.js");
     29 import { recordEvent } from "../../utils/telemetry";
     30 import { comparePosition } from "../../utils/location";
     31 import { getTextAtPosition, isLineBlackboxed } from "../../utils/source";
     32 import { getMappedScopesForLocation } from "../pause/mapScopes";
     33 import { validateBreakpoint } from "../../utils/context";
     34 
     35 // This file has the primitive operations used to modify individual breakpoints
     36 // and keep them in sync with the breakpoints installed on server threads. These
     37 // are collected here to make it easier to preserve the following invariant:
     38 //
     39 // Breakpoints are included in reducer state if they are disabled or requests
     40 // have been dispatched to set them in all server threads.
     41 //
     42 // To maintain this property, updates to the reducer and installed breakpoints
     43 // must happen with no intervening await. Using await allows other operations to
     44 // modify the breakpoint state in the interim and potentially cause breakpoint
     45 // state to go out of sync.
     46 //
     47 // The reducer is optimistically updated when users set or remove a breakpoint,
     48 // but it might take a little while before the breakpoints have been set or
     49 // removed in each thread. Once all outstanding requests sent to a thread have
     50 // been processed, the reducer and server threads will be in sync.
     51 //
     52 // There is another exception to the above invariant when first connecting to
     53 // the server: breakpoints have been installed on all generated locations in the
     54 // pending breakpoints, but no breakpoints have been added to the reducer. When
     55 // a matching source appears, either the server breakpoint will be removed or a
     56 // breakpoint will be added to the reducer, to restore the above invariant.
     57 // See syncBreakpoint.js for more.
     58 
     59 async function clientSetBreakpoint(client, { getState, dispatch }, breakpoint) {
     60  const breakpointServerLocation = makeBreakpointServerLocation(
     61    getState(),
     62    breakpoint.generatedLocation
     63  );
     64  const shouldMapBreakpointExpressions =
     65    isMapScopesEnabled(getState()) &&
     66    breakpoint.location.source.isOriginal &&
     67    (breakpoint.options.logValue || breakpoint.options.condition);
     68 
     69  if (shouldMapBreakpointExpressions) {
     70    breakpoint = await dispatch(updateBreakpointSourceMapping(breakpoint));
     71  }
     72  return client.setBreakpoint(breakpointServerLocation, breakpoint.options);
     73 }
     74 
     75 function clientRemoveBreakpoint(client, state, generatedLocation) {
     76  const breakpointServerLocation = makeBreakpointServerLocation(
     77    state,
     78    generatedLocation
     79  );
     80  return client.removeBreakpoint(breakpointServerLocation);
     81 }
     82 
     83 export function enableBreakpoint(initialBreakpoint) {
     84  return thunkArgs => {
     85    const { dispatch, getState, client } = thunkArgs;
     86    const state = getState();
     87    const breakpoint = getBreakpoint(state, initialBreakpoint.location);
     88    const blackboxedRanges = getBlackBoxRanges(state);
     89    const isSourceOnIgnoreList =
     90      isSourceMapIgnoreListEnabled(state) &&
     91      isSourceOnSourceMapIgnoreList(state, breakpoint.location.source);
     92    if (
     93      !breakpoint ||
     94      !breakpoint.disabled ||
     95      isLineBlackboxed(
     96        blackboxedRanges[breakpoint.location.source.url],
     97        breakpoint.location.line,
     98        isSourceOnIgnoreList
     99      )
    100    ) {
    101      return null;
    102    }
    103 
    104    // This action is used from various context menus and automatically re-enables breakpoints.
    105    dispatch(setSkipPausing(false));
    106 
    107    return dispatch({
    108      type: "SET_BREAKPOINT",
    109      breakpoint: createBreakpoint({ ...breakpoint, disabled: false }),
    110      [PROMISE]: clientSetBreakpoint(client, thunkArgs, breakpoint),
    111    });
    112  };
    113 }
    114 
    115 export function addBreakpoint(
    116  initialLocation,
    117  options = {},
    118  disabled,
    119  shouldCancel = () => false
    120 ) {
    121  return async thunkArgs => {
    122    const { dispatch, getState, client } = thunkArgs;
    123    recordEvent("add_breakpoint");
    124 
    125    await dispatch(setBreakpointPositions(initialLocation));
    126 
    127    const position = initialLocation.column
    128      ? getBreakpointPositionsForLocation(getState(), initialLocation)
    129      : getFirstBreakpointPosition(getState(), initialLocation);
    130 
    131    // No position is found if the `initialLocation` is on a non-breakable line or
    132    // the line no longer exists.
    133    if (!position) {
    134      console.error(
    135        `Unable to add breakpoint at non-breakable location "${JSON.stringify(initialLocation)}"`
    136      );
    137      return null;
    138    }
    139 
    140    const { location, generatedLocation } = position;
    141 
    142    if (!location.source || !generatedLocation.source) {
    143      return null;
    144    }
    145 
    146    const originalContent = getSettledSourceTextContent(getState(), location);
    147    const originalText = getTextAtPosition(
    148      location.source.id,
    149      originalContent,
    150      location
    151    );
    152 
    153    const content = getSettledSourceTextContent(getState(), generatedLocation);
    154    const text = getTextAtPosition(
    155      generatedLocation.source.id,
    156      content,
    157      generatedLocation
    158    );
    159 
    160    const id = makeBreakpointId(location);
    161    const breakpoint = createBreakpoint({
    162      id,
    163      disabled,
    164      options,
    165      location,
    166      generatedLocation,
    167      text,
    168      originalText,
    169    });
    170 
    171    if (shouldCancel()) {
    172      return null;
    173    }
    174 
    175    return dispatch({
    176      type: "SET_BREAKPOINT",
    177      breakpoint,
    178      // If we just clobbered an enabled breakpoint with a disabled one, we need
    179      // to remove any installed breakpoint in the server.
    180      [PROMISE]: disabled
    181        ? clientRemoveBreakpoint(client, getState(), generatedLocation)
    182        : clientSetBreakpoint(client, thunkArgs, breakpoint),
    183    });
    184  };
    185 }
    186 
    187 /**
    188 * Remove a single breakpoint
    189 *
    190 * @memberof actions/breakpoints
    191 * @static
    192 */
    193 export function removeBreakpoint(initialBreakpoint) {
    194  return ({ dispatch, getState, client }) => {
    195    recordEvent("remove_breakpoint");
    196 
    197    const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
    198    if (!breakpoint) {
    199      return null;
    200    }
    201 
    202    return dispatch({
    203      type: "REMOVE_BREAKPOINT",
    204      breakpoint,
    205      // If the breakpoint is disabled then it is not installed in the server.
    206      [PROMISE]: breakpoint.disabled
    207        ? Promise.resolve()
    208        : clientRemoveBreakpoint(
    209            client,
    210            getState(),
    211            breakpoint.generatedLocation
    212          ),
    213    });
    214  };
    215 }
    216 
    217 /**
    218 * Remove all installed, pending, and client breakpoints associated with a
    219 * target generated location.
    220 *
    221 * @param {object} target
    222 *        Location object where to remove breakpoints.
    223 */
    224 export function removeBreakpointAtGeneratedLocation(target) {
    225  return ({ dispatch, getState, client }) => {
    226    // remove breakpoint from the server
    227    const onBreakpointRemoved = clientRemoveBreakpoint(
    228      client,
    229      getState(),
    230      target
    231    );
    232    // Remove any breakpoints matching the generated location.
    233    const breakpoints = getBreakpointsList(getState());
    234    for (const breakpoint of breakpoints) {
    235      const { generatedLocation } = breakpoint;
    236      if (
    237        generatedLocation.source.id == target.source.id &&
    238        comparePosition(generatedLocation, target)
    239      ) {
    240        dispatch({
    241          type: "REMOVE_BREAKPOINT",
    242          breakpoint,
    243          [PROMISE]: onBreakpointRemoved,
    244        });
    245      }
    246    }
    247 
    248    // Remove any remaining pending breakpoints matching the generated location.
    249    const pending = getPendingBreakpointList(getState());
    250    for (const pendingBreakpoint of pending) {
    251      const { generatedLocation } = pendingBreakpoint;
    252      if (
    253        generatedLocation.sourceUrl == target.source.url &&
    254        comparePosition(generatedLocation, target)
    255      ) {
    256        dispatch({
    257          type: "REMOVE_PENDING_BREAKPOINT",
    258          pendingBreakpoint,
    259        });
    260      }
    261    }
    262    return onBreakpointRemoved;
    263  };
    264 }
    265 
    266 /**
    267 * Disable a single breakpoint
    268 *
    269 * @memberof actions/breakpoints
    270 * @static
    271 */
    272 export function disableBreakpoint(initialBreakpoint) {
    273  return ({ dispatch, getState, client }) => {
    274    const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
    275    if (!breakpoint || breakpoint.disabled) {
    276      return null;
    277    }
    278 
    279    return dispatch({
    280      type: "SET_BREAKPOINT",
    281      breakpoint: createBreakpoint({ ...breakpoint, disabled: true }),
    282      [PROMISE]: clientRemoveBreakpoint(
    283        client,
    284        getState(),
    285        breakpoint.generatedLocation
    286      ),
    287    });
    288  };
    289 }
    290 
    291 /**
    292 * Update the options of a breakpoint.
    293 *
    294 * @throws {Error} "not implemented"
    295 * @memberof actions/breakpoints
    296 * @static
    297 * @param {SourceLocation} location
    298 *        @see DebuggerController.Breakpoints.addBreakpoint
    299 * @param {object} options
    300 *        Any options to set on the breakpoint
    301 */
    302 export function setBreakpointOptions(location, options = {}) {
    303  return thunkArgs => {
    304    const { dispatch, getState, client } = thunkArgs;
    305    let breakpoint = getBreakpoint(getState(), location);
    306    if (!breakpoint) {
    307      return dispatch(addBreakpoint(location, options));
    308    }
    309 
    310    // Note: setting a breakpoint's options implicitly enables it.
    311    breakpoint = createBreakpoint({ ...breakpoint, disabled: false, options });
    312 
    313    return dispatch({
    314      type: "SET_BREAKPOINT",
    315      breakpoint,
    316      [PROMISE]: clientSetBreakpoint(client, thunkArgs, breakpoint),
    317    });
    318  };
    319 }
    320 
    321 async function updateExpression(parserWorker, mappings, originalExpression) {
    322  const mapped = await parserWorker.mapExpression(
    323    originalExpression,
    324    mappings,
    325    [],
    326    false,
    327    false
    328  );
    329  if (!mapped) {
    330    return originalExpression;
    331  }
    332  if (!originalExpression.trimEnd().endsWith(";")) {
    333    return mapped.expression.replace(/;$/, "");
    334  }
    335  return mapped.expression;
    336 }
    337 
    338 function updateBreakpointSourceMapping(breakpoint) {
    339  return async ({ getState, dispatch, parserWorker }) => {
    340    const options = { ...breakpoint.options };
    341 
    342    const mappedScopes = await dispatch(
    343      getMappedScopesForLocation(breakpoint.location)
    344    );
    345    if (!mappedScopes) {
    346      return breakpoint;
    347    }
    348    const { mappings } = mappedScopes;
    349 
    350    if (options.condition) {
    351      options.condition = await updateExpression(
    352        parserWorker,
    353        mappings,
    354        options.condition
    355      );
    356    }
    357    if (options.logValue) {
    358      options.logValue = await updateExpression(
    359        parserWorker,
    360        mappings,
    361        options.logValue
    362      );
    363    }
    364 
    365    // As we waited for lots of asynchronous operations,
    366    // verify that the breakpoint is still valid before
    367    // trying to set/update it on the server.
    368    validateBreakpoint(getState(), breakpoint);
    369 
    370    return { ...breakpoint, options };
    371  };
    372 }