input.js (15527B)
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 "use strict"; 6 7 const { 8 Utils: WebConsoleUtils, 9 } = require("resource://devtools/client/webconsole/utils.js"); 10 const { 11 EVALUATE_EXPRESSION, 12 SET_TERMINAL_INPUT, 13 SET_TERMINAL_EAGER_RESULT, 14 EDITOR_PRETTY_PRINT, 15 HELP_URL, 16 } = require("resource://devtools/client/webconsole/constants.js"); 17 const { 18 getAllPrefs, 19 } = require("resource://devtools/client/webconsole/selectors/prefs.js"); 20 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js"); 21 const l10n = require("resource://devtools/client/webconsole/utils/l10n.js"); 22 23 loader.lazyServiceGetter( 24 this, 25 "clipboardHelper", 26 "@mozilla.org/widget/clipboardhelper;1", 27 "nsIClipboardHelper" 28 ); 29 loader.lazyRequireGetter( 30 this, 31 "messagesActions", 32 "resource://devtools/client/webconsole/actions/messages.js" 33 ); 34 loader.lazyRequireGetter( 35 this, 36 "historyActions", 37 "resource://devtools/client/webconsole/actions/history.js" 38 ); 39 loader.lazyRequireGetter( 40 this, 41 "ConsoleCommand", 42 "resource://devtools/client/webconsole/types.js", 43 true 44 ); 45 loader.lazyRequireGetter( 46 this, 47 "netmonitorBlockingActions", 48 "resource://devtools/client/netmonitor/src/actions/request-blocking.js" 49 ); 50 51 loader.lazyRequireGetter( 52 this, 53 ["saveScreenshot", "captureAndSaveScreenshot"], 54 "resource://devtools/client/shared/screenshot.js", 55 true 56 ); 57 loader.lazyRequireGetter( 58 this, 59 "createSimpleTableMessage", 60 "resource://devtools/client/webconsole/utils/messages.js", 61 true 62 ); 63 loader.lazyRequireGetter( 64 this, 65 "getSelectedTarget", 66 "resource://devtools/shared/commands/target/selectors/targets.js", 67 true 68 ); 69 70 async function getMappedExpression(hud, expression) { 71 let mapResult; 72 try { 73 mapResult = await hud.getMappedExpression(expression); 74 } catch (e) { 75 console.warn("Error when calling getMappedExpression", e); 76 } 77 78 let mapped = null; 79 if (mapResult) { 80 ({ expression, mapped } = mapResult); 81 } 82 return { expression, mapped }; 83 } 84 85 function evaluateExpression(expression, from = "input") { 86 return async ({ dispatch, webConsoleUI, hud, commands }) => { 87 if (!expression) { 88 expression = hud.getInputSelection() || hud.getInputValue(); 89 } 90 if (!expression) { 91 return null; 92 } 93 94 // We use the messages action as it's doing additional transformation on the message. 95 const { messages } = dispatch( 96 messagesActions.messagesAdd([ 97 new ConsoleCommand({ 98 messageText: expression, 99 timeStamp: Date.now(), 100 }), 101 ]) 102 ); 103 const [consoleCommandMessage] = messages; 104 105 dispatch({ 106 type: EVALUATE_EXPRESSION, 107 expression, 108 from, 109 }); 110 111 WebConsoleUtils.usageCount++; 112 113 let mapped; 114 ({ expression, mapped } = await getMappedExpression(hud, expression)); 115 116 // Even if the evaluation fails, 117 // we still need to pass the error response to onExpressionEvaluated. 118 const onSettled = res => res; 119 120 const response = await commands.scriptCommand 121 .execute(expression, { 122 frameActor: hud.getSelectedFrameActorID(), 123 selectedNodeActor: webConsoleUI.getSelectedNodeActorID(), 124 selectedTargetFront: getSelectedTarget( 125 webConsoleUI.hud.commands.targetCommand.store.getState() 126 ), 127 mapped, 128 // Allow breakpoints to be triggerred and the evaluated source to be shown in debugger UI 129 disableBreaks: false, 130 }) 131 .then(onSettled, onSettled); 132 133 const serverConsoleCommandTimestamp = response.startTime; 134 135 // In case of remote debugging, it might happen that the debuggee page does not have 136 // the exact same clock time as the client. This could cause some ordering issues 137 // where the result message is displayed *before* the expression that lead to it. 138 if ( 139 serverConsoleCommandTimestamp && 140 consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp 141 ) { 142 // If we're in such case, we remove the original command message, and add it again, 143 // with the timestamp coming from the server. 144 dispatch(messagesActions.messageRemove(consoleCommandMessage.id)); 145 dispatch( 146 messagesActions.messagesAdd([ 147 new ConsoleCommand({ 148 messageText: expression, 149 timeStamp: serverConsoleCommandTimestamp, 150 }), 151 ]) 152 ); 153 } 154 155 return dispatch(onExpressionEvaluated(response)); 156 }; 157 } 158 159 /** 160 * The JavaScript evaluation response handler. 161 * 162 * @private 163 * @param {object} response 164 * The message received from the server. 165 */ 166 function onExpressionEvaluated(response) { 167 return async ({ dispatch, webConsoleUI }) => { 168 if (response.error) { 169 console.error(`Evaluation error`, response.error, ": ", response.message); 170 return; 171 } 172 173 // If the evaluation was a top-level await expression that was rejected, there will 174 // be an uncaught exception reported, so we don't need to do anything. 175 if (response.topLevelAwaitRejected === true) { 176 return; 177 } 178 179 if (!response.helperResult) { 180 webConsoleUI.wrapper.dispatchMessageAdd(response); 181 return; 182 } 183 184 await dispatch(handleHelperResult(response)); 185 }; 186 } 187 188 function handleHelperResult(response) { 189 // eslint-disable-next-line complexity 190 return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => { 191 const { result, helperResult } = response; 192 const helperHasRawOutput = !!helperResult?.rawOutput; 193 194 if (helperResult?.type) { 195 switch (helperResult.type) { 196 case "exception": 197 dispatch( 198 messagesActions.messagesAdd([ 199 { 200 level: "error", 201 arguments: [helperResult.message], 202 chromeContext: true, 203 resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE, 204 }, 205 ]) 206 ); 207 break; 208 case "clearOutput": 209 dispatch(messagesActions.messagesClear()); 210 break; 211 case "clearHistory": 212 dispatch(historyActions.clearHistory()); 213 break; 214 case "historyOutput": { 215 const history = getState().history.entries || []; 216 const columns = new Map([ 217 ["_index", "(index)"], 218 ["expression", "Expressions"], 219 ]); 220 dispatch( 221 messagesActions.messagesAdd([ 222 { 223 ...createSimpleTableMessage( 224 columns, 225 history.map((expression, index) => { 226 return { _index: index, expression }; 227 }) 228 ), 229 }, 230 ]) 231 ); 232 break; 233 } 234 case "inspectObject": { 235 const objectActor = helperResult.object; 236 if (hud.toolbox && !helperResult.forceExpandInConsole) { 237 hud.toolbox.inspectObjectActor(objectActor); 238 } else { 239 webConsoleUI.inspectObjectActor(objectActor); 240 } 241 break; 242 } 243 case "help": 244 hud.openLink(HELP_URL); 245 break; 246 case "copyValueToClipboard": 247 clipboardHelper.copyString(helperResult.value); 248 dispatch( 249 messagesActions.messagesAdd([ 250 { 251 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, 252 message: l10n.getStr( 253 "webconsole.message.commands.copyValueToClipboard" 254 ), 255 }, 256 ]) 257 ); 258 break; 259 case "screenshotOutput": { 260 const { args, value } = helperResult; 261 const targetFront = 262 getSelectedTarget(hud.commands.targetCommand.store.getState()) || 263 hud.currentTarget; 264 let screenshotMessages; 265 266 // @backward-compat { version 87 } The screenshot-content actor isn't available 267 // in older server. 268 // With an old server, the console actor captures the screenshot when handling 269 // the command, and send it to the client which only needs to save it to a file. 270 // With a new server, the server simply acknowledges the command, 271 // and the client will drive the whole screenshot process (capture and save). 272 if (targetFront.hasActor("screenshotContent")) { 273 screenshotMessages = await captureAndSaveScreenshot( 274 targetFront, 275 webConsoleUI.getPanelWindow(), 276 args 277 ); 278 } else { 279 screenshotMessages = await saveScreenshot( 280 webConsoleUI.getPanelWindow(), 281 args, 282 value 283 ); 284 } 285 286 if (screenshotMessages && screenshotMessages.length) { 287 dispatch( 288 messagesActions.messagesAdd( 289 screenshotMessages.map(message => ({ 290 level: message.level || "log", 291 arguments: [message.text], 292 chromeContext: true, 293 resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE, 294 })) 295 ) 296 ); 297 } 298 break; 299 } 300 case "blockURL": { 301 const blockURL = helperResult.args.url; 302 // The console actor isn't able to block the request as the console actor runs in the content 303 // process, while the request has to be blocked from the parent process. 304 // Then, calling the Netmonitor action will only update the visual state of the Netmonitor, 305 // but we also have to block the request via the NetworkParentActor. 306 await hud.commands.networkCommand.blockRequestForUrl(blockURL); 307 toolbox 308 .getPanel("netmonitor") 309 ?.panelWin.store.dispatch( 310 netmonitorBlockingActions.addBlockedUrl(blockURL) 311 ); 312 313 dispatch( 314 messagesActions.messagesAdd([ 315 { 316 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, 317 message: l10n.getFormatStr( 318 "webconsole.message.commands.blockedURL", 319 [blockURL] 320 ), 321 }, 322 ]) 323 ); 324 break; 325 } 326 case "unblockURL": { 327 const unblockURL = helperResult.args.url; 328 await hud.commands.networkCommand.unblockRequestForUrl(unblockURL); 329 toolbox 330 .getPanel("netmonitor") 331 ?.panelWin.store.dispatch( 332 netmonitorBlockingActions.removeBlockedUrl(unblockURL) 333 ); 334 335 dispatch( 336 messagesActions.messagesAdd([ 337 { 338 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, 339 message: l10n.getFormatStr( 340 "webconsole.message.commands.unblockedURL", 341 [unblockURL] 342 ), 343 }, 344 ]) 345 ); 346 // early return as we already dispatched necessary messages. 347 return; 348 } 349 350 // Sent when using ":command --help or :command --usage" 351 // to help discover command arguments. 352 // 353 // The remote runtime will tell us about the usage as it may 354 // be different from the client one. 355 case "usage": 356 dispatch( 357 messagesActions.messagesAdd([ 358 { 359 resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, 360 message: helperResult.message, 361 }, 362 ]) 363 ); 364 break; 365 366 case "traceOutput": 367 // Nothing in particular to do. 368 // The JSTRACER_STATE resource will report the start/stop of the profiler. 369 break; 370 } 371 } 372 373 const hasErrorMessage = 374 response.exceptionMessage || 375 (helperResult && helperResult.type === "error"); 376 377 // Hide undefined results coming from helper functions. 378 const hasUndefinedResult = 379 result && typeof result == "object" && result.type == "undefined"; 380 381 if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) { 382 dispatch(messagesActions.messagesAdd([response])); 383 } 384 }; 385 } 386 387 function focusInput() { 388 return ({ hud }) => { 389 return hud.focusInput(); 390 }; 391 } 392 393 function setInputValue(value) { 394 return ({ hud }) => { 395 return hud.setInputValue(value); 396 }; 397 } 398 399 /** 400 * Request an eager evaluation from the server. 401 * 402 * @param {string} expression: The expression to evaluate. 403 * @param {boolean} force: When true, will request an eager evaluation again, even if 404 * the expression is the same one than the one that was used in 405 * the previous evaluation. 406 */ 407 function terminalInputChanged(expression, force = false) { 408 return async ({ dispatch, webConsoleUI, hud, commands, getState }) => { 409 const prefs = getAllPrefs(getState()); 410 if (!prefs.eagerEvaluation) { 411 return null; 412 } 413 414 const { terminalInput = "" } = getState().history; 415 416 // Only re-evaluate if the expression did change. 417 if ( 418 (!terminalInput && !expression) || 419 (typeof terminalInput === "string" && 420 typeof expression === "string" && 421 expression.trim() === terminalInput.trim() && 422 !force) 423 ) { 424 return null; 425 } 426 427 dispatch({ 428 type: SET_TERMINAL_INPUT, 429 expression: expression.trim(), 430 }); 431 432 // There's no need to evaluate an empty string. 433 if (!expression || !expression.trim()) { 434 return dispatch({ 435 type: SET_TERMINAL_EAGER_RESULT, 436 expression, 437 result: null, 438 }); 439 } 440 441 let mapped; 442 ({ expression, mapped } = await getMappedExpression(hud, expression)); 443 444 // We don't want to evaluate top-level await expressions (see Bug 1786805) 445 if (mapped?.await) { 446 return dispatch({ 447 type: SET_TERMINAL_EAGER_RESULT, 448 expression, 449 result: null, 450 }); 451 } 452 453 const response = await commands.scriptCommand.execute(expression, { 454 frameActor: hud.getSelectedFrameActorID(), 455 selectedNodeActor: webConsoleUI.getSelectedNodeActorID(), 456 selectedTargetFront: getSelectedTarget( 457 hud.commands.targetCommand.store.getState() 458 ), 459 mapped, 460 eager: true, 461 }); 462 463 // If the terminal input changed while the expression was evaluated, don't render 464 // the results of the eager evaluation, it will be handled by the last call to 465 // terminalInputChanged 466 if (expression.trim() !== getState().history?.terminalInput) { 467 return null; 468 } 469 470 return dispatch({ 471 type: SET_TERMINAL_EAGER_RESULT, 472 result: getEagerEvaluationResult(response), 473 }); 474 }; 475 } 476 477 /** 478 * Refresh the current eager evaluation by requesting a new eager evaluation. 479 */ 480 function updateInstantEvaluationResultForCurrentExpression() { 481 return ({ getState, dispatch }) => 482 dispatch(terminalInputChanged(getState().history.terminalInput, true)); 483 } 484 485 function getEagerEvaluationResult(response) { 486 const result = response.exception || response.result; 487 // Don't show syntax errors results to the user. 488 if (result?.isSyntaxError || (result && result.type == "undefined")) { 489 return null; 490 } 491 492 return result; 493 } 494 495 function prettyPrintEditor() { 496 return { 497 type: EDITOR_PRETTY_PRINT, 498 }; 499 } 500 501 module.exports = { 502 evaluateExpression, 503 focusInput, 504 setInputValue, 505 terminalInputChanged, 506 updateInstantEvaluationResultForCurrentExpression, 507 prettyPrintEditor, 508 };