tor-browser

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

pause.js (12019B)


      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 /* eslint complexity: ["error", 38]*/
      6 
      7 /**
      8 * Pause reducer
      9 *
     10 * @module reducers/pause
     11 */
     12 
     13 import { prefs } from "../utils/prefs";
     14 
     15 // Pause state associated with an individual thread.
     16 
     17 // Pause state describing all threads.
     18 
     19 export function initialPauseState(thread = "UnknownThread") {
     20  return {
     21    cx: {
     22      navigateCounter: 0,
     23    },
     24    // This `threadcx` is the `cx` variable we pass around in components and actions.
     25    // This is pulled via getThreadContext().
     26    // This stores information about the currently selected thread and its paused state.
     27    threadcx: {
     28      navigateCounter: 0,
     29      thread,
     30      pauseCounter: 0,
     31    },
     32    threads: {},
     33    skipPausing: prefs.skipPausing,
     34    mapScopes: prefs.mapScopes,
     35    shouldPauseOnDebuggerStatement: prefs.pauseOnDebuggerStatement,
     36    shouldPauseOnExceptions: prefs.pauseOnExceptions,
     37    shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions,
     38  };
     39 }
     40 
     41 const resumedPauseState = {
     42  isPaused: false,
     43  frames: null,
     44  framesLoading: false,
     45  frameScopes: {
     46    generated: {},
     47    original: {},
     48    mappings: {},
     49  },
     50  selectedFrameId: null,
     51  why: null,
     52  inlinePreview: {},
     53 };
     54 
     55 const createInitialPauseState = () => ({
     56  ...resumedPauseState,
     57  isWaitingOnBreak: false,
     58  command: null,
     59  previousLocation: null,
     60  expandedScopes: new Set(),
     61  lastExpandedScopes: [],
     62  shouldBreakpointsPaneOpenOnPause: false,
     63 });
     64 
     65 export function getThreadPauseState(state, thread) {
     66  // Thread state is lazily initialized so that we don't have to keep track of
     67  // the current set of worker threads.
     68  return state.threads[thread] || createInitialPauseState();
     69 }
     70 
     71 function update(state = initialPauseState(), action) {
     72  // All the actions updating pause state must pass an object which designate
     73  // the related thread.
     74  const getActionThread = () => {
     75    const thread =
     76      action.thread || action.selectedFrame?.thread || action.frame?.thread;
     77    if (!thread) {
     78      throw new Error(`Missing thread in action ${action.type}`);
     79    }
     80    return thread;
     81  };
     82 
     83  // `threadState` and `updateThreadState` help easily get and update
     84  // the pause state for a given thread.
     85  const threadState = () => {
     86    return getThreadPauseState(state, getActionThread());
     87  };
     88  const updateThreadState = newThreadState => {
     89    return {
     90      ...state,
     91      threads: {
     92        ...state.threads,
     93        [getActionThread()]: { ...threadState(), ...newThreadState },
     94      },
     95    };
     96  };
     97 
     98  switch (action.type) {
     99    case "SELECT_THREAD": {
    100      // Ignore the action if the related thread doesn't exist.
    101      if (!state.threads[action.thread]) {
    102        console.warn(
    103          `Trying to select a destroyed or non-existent thread '${action.thread}'`
    104        );
    105        return state;
    106      }
    107 
    108      return {
    109        ...state,
    110        threadcx: {
    111          ...state.threadcx,
    112          thread: action.thread,
    113          pauseCounter: state.threadcx.pauseCounter + 1,
    114        },
    115      };
    116    }
    117 
    118    case "INSERT_THREAD": {
    119      // When navigating to a new location,
    120      // we receive NAVIGATE early, which clear things
    121      // then we have REMOVE_THREAD of the previous thread.
    122      // INSERT_THREAD will be the very first event with the new thread actor ID.
    123      // Automatically select the new top level thread.
    124      if (action.newThread.isTopLevel) {
    125        return {
    126          ...state,
    127          threadcx: {
    128            ...state.threadcx,
    129            thread: action.newThread.actor,
    130            pauseCounter: state.threadcx.pauseCounter + 1,
    131          },
    132          threads: {
    133            ...state.threads,
    134            [action.newThread.actor]: createInitialPauseState(),
    135          },
    136        };
    137      }
    138 
    139      return {
    140        ...state,
    141        threads: {
    142          ...state.threads,
    143          [action.newThread.actor]: createInitialPauseState(),
    144        },
    145      };
    146    }
    147 
    148    case "REMOVE_THREAD": {
    149      const { threadActorID } = action;
    150      if (
    151        threadActorID in state.threads ||
    152        threadActorID == state.threadcx.thread
    153      ) {
    154        // Remove the thread from the cached list
    155        const threads = { ...state.threads };
    156        delete threads[threadActorID];
    157        let threadcx = state.threadcx;
    158 
    159        // And also switch to another thread if this was the currently selected one.
    160        // As we don't store thread objects in this reducer, and only store thread actor IDs,
    161        // we can't try to find the top level thread. So we pick the first available thread,
    162        // and hope that's the top level one.
    163        if (state.threadcx.thread == threadActorID) {
    164          threadcx = {
    165            ...threadcx,
    166            thread: Object.keys(threads)[0],
    167            pauseCounter: threadcx.pauseCounter + 1,
    168          };
    169        }
    170        return {
    171          ...state,
    172          threadcx,
    173          threads,
    174        };
    175      }
    176      break;
    177    }
    178 
    179    case "PAUSED": {
    180      const { thread, topFrame, why } = action;
    181      state = {
    182        ...state,
    183        threadcx: {
    184          ...state.threadcx,
    185          pauseCounter: state.threadcx.pauseCounter + 1,
    186          thread,
    187        },
    188      };
    189 
    190      return updateThreadState({
    191        isWaitingOnBreak: false,
    192        selectedFrameId: topFrame.id,
    193        isPaused: true,
    194        // On pause, we only receive the top frame, all subsequent ones
    195        // will be asynchronously populated via `fetchFrames` action
    196        frames: [topFrame],
    197        framesLoading: true,
    198        frameScopes: { ...resumedPauseState.frameScopes },
    199        why,
    200        shouldBreakpointsPaneOpenOnPause: why.type === "breakpoint",
    201      });
    202    }
    203 
    204    case "FETCHED_FRAMES": {
    205      const { frames } = action;
    206 
    207      // We typically receive a PAUSED action before this one,
    208      // with only the first frame. Here, we avoid replacing it
    209      // with a copy of it in order to avoid triggerring selectors
    210      // uncessarily
    211      // (note that in jest, action's frames might be empty)
    212      // (and if we resume in between PAUSED and FETCHED_FRAMES
    213      //  threadState().frames might be null)
    214      if (threadState().frames) {
    215        const previousFirstFrame = threadState().frames[0];
    216        if (previousFirstFrame.id == frames[0]?.id) {
    217          frames.splice(0, 1, previousFirstFrame);
    218        }
    219      }
    220      return updateThreadState({ frames, framesLoading: false });
    221    }
    222 
    223    case "MAP_FRAMES": {
    224      const { selectedFrameId, frames } = action;
    225      return updateThreadState({ frames, selectedFrameId });
    226    }
    227 
    228    case "UPDATE_FRAMES": {
    229      const { frames } = action;
    230      return updateThreadState({ frames });
    231    }
    232 
    233    case "ADD_SCOPES": {
    234      const { status, value } = action;
    235      const selectedFrameId = action.selectedFrame.id;
    236 
    237      const generated = {
    238        ...threadState().frameScopes.generated,
    239        [selectedFrameId]: {
    240          pending: status !== "done",
    241          // Environment Scope information from the platform.
    242          // See https://searchfox.org/mozilla-central/rev/b0e8e4ceb46cb3339cdcb90310fcc161ef4b9e3e/devtools/server/actors/environment.js#42-81
    243          scope: value,
    244        },
    245      };
    246 
    247      return updateThreadState({
    248        frameScopes: {
    249          ...threadState().frameScopes,
    250          generated,
    251        },
    252      });
    253    }
    254 
    255    case "MAP_SCOPES": {
    256      const { status, value } = action;
    257      const selectedFrameId = action.selectedFrame.id;
    258 
    259      const original = {
    260        ...threadState().frameScopes.original,
    261        [selectedFrameId]: {
    262          pending: status !== "done",
    263          scope: value?.scope,
    264        },
    265      };
    266 
    267      const mappings = {
    268        ...threadState().frameScopes.mappings,
    269        [selectedFrameId]: value?.mappings,
    270      };
    271 
    272      return updateThreadState({
    273        frameScopes: {
    274          ...threadState().frameScopes,
    275          original,
    276          mappings,
    277        },
    278      });
    279    }
    280 
    281    case "BREAK_ON_NEXT":
    282      return updateThreadState({ isWaitingOnBreak: true });
    283 
    284    case "SELECT_FRAME":
    285      return updateThreadState({ selectedFrameId: action.frame.id });
    286 
    287    case "PAUSE_ON_DEBUGGER_STATEMENT": {
    288      const { shouldPauseOnDebuggerStatement } = action;
    289 
    290      prefs.pauseOnDebuggerStatement = shouldPauseOnDebuggerStatement;
    291 
    292      return {
    293        ...state,
    294        shouldPauseOnDebuggerStatement,
    295      };
    296    }
    297 
    298    case "PAUSE_ON_EXCEPTIONS": {
    299      const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action;
    300 
    301      prefs.pauseOnExceptions = shouldPauseOnExceptions;
    302      prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions;
    303 
    304      // Preserving for the old debugger
    305      prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions;
    306 
    307      return {
    308        ...state,
    309        shouldPauseOnExceptions,
    310        shouldPauseOnCaughtExceptions,
    311      };
    312    }
    313 
    314    case "COMMAND":
    315      if (action.status === "start") {
    316        return updateThreadState({
    317          ...resumedPauseState,
    318          command: action.command,
    319          previousLocation: getPauseLocation(threadState(), action),
    320        });
    321      }
    322      return updateThreadState({ command: null });
    323 
    324    case "RESUME": {
    325      if (action.thread == state.threadcx.thread) {
    326        state = {
    327          ...state,
    328          threadcx: {
    329            ...state.threadcx,
    330            pauseCounter: state.threadcx.pauseCounter + 1,
    331          },
    332        };
    333      }
    334 
    335      return updateThreadState({
    336        ...resumedPauseState,
    337        expandedScopes: new Set(),
    338        lastExpandedScopes: [...threadState().expandedScopes],
    339        shouldBreakpointsPaneOpenOnPause: false,
    340      });
    341    }
    342 
    343    case "EVALUATE_EXPRESSION":
    344      return updateThreadState({
    345        command: action.status === "start" ? "expression" : null,
    346      });
    347 
    348    case "NAVIGATE": {
    349      const navigateCounter = state.cx.navigateCounter + 1;
    350      return {
    351        ...state,
    352        cx: {
    353          navigateCounter,
    354        },
    355        threadcx: {
    356          navigateCounter,
    357          thread: action.mainThread.actor,
    358          pauseCounter: 0,
    359        },
    360        threads: {
    361          ...state.threads,
    362          [action.mainThread.actor]: {
    363            ...getThreadPauseState(state, action.mainThread.actor),
    364            ...resumedPauseState,
    365          },
    366        },
    367      };
    368    }
    369 
    370    case "TOGGLE_SKIP_PAUSING": {
    371      const { skipPausing } = action;
    372      prefs.skipPausing = skipPausing;
    373 
    374      return { ...state, skipPausing };
    375    }
    376 
    377    case "TOGGLE_MAP_SCOPES": {
    378      const { mapScopes } = action;
    379      prefs.mapScopes = mapScopes;
    380      return { ...state, mapScopes };
    381    }
    382 
    383    case "SET_EXPANDED_SCOPE": {
    384      const { path, expanded } = action;
    385      const expandedScopes = new Set(threadState().expandedScopes);
    386      if (expanded) {
    387        expandedScopes.add(path);
    388      } else {
    389        expandedScopes.delete(path);
    390      }
    391      return updateThreadState({ expandedScopes });
    392    }
    393 
    394    case "ADD_INLINE_PREVIEW": {
    395      const { selectedFrame, previews } = action;
    396      const selectedFrameId = selectedFrame.id;
    397 
    398      return updateThreadState({
    399        inlinePreview: {
    400          ...threadState().inlinePreview,
    401          [selectedFrameId]: previews,
    402        },
    403      });
    404    }
    405 
    406    case "RESET_BREAKPOINTS_PANE_STATE": {
    407      return updateThreadState({
    408        ...threadState(),
    409        shouldBreakpointsPaneOpenOnPause: false,
    410      });
    411    }
    412  }
    413 
    414  return state;
    415 }
    416 
    417 function getPauseLocation(state, action) {
    418  const { frames, previousLocation } = state;
    419 
    420  // NOTE: We store the previous location so that we ensure that we
    421  // do not stop at the same location twice when we step over.
    422  if (action.command !== "stepOver") {
    423    return null;
    424  }
    425 
    426  const frame = frames?.[0];
    427  if (!frame) {
    428    return previousLocation;
    429  }
    430 
    431  return {
    432    location: frame.location,
    433    generatedLocation: frame.generatedLocation,
    434  };
    435 }
    436 
    437 export default update;