commands.js (14585B)
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 { createFrame } from "./create"; 6 import { makeBreakpointServerLocationId } from "../../utils/breakpoint/index"; 7 8 import * as objectInspector from "resource://devtools/client/shared/components/object-inspector/index.js"; 9 10 let commands; 11 let breakpoints; 12 13 // The maximal number of stackframes to retrieve when pausing 14 const CALL_STACK_PAGE_SIZE = 1000; 15 16 function setupCommands(innerCommands) { 17 commands = innerCommands; 18 breakpoints = {}; 19 } 20 21 function currentTarget() { 22 return commands.targetCommand.targetFront; 23 } 24 25 function currentThreadFront() { 26 return currentTarget().threadFront; 27 } 28 29 /** 30 * Create an object front for the passed grip 31 * 32 * @param {object} grip 33 * @param {object} frame: An optional frame that will manage the created object front. 34 * if not passed, the current thread front will manage the object. 35 * @returns {ObjectFront} 36 */ 37 function createObjectFront(grip, frame) { 38 if (!grip.actor) { 39 throw new Error("Actor is missing"); 40 } 41 const threadFront = frame?.thread 42 ? lookupThreadFront(frame.thread) 43 : currentThreadFront(); 44 const frameFront = frame ? threadFront.getActorByID(frame.id) : null; 45 return commands.client.createObjectFront(grip, threadFront, frameFront); 46 } 47 48 async function loadObjectProperties(root, threadActorID) { 49 const { utils } = objectInspector; 50 const properties = await utils.loadProperties.loadItemProperties( 51 root, 52 commands.client, 53 undefined, 54 threadActorID 55 ); 56 return utils.node.getChildren({ 57 item: root, 58 loadedProperties: new Map([[root.path, properties]]), 59 }); 60 } 61 62 function releaseActor(actor) { 63 if (!actor) { 64 return Promise.resolve(); 65 } 66 const objFront = commands.client.getFrontByID(actor); 67 68 if (!objFront) { 69 return Promise.resolve(); 70 } 71 72 return objFront.release().catch(() => {}); 73 } 74 75 function lookupTarget(thread) { 76 if (thread == currentThreadFront().actor) { 77 return currentTarget(); 78 } 79 80 const targets = commands.targetCommand.getAllTargets( 81 commands.targetCommand.ALL_TYPES 82 ); 83 return targets.find(target => target.targetForm.threadActor == thread); 84 } 85 86 function lookupThreadFront(thread) { 87 const target = lookupTarget(thread); 88 return target.threadFront; 89 } 90 91 function listThreadFronts() { 92 const targets = commands.targetCommand.getAllTargets( 93 commands.targetCommand.ALL_TYPES 94 ); 95 return targets.map(target => target.threadFront).filter(front => !!front); 96 } 97 98 function forEachThread(iteratee) { 99 // We have to be careful here to atomically initiate the operation on every 100 // thread, with no intervening await. Otherwise, other code could run and 101 // trigger additional thread operations. Requests on server threads will 102 // resolve in FIFO order, and this could result in client and server state 103 // going out of sync. 104 105 const promises = listThreadFronts().map( 106 // If a thread shuts down while sending the message then it will 107 // throw. Ignore these exceptions. 108 t => iteratee(t).catch(e => console.log(e)) 109 ); 110 111 return Promise.all(promises); 112 } 113 114 function resume(thread) { 115 return lookupThreadFront(thread).resume(); 116 } 117 118 function stepIn(thread, frameId) { 119 return lookupThreadFront(thread).stepIn(frameId); 120 } 121 122 function stepOver(thread, frameId) { 123 return lookupThreadFront(thread).stepOver(frameId); 124 } 125 126 function stepOut(thread, frameId) { 127 return lookupThreadFront(thread).stepOut(frameId); 128 } 129 130 function restart(thread, frameId) { 131 return lookupThreadFront(thread).restart(frameId); 132 } 133 134 function breakOnNext(thread) { 135 return lookupThreadFront(thread).breakOnNext(); 136 } 137 138 async function sourceContents({ actor, thread }) { 139 const sourceThreadFront = lookupThreadFront(thread); 140 const sourceFront = sourceThreadFront.source({ actor }); 141 const { source, contentType } = await sourceFront.source(); 142 return { source, contentType }; 143 } 144 145 async function setXHRBreakpoint(path, method) { 146 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 147 if (!hasWatcherSupport) { 148 // Without watcher support, forward setXHRBreakpoint to all threads. 149 await forEachThread(thread => thread.setXHRBreakpoint(path, method)); 150 return; 151 } 152 const breakpointsFront = 153 await commands.targetCommand.watcherFront.getBreakpointListActor(); 154 await breakpointsFront.setXHRBreakpoint(path, method); 155 } 156 157 async function removeXHRBreakpoint(path, method) { 158 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 159 if (!hasWatcherSupport) { 160 // Without watcher support, forward removeXHRBreakpoint to all threads. 161 await forEachThread(thread => thread.removeXHRBreakpoint(path, method)); 162 return; 163 } 164 const breakpointsFront = 165 await commands.targetCommand.watcherFront.getBreakpointListActor(); 166 await breakpointsFront.removeXHRBreakpoint(path, method); 167 } 168 169 export function toggleJavaScriptEnabled(enabled) { 170 return commands.targetConfigurationCommand.updateConfiguration({ 171 javascriptEnabled: enabled, 172 }); 173 } 174 175 async function addWatchpoint(object, property, label, watchpointType) { 176 if (!currentTarget().getTrait("watchpoints")) { 177 return; 178 } 179 const objectFront = createObjectFront(object); 180 await objectFront.addWatchpoint(property, label, watchpointType); 181 } 182 183 async function removeWatchpoint(object, property) { 184 if (!currentTarget().getTrait("watchpoints")) { 185 return; 186 } 187 const objectFront = createObjectFront(object); 188 await objectFront.removeWatchpoint(property); 189 } 190 191 function hasBreakpoint(location) { 192 return !!breakpoints[makeBreakpointServerLocationId(location)]; 193 } 194 195 function getServerBreakpointsList() { 196 return Object.values(breakpoints); 197 } 198 199 async function setBreakpoint(location, options) { 200 const breakpoint = breakpoints[makeBreakpointServerLocationId(location)]; 201 if ( 202 breakpoint && 203 JSON.stringify(breakpoint.options) == JSON.stringify(options) 204 ) { 205 return null; 206 } 207 breakpoints[makeBreakpointServerLocationId(location)] = { location, options }; 208 209 // Map frontend options to a more restricted subset of what 210 // the server supports. For example frontend uses `hidden` attribute 211 // which isn't meant to be passed to the server. 212 // (note that protocol.js specification isn't enough to filter attributes, 213 // all primitive attributes will be passed as-is) 214 const serverOptions = { 215 condition: options.condition, 216 logValue: options.logValue, 217 showStacktrace: options.showStacktrace, 218 }; 219 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 220 if (!hasWatcherSupport) { 221 // Without watcher support, unconditionally forward setBreakpoint to all threads. 222 return forEachThread(async thread => 223 thread.setBreakpoint(location, serverOptions) 224 ); 225 } 226 const breakpointsFront = 227 await commands.targetCommand.watcherFront.getBreakpointListActor(); 228 await breakpointsFront.setBreakpoint(location, serverOptions); 229 230 // Call setBreakpoint for threads linked to targets 231 // not managed by the watcher. 232 return forEachThread(async thread => { 233 if ( 234 !commands.targetCommand.hasTargetWatcherSupport( 235 thread.targetFront.targetType 236 ) 237 ) { 238 return thread.setBreakpoint(location, serverOptions); 239 } 240 241 return Promise.resolve(); 242 }); 243 } 244 245 async function removeBreakpoint(location) { 246 delete breakpoints[makeBreakpointServerLocationId(location)]; 247 248 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 249 if (!hasWatcherSupport) { 250 // Without watcher support, unconditionally forward removeBreakpoint to all threads. 251 return forEachThread(async thread => thread.removeBreakpoint(location)); 252 } 253 const breakpointsFront = 254 await commands.targetCommand.watcherFront.getBreakpointListActor(); 255 await breakpointsFront.removeBreakpoint(location); 256 257 // Call removeBreakpoint for threads linked to targets 258 // not managed by the watcher. 259 return forEachThread(async thread => { 260 if ( 261 !commands.targetCommand.hasTargetWatcherSupport( 262 thread.targetFront.targetType 263 ) 264 ) { 265 return thread.removeBreakpoint(location); 266 } 267 268 return Promise.resolve(); 269 }); 270 } 271 272 async function evaluateExpressions(expressions, options) { 273 return Promise.all( 274 expressions.map(expression => evaluate(expression, options)) 275 ); 276 } 277 278 /** 279 * Evaluate some JS expression in a given thread. 280 * 281 * @param {string} expression 282 * @param {object} options 283 * @param {string} options.frameId 284 * Optional frame actor ID into which the expression should be evaluated. 285 * @param {string} options.threadId 286 * Optional thread actor ID into which the expression should be evaluated. 287 * @param {string} options.selectedNodeActor 288 * Optional node actor ID which related to "$0" in the evaluated expression. 289 * @param {boolean} options.evalInTracer 290 * To be set to true, if the object actors created during the evaluation 291 * should be registered in the tracer actor Pool. 292 * @return {object} 293 * See ScriptCommand.execute JS Doc. 294 */ 295 async function evaluate( 296 expression, 297 { frameId, threadId, selectedNodeActor, evalInTracer } = {} 298 ) { 299 if (!currentTarget() || !expression) { 300 return { result: null }; 301 } 302 303 const selectedTargetFront = threadId ? lookupTarget(threadId) : null; 304 305 return commands.scriptCommand.execute(expression, { 306 frameActor: frameId, 307 selectedTargetFront, 308 disableBreaks: true, 309 selectedNodeActor, 310 evalInTracer, 311 }); 312 } 313 314 async function autocomplete(input, cursor, frameId) { 315 if (!currentTarget() || !input) { 316 return {}; 317 } 318 const consoleFront = await currentTarget().getFront("console"); 319 if (!consoleFront) { 320 return {}; 321 } 322 323 return new Promise(resolve => { 324 consoleFront.autocomplete( 325 input, 326 cursor, 327 result => resolve(result), 328 frameId 329 ); 330 }); 331 } 332 333 async function getFrames(thread) { 334 const threadFront = lookupThreadFront(thread); 335 const response = await threadFront.getFrames(0, CALL_STACK_PAGE_SIZE); 336 337 return Promise.all( 338 response.frames.map((frame, i) => createFrame(thread, frame, i)) 339 ); 340 } 341 342 async function getFrameScopes(frame) { 343 const frameFront = lookupThreadFront(frame.thread).getActorByID(frame.id); 344 return frameFront.getEnvironment(); 345 } 346 347 async function pauseOnDebuggerStatement(shouldPauseOnDebuggerStatement) { 348 await commands.threadConfigurationCommand.updateConfiguration({ 349 shouldPauseOnDebuggerStatement, 350 }); 351 } 352 353 async function pauseOnExceptions( 354 shouldPauseOnExceptions, 355 shouldPauseOnCaughtExceptions 356 ) { 357 await commands.threadConfigurationCommand.updateConfiguration({ 358 pauseOnExceptions: shouldPauseOnExceptions, 359 ignoreCaughtExceptions: !shouldPauseOnCaughtExceptions, 360 }); 361 } 362 363 async function blackBox(sourceActor, shouldBlackBox, ranges) { 364 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 365 if (hasWatcherSupport) { 366 const blackboxingFront = 367 await commands.targetCommand.watcherFront.getBlackboxingActor(); 368 if (shouldBlackBox) { 369 await blackboxingFront.blackbox(sourceActor.url, ranges); 370 } else { 371 await blackboxingFront.unblackbox(sourceActor.url, ranges); 372 } 373 } else { 374 const sourceFront = currentThreadFront().source({ 375 actor: sourceActor.actor, 376 }); 377 // If there are no ranges, the whole source is being blackboxed 378 if (!ranges.length) { 379 await toggleBlackBoxSourceFront(sourceFront, shouldBlackBox); 380 return; 381 } 382 // Blackbox the specific ranges 383 for (const range of ranges) { 384 await toggleBlackBoxSourceFront(sourceFront, shouldBlackBox, range); 385 } 386 } 387 } 388 389 async function toggleBlackBoxSourceFront(sourceFront, shouldBlackBox, range) { 390 if (shouldBlackBox) { 391 await sourceFront.blackBox(range); 392 } else { 393 await sourceFront.unblackBox(range); 394 } 395 } 396 397 async function setSkipPausing(shouldSkip) { 398 await commands.threadConfigurationCommand.updateConfiguration({ 399 skipBreakpoints: shouldSkip, 400 }); 401 } 402 403 async function setEventListenerBreakpoints(ids) { 404 const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport(); 405 if (!hasWatcherSupport) { 406 await forEachThread(thread => thread.setActiveEventBreakpoints(ids)); 407 return; 408 } 409 const breakpointListFront = 410 await commands.targetCommand.watcherFront.getBreakpointListActor(); 411 await breakpointListFront.setActiveEventBreakpoints(ids); 412 } 413 414 async function getEventListenerBreakpointTypes() { 415 return currentThreadFront().getAvailableEventBreakpoints(); 416 } 417 418 async function toggleEventLogging(logEventBreakpoints) { 419 await commands.threadConfigurationCommand.updateConfiguration({ 420 logEventBreakpoints, 421 }); 422 } 423 424 function getMainThread() { 425 return currentThreadFront().actor; 426 } 427 428 async function getSourceActorBreakpointPositions({ thread, actor }, range) { 429 const sourceThreadFront = lookupThreadFront(thread); 430 const sourceFront = sourceThreadFront.source({ actor }); 431 return sourceFront.getBreakpointPositionsCompressed(range); 432 } 433 434 async function getSourceActorBreakableLines({ thread, actor }) { 435 let actorLines = []; 436 try { 437 const sourceThreadFront = lookupThreadFront(thread); 438 const sourceFront = sourceThreadFront.source({ actor }); 439 actorLines = await sourceFront.getBreakableLines(); 440 } catch (e) { 441 // Exceptions could be due to the target thread being shut down. 442 console.warn(`getSourceActorBreakableLines failed: ${e}`); 443 } 444 445 return actorLines; 446 } 447 448 function getFrontByID(actorID) { 449 return commands.client.getFrontByID(actorID); 450 } 451 452 function fetchAncestorFramePositions(index) { 453 currentThreadFront().fetchAncestorFramePositions(index); 454 } 455 456 const clientCommands = { 457 autocomplete, 458 blackBox, 459 createObjectFront, 460 loadObjectProperties, 461 releaseActor, 462 resume, 463 stepIn, 464 stepOut, 465 stepOver, 466 restart, 467 breakOnNext, 468 sourceContents, 469 getSourceActorBreakpointPositions, 470 getSourceActorBreakableLines, 471 hasBreakpoint, 472 getServerBreakpointsList, 473 setBreakpoint, 474 setXHRBreakpoint, 475 removeXHRBreakpoint, 476 addWatchpoint, 477 removeWatchpoint, 478 removeBreakpoint, 479 evaluate, 480 evaluateExpressions, 481 getFrameScopes, 482 getFrames, 483 pauseOnDebuggerStatement, 484 pauseOnExceptions, 485 toggleEventLogging, 486 getMainThread, 487 setSkipPausing, 488 setEventListenerBreakpoints, 489 getEventListenerBreakpointTypes, 490 getFrontByID, 491 fetchAncestorFramePositions, 492 toggleJavaScriptEnabled, 493 }; 494 495 export { setupCommands, clientCommands };