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/. */
7 const EventEmitter
= require("resource://devtools/shared/event-emitter.js");
8 const KeyShortcuts
= require("resource://devtools/client/shared/key-shortcuts.js");
11 } = require("resource://devtools/client/webconsole/utils/messages.js");
13 const { BrowserLoader
} = ChromeUtils
.importESModule(
14 "resource://devtools/shared/loader/browser-loader.sys.mjs"
17 getAdHocFrontOrPrimitiveGrip
,
18 } = require("resource://devtools/client/fronts/object.js");
23 } = require("resource://devtools/client/webconsole/constants.js");
25 const FirefoxDataProvider
= require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
28 ChromeUtils
.defineESModuleGetters(lazy
, {
29 AppConstants
: "resource://gre/modules/AppConstants.sys.mjs",
32 loader
.lazyRequireGetter(
34 "START_IGNORE_ACTION",
35 "resource://devtools/client/shared/redux/middleware/ignore.js",
38 const ZoomKeys
= require("resource://devtools/client/shared/zoom-keys.js");
39 loader
.lazyRequireGetter(
42 "resource://devtools/shared/specs/tracer.js",
46 const PREF_SIDEBAR_ENABLED
= "devtools.webconsole.sidebarToggle";
47 const PREF_BROWSERTOOLBOX_SCOPE
= "devtools.browsertoolbox.scope";
50 * A WebConsoleUI instance is an interactive console initialized *per target*
51 * that displays console log data as well as provides an interactive terminal to
52 * manipulate the target's document content.
54 * The WebConsoleUI is responsible for the actual Web Console UI
59 * @param {WebConsole} hud: The WebConsole owner object.
63 this.hudId
= this.hud
.hudId
;
64 this.isBrowserConsole
= this.hud
.isBrowserConsole
;
66 this.isBrowserToolboxConsole
=
67 this.hud
.commands
.descriptorFront
.isBrowserProcessDescriptor
&&
68 !this.isBrowserConsole
;
70 this.window
= this.hud
.iframeWindow
;
72 this._onPanelSelected
= this._onPanelSelected
.bind(this);
73 this._onChangeSplitConsoleState
=
74 this._onChangeSplitConsoleState
.bind(this);
75 this._onTargetAvailable
= this._onTargetAvailable
.bind(this);
76 this._onTargetDestroyed
= this._onTargetDestroyed
.bind(this);
77 this._onResourceAvailable
= this._onResourceAvailable
.bind(this);
78 this._onNetworkResourceUpdated
= this._onNetworkResourceUpdated
.bind(this);
79 this._onScopePrefChanged
= this._onScopePrefChanged
.bind(this);
80 this._onShowConsoleEvaluation
= this._onShowConsoleEvaluation
.bind(this);
82 if (this.isBrowserConsole
) {
83 Services
.prefs
.addObserver(
84 PREF_BROWSERTOOLBOX_SCOPE
,
85 this._onScopePrefChanged
89 EventEmitter
.decorate(this);
93 * Initialize the WebConsoleUI instance.
95 * A promise object that resolves once the frame is ready to use.
98 if (this._initializer
) {
99 return this._initializer
;
102 this._initializer
= (async () => {
105 if (this.isBrowserConsole
) {
107 // TargetCommand.startListening will start fetching additional targets
108 // and may overload the Browser Console with loads of targets and resources.
109 // We can call it from here, as `_attachTargets` is called after the UI is initialized.
111 // TargetCommand.startListening has to be called before:
112 // - `_attachTargets`, in order to set TargetCommand.watcherFront which is used by ResourceWatcher.watchResources.
113 // - `ConsoleCommands`, in order to set TargetCommand.targetFront which is wrapped by hud.currentTarget
114 await
this.hud
.commands
.targetCommand
.startListening();
115 if (this._destroyed
) {
120 await
this.wrapper
.init();
121 if (this._destroyed
) {
125 // Bug 1605763: It's important to call _attachTargets once the UI is initialized, as
126 // it may overload the Browser Console with many updates.
127 // It is also important to do it only after the wrapper is initialized,
128 // otherwise its `store` will be null while we already call a few dispatch methods
129 // from onResourceAvailable
130 await
this._attachTargets();
131 if (this._destroyed
) {
135 // `_attachTargets` will process resources and throttle some actions
136 // Wait for these actions to be dispatched before reporting that the
137 // console is initialized. Otherwise `showToolbox` will resolve before
138 // all already existing console messages are displayed.
139 await
this.wrapper
.waitAsyncDispatches();
140 this._initNotifications();
143 return this._initializer
;
147 if (this._destroyed
) {
151 this._destroyed
= true;
153 this.React
= this.ReactDOM
= this.FrameView
= null;
156 this.wrapper
.getStore()?.dispatch(START_IGNORE_ACTION
);
157 this.wrapper
.destroy();
161 this.jsterm
.destroy();
165 const { toolbox
} = this.hud
;
167 toolbox
.off("webconsole-selected", this._onPanelSelected
);
168 toolbox
.off("split-console", this._onChangeSplitConsoleState
);
169 toolbox
.off("select", this._onChangeSplitConsoleState
);
171 "show-original-variable-mapping-warnings",
172 this._onShowConsoleEvaluation
176 if (this.isBrowserConsole
) {
177 Services
.prefs
.removeObserver(
178 PREF_BROWSERTOOLBOX_SCOPE
,
179 this._onScopePrefChanged
183 // Stop listening for targets
184 this.hud
.commands
.targetCommand
.unwatchTargets({
185 types
: this.hud
.commands
.targetCommand
.ALL_TYPES
,
186 onAvailable
: this._onTargetAvailable
,
187 onDestroyed
: this._onTargetDestroyed
,
190 const resourceCommand
= this.hud
.resourceCommand
;
191 if (this._watchedResources
) {
192 resourceCommand
.unwatchResources(this._watchedResources
, {
193 onAvailable
: this._onResourceAvailable
,
197 this.stopWatchingNetworkResources();
199 if (this.networkDataProvider
) {
200 this.networkDataProvider
.destroy();
201 this.networkDataProvider
= null;
204 // Nullify `hud` last as it nullify also target which is used on destroy
205 this.window
= this.hud
= this.wrapper
= null;
209 * Clear the Web Console output.
211 * This method emits the "messages-cleared" notification.
213 * @param boolean clearStorage
214 * True if you want to clear the console messages storage associated to
216 * @param object event
217 * If the event exists, calls preventDefault on it.
219 async
clearOutput(clearStorage
, event
) {
221 event
.preventDefault();
224 this.wrapper
.dispatchMessagesClear();
228 await
this.clearMessagesCache();
230 this.emitForTests("messages-cleared");
233 async
clearMessagesCache() {
234 if (this._destroyed
) {
238 // This can be called during console destruction and getAllFronts would reject in such case.
240 const consoleFronts
= await
this.hud
.commands
.targetCommand
.getAllFronts(
241 this.hud
.commands
.targetCommand
.ALL_TYPES
,
245 for (const consoleFront
of consoleFronts
) {
246 promises
.push(consoleFront
.clearMessagesCacheAsync());
248 await Promise
.all(promises
);
249 this.emitForTests("messages-cache-cleared");
251 console
.warn("Exception in clearMessagesCache", e
);
256 * Remove all of the private messages from the Web Console output.
258 * This method emits the "private-messages-cleared" notification.
260 clearPrivateMessages() {
261 if (this._destroyed
) {
265 this.wrapper
.dispatchPrivateMessagesClear();
266 this.emitForTests("private-messages-cleared");
269 inspectObjectActor(objectActor
) {
270 const { targetFront
} = this.hud
.commands
.targetCommand
;
271 this.wrapper
.dispatchMessageAdd(
274 type
: "inspectObject",
276 objectActor
&& objectActor
.getGrip
278 : getAdHocFrontOrPrimitiveGrip(objectActor
, targetFront
),
286 disableAllNetworkMessages() {
287 if (this._destroyed
) {
290 this.wrapper
.dispatchNetworkMessagesDisable();
297 logWarningAboutReplacedAPI() {
298 return this.hud
.currentTarget
.logWarningInPage(
299 l10n
.getStr("ConsoleAPIDisabled"),
305 * Connect to the server using the remote debugging protocol.
309 * A promise object that is resolved/reject based on the proxies connections.
311 async
_attachTargets() {
312 const { commands
, resourceCommand
} = this.hud
;
313 this.networkDataProvider
= new FirefoxDataProvider({
316 updateRequest
: (id
, data
) =>
317 this.wrapper
.batchedRequestUpdates({ id
, data
}),
322 // Listen for all target types, including:
323 // - frames, in order to get the parent process target
324 // which is considered as a frame rather than a process.
325 // - workers, for similar reason. When we open a toolbox
326 // for just a worker, the top level target is a worker target.
327 // - processes, as we want to spawn additional proxies for them.
328 await commands
.targetCommand
.watchTargets({
329 types
: this.hud
.commands
.targetCommand
.ALL_TYPES
,
330 onAvailable
: this._onTargetAvailable
,
331 onDestroyed
: this._onTargetDestroyed
,
334 this._watchedResources
= [
335 resourceCommand
.TYPES
.CONSOLE_MESSAGE
,
336 resourceCommand
.TYPES
.ERROR_MESSAGE
,
337 resourceCommand
.TYPES
.PLATFORM_MESSAGE
,
338 resourceCommand
.TYPES
.DOCUMENT_EVENT
,
339 resourceCommand
.TYPES
.LAST_PRIVATE_CONTEXT_EXIT
,
340 resourceCommand
.TYPES
.JSTRACER_TRACE
,
341 resourceCommand
.TYPES
.JSTRACER_STATE
,
344 // CSS Warnings are only enabled when the user explicitely requested to show them
345 // as it can slow down page load.
346 const shouldShowCssWarnings
= this.wrapper
.getFilterState(FILTERS
.CSS
);
347 if (shouldShowCssWarnings
) {
348 this._watchedResources
.push(resourceCommand
.TYPES
.CSS_MESSAGE
);
351 await resourceCommand
.watchResources(this._watchedResources
, {
352 onAvailable
: this._onResourceAvailable
,
355 if (this.isBrowserConsole
|| this.isBrowserToolboxConsole
) {
356 const shouldEnableNetworkMonitoring
= Services
.prefs
.getBoolPref(
357 PREFS
.UI
.ENABLE_NETWORK_MONITORING
359 if (shouldEnableNetworkMonitoring
) {
360 await
this.startWatchingNetworkResources();
362 await
this.stopWatchingNetworkResources();
365 // We should always watch for network resources in the webconsole
366 await
this.startWatchingNetworkResources();
370 async
startWatchingNetworkResources() {
371 const { commands
, resourceCommand
} = this.hud
;
372 await resourceCommand
.watchResources(
374 resourceCommand
.TYPES
.NETWORK_EVENT
,
375 resourceCommand
.TYPES
.NETWORK_EVENT_STACKTRACE
,
378 onAvailable
: this._onResourceAvailable
,
379 onUpdated
: this._onNetworkResourceUpdated
,
383 // When opening a worker toolbox from about:debugging,
384 // we do not instantiate any Watcher actor yet and would throw here.
385 // But even once we do, we wouldn't support network inspection anyway.
386 if (commands
.targetCommand
.hasTargetWatcherSupport()) {
387 const networkFront
= await commands
.watcherFront
.getNetworkParentActor();
388 // There is no way to view response bodies from the Browser Console, so do
389 // not waste the memory.
391 !this.isBrowserConsole
&&
392 Services
.prefs
.getBoolPref(
393 "devtools.netmonitor.saveRequestAndResponseBodies"
395 await networkFront
.setSaveRequestAndResponseBodies(saveBodies
);
399 async
stopWatchingNetworkResources() {
400 if (this._destroyed
) {
404 await
this.hud
.resourceCommand
.unwatchResources(
406 this.hud
.resourceCommand
.TYPES
.NETWORK_EVENT
,
407 this.hud
.resourceCommand
.TYPES
.NETWORK_EVENT_STACKTRACE
,
410 onAvailable
: this._onResourceAvailable
,
411 onUpdated
: this._onNetworkResourceUpdated
,
416 handleDocumentEvent(resource
) {
417 // Only consider top level document, and ignore remote iframes top document
418 if (!resource
.targetFront
.isTopLevel
) {
422 if (resource
.name
== "will-navigate") {
423 this.handleWillNavigate({
424 timeStamp
: resource
.time
,
425 url
: resource
.newURI
,
427 } else if (resource
.name
== "dom-complete") {
428 this.handleNavigated({
429 hasNativeConsoleAPI
: resource
.hasNativeConsoleAPI
,
432 // For now, ignore all other DOCUMENT_EVENT's.
436 * Handler for when the page is done loading.
438 * @param Boolean hasNativeConsoleAPI
439 * True if the `console` object is the native one and hasn't been overloaded by a custom
440 * object by the page itself.
442 async
handleNavigated({ hasNativeConsoleAPI
}) {
443 // Updates instant evaluation on page navigation
444 this.wrapper
.dispatchUpdateInstantEvaluationResultForCurrentExpression();
446 // Wait for completion of any async dispatch before notifying that the console
447 // is fully updated after a page reload
448 await
this.wrapper
.waitAsyncDispatches();
450 if (!hasNativeConsoleAPI
) {
451 this.logWarningAboutReplacedAPI();
454 this.emit("reloaded");
457 handleWillNavigate({ timeStamp
, url
}) {
458 this.wrapper
.dispatchTabWillNavigate({ timeStamp
, url
});
462 * Called when the CSS Warning filter is enabled, in order to start observing for them in the backend.
464 async
watchCssMessages() {
465 const { resourceCommand
} = this.hud
;
466 if (this._watchedResources
.includes(resourceCommand
.TYPES
.CSS_MESSAGE
)) {
469 await resourceCommand
.watchResources([resourceCommand
.TYPES
.CSS_MESSAGE
], {
470 onAvailable
: this._onResourceAvailable
,
472 this._watchedResources
.push(resourceCommand
.TYPES
.CSS_MESSAGE
);
475 // eslint-disable-next-line complexity
476 _onResourceAvailable(resources
) {
477 if (this._destroyed
) {
481 const { logMethod
} = this.hud
.commands
.tracerCommand
.getTracingOptions();
484 for (const resource
of resources
) {
485 const { TYPES
} = this.hud
.resourceCommand
;
486 if (resource
.resourceType
=== TYPES
.DOCUMENT_EVENT
) {
487 this.handleDocumentEvent(resource
);
490 if (resource
.resourceType
== TYPES
.LAST_PRIVATE_CONTEXT_EXIT
) {
491 // Private messages only need to be removed from the output in Browser Console/Browser Toolbox
492 // (but in theory this resource should only be send from parent process watchers)
493 if (this.isBrowserConsole
|| this.isBrowserToolboxConsole
) {
494 this.clearPrivateMessages();
498 // Ignore messages forwarded from content processes if we're in fission browser toolbox.
501 ((resource
.resourceType
=== TYPES
.ERROR_MESSAGE
||
502 resource
.resourceType
=== TYPES
.CSS_MESSAGE
) &&
503 resource
.pageError
?.isForwardedFromContentProcess
&&
504 (this.isBrowserToolboxConsole
|| this.isBrowserConsole
))
509 // Don't show messages emitted from a private window before the Browser Console was
510 // opened to avoid leaking data from past usage of the browser (e.g. content message
511 // from now closed private tabs)
513 (this.isBrowserToolboxConsole
|| this.isBrowserConsole
) &&
514 resource
.isAlreadyExistingResource
&&
515 (resource
.pageError
?.private || resource
.private)
521 resource
.resourceType
=== TYPES
.JSTRACER_TRACE
&&
522 logMethod
!= TRACER_LOG_METHODS
.CONSOLE
526 if (resource
.resourceType
=== TYPES
.NETWORK_EVENT_STACKTRACE
) {
527 this.networkDataProvider
?.onStackTraceAvailable(resource
);
531 if (resource
.resourceType
=== TYPES
.NETWORK_EVENT
) {
532 this.networkDataProvider
?.onNetworkResourceAvailable(resource
);
534 messages
.push(resource
);
536 this.wrapper
.dispatchMessagesAdd(messages
);
539 _onNetworkResourceUpdated(updates
) {
540 if (this._destroyed
) {
544 const messageUpdates
= [];
545 for (const { resource
} of updates
) {
547 resource
.resourceType
== this.hud
.resourceCommand
.TYPES
.NETWORK_EVENT
549 this.networkDataProvider
?.onNetworkResourceUpdated(resource
);
550 messageUpdates
.push(resource
);
553 this.wrapper
.dispatchMessagesUpdate(messageUpdates
);
557 * Called any time a new target is available.
558 * i.e. it was already existing or has just been created.
562 async
_onTargetAvailable() {
563 // onTargetAvailable is a mandatory argument for watchTargets,
564 // we still define it solely for being able to use onTargetDestroyed.
567 _onTargetDestroyed({ targetFront
, isModeSwitching
}) {
568 // Don't try to do anything if the WebConsole is being destroyed
569 if (this._destroyed
) {
573 // We only want to remove messages from a target destroyed when we're switching mode
574 // in the Browser Console/Browser Toolbox Console.
575 // For regular cases, we want to keep the message history (the output will still be
576 // cleared when the top level target navigates, if "Persist Logs" isn't true, via handleWillNavigate)
577 if (isModeSwitching
) {
578 this.wrapper
.dispatchTargetMessagesRemove(targetFront
);
583 this.document
= this.window
.document
;
584 this.rootElement
= this.document
.documentElement
;
586 this.outputNode
= this.document
.getElementById("app-wrapper");
588 const { toolbox
} = this.hud
;
590 // Initialize module loader and load all the WebConsoleWrapper. The entire code-base
591 // doesn't need any extra privileges and runs entirely in content scope.
592 const WebConsoleWrapper
= BrowserLoader({
593 baseURI
: "resource://devtools/client/webconsole/",
595 }).require("resource://devtools/client/webconsole/webconsole-wrapper.js");
597 this.wrapper
= new WebConsoleWrapper(
604 this._initShortcuts();
605 this._initOutputSyntaxHighlighting();
608 toolbox
.on("webconsole-selected", this._onPanelSelected
);
609 toolbox
.on("split-console", this._onChangeSplitConsoleState
);
610 toolbox
.on("select", this._onChangeSplitConsoleState
);
614 _initOutputSyntaxHighlighting() {
615 // Given a DOM node, we syntax highlight identically to how the input field
616 // looks. See https://codemirror.net/demo/runmode.html;
617 const syntaxHighlightNode
= node
=> {
618 const editor
= this.jsterm
&& this.jsterm
.editor
;
619 if (node
&& editor
) {
620 node
.classList
.add("cm-s-mozilla");
621 editor
.CodeMirror
.runMode(
623 "application/javascript",
629 // Use a Custom Element to handle syntax highlighting to avoid
630 // dealing with refs or innerHTML from React.
631 const win
= this.window
;
632 win
.customElements
.define(
633 "syntax-highlighted",
634 class extends win
.HTMLElement
{
635 connectedCallback() {
636 if (!this.connected
) {
637 this.connected
= true;
638 syntaxHighlightNode(this);
640 // Highlight Again when the innerText changes
641 // We remove the listener before running codemirror mode and add
642 // it again to capture text changes
643 this.observer
= new win
.MutationObserver((mutations
, observer
) => {
644 observer
.disconnect();
645 syntaxHighlightNode(this);
646 observer
.observe(this, { childList
: true });
649 this.observer
.observe(this, { childList
: true });
656 _initNotifications() {
657 if (this.hud
.toolbox
) {
658 this.wrapper
.toggleOriginalVariableMappingEvaluationNotification(
660 .getPanel("jsdebugger")
661 ?.shouldShowOriginalVariableMappingWarnings()
664 "show-original-variable-mapping-warnings",
665 this._onShowConsoleEvaluation
671 const shortcuts
= new KeyShortcuts({
676 if (lazy
.AppConstants
.platform
=== "macosx") {
677 const alternativaClearShortcut
= l10n
.getStr(
678 "webconsole.clear.alternativeKeyOSX"
680 shortcuts
.on(alternativaClearShortcut
, event
=>
681 this.clearOutput(true, event
)
683 clearShortcut
= l10n
.getStr("webconsole.clear.keyOSX");
685 clearShortcut
= l10n
.getStr("webconsole.clear.key");
688 shortcuts
.on(clearShortcut
, event
=> this.clearOutput(true, event
));
690 if (this.isBrowserConsole
) {
691 // Make sure keyboard shortcuts work immediately after opening
692 // the Browser Console (Bug 1461366).
695 l10n
.getStr("webconsole.close.key"),
696 this.window
.close
.bind(this.window
)
699 ZoomKeys
.register(this.window
, shortcuts
);
701 /* This is the same as DevelopmentHelpers.quickRestart, but it runs in all
702 * builds (even official). This allows a user to do a restart + session restore
703 * with Ctrl+Shift+J (open Browser Console) and then Ctrl+Alt+R (restart).
705 shortcuts
.on("CmdOrCtrl+Alt+R", () => {
706 this.hud
.commands
.targetCommand
.reloadTopLevelTarget();
708 } else if (Services
.prefs
.getBoolPref(PREF_SIDEBAR_ENABLED
)) {
709 shortcuts
.on("Esc", () => {
710 this.wrapper
.dispatchSidebarClose();
719 * Sets the focus to JavaScript input field when the web console tab is
720 * selected or when there is a split console present.
724 // We can only focus when we have the jsterm reference. This is fine because if the
725 // jsterm is not mounted yet, it will be focused in JSTerm's componentDidMount.
731 _onChangeSplitConsoleState() {
732 this.wrapper
.dispatchSplitConsoleCloseButtonToggle();
735 _onScopePrefChanged() {
736 if (this.isBrowserConsole
) {
737 this.hud
.updateWindowTitle();
741 _onShowConsoleEvaluation(isOriginalVariableMappingEnabled
) {
742 this.wrapper
.toggleOriginalVariableMappingEvaluationNotification(
743 isOriginalVariableMappingEnabled
748 return this.jsterm
&& this.jsterm
.getSelectionStart();
751 getJsTermTooltipAnchor() {
752 return this.outputNode
.querySelector(".CodeMirror-cursor");
755 attachRef(id
, node
) {
759 getSelectedNodeActorID() {
760 const inspectorSelection
= this.hud
.getInspectorSelection();
761 return inspectorSelection
?.nodeFront
?.actorID
;
765 exports
.WebConsoleUI
= WebConsoleUI
;