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 }