mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-24 09:52:31 +00:00
They have fixed the exception mentioned here, but the script still has trouble displaying all live-captured packets.
549 lines
41 KiB
Markdown
549 lines
41 KiB
Markdown
# Firefox DevTools Integration
|
|
|
|
Ladybird contains a DevTools server. This document describes how to use the server and the protocol used to communicate
|
|
with the DevTools client.
|
|
|
|
## Using DevTools
|
|
|
|
The DevTools server may be enabled in the "Inspect" menu via the "Enable DevTools" menu item. A banner will be displayed
|
|
in Ladybird with the port on which the DevTools server is listening. This port may be changed with the `--devtools-port`
|
|
command line flag. To disable DevTools, use the same menu item (which will now be titled "Disable DevTools"), or close
|
|
the banner.
|
|
|
|
Once DevTools is enabled, in Firefox, navigate to `about:debugging` and select the "Setup" tab. In the "Network Location"
|
|
form, enter the DevTools server address. In the above example, this will be `localhost:6000`. You will only have to
|
|
enter this information once:
|
|
|
|
<img src="Images/devtools_setup.png" alt="DevTools setup" width="675px" />
|
|
|
|
This will make the address appear on the left-side of the page. Click "Connect":
|
|
|
|
<img src="Images/devtools_connect.png" alt="DevTools connect" width="240px" />
|
|
|
|
Once connected, the listed address will now be clickable itself. Click it to be brought to a page which shows Ladybird's
|
|
tab list:
|
|
|
|
<img src="Images/devtools_tab_list.png" alt="DevTools tab list" width="675px" />
|
|
|
|
Click the "Inspect" button next to the tab you wish to debug. This will open another tab in Firefox with an inspector
|
|
panel, which you may use to view the DOM tree:
|
|
|
|
<img src="Images/devtools_dom_tree.png" alt="DevTools DOM tree" width="675px" />
|
|
|
|
## DevTools protocol
|
|
|
|
Firefox's documentation of its [DevTools protocol](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html)
|
|
is quite incomplete, and even incorrect in some places. To aid with development, this section describes the protocol that
|
|
Ladybird has implemented. Note that this is only observed behavior - our implementation could certainly be incorrect or
|
|
incomplete in places.
|
|
|
|
The protocol itself is based on ["actors"](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#actors).
|
|
Each packet contains a JSON object. Messages sent from the DevTools client contain a `"to"` field indicating the actor
|
|
for which the message is intended. Messages sent from the DevTools server contain a `"from"` field indicating the actor
|
|
from which the message originated.
|
|
|
|
A very important aspect of this protocol is that the client may send multiple requests to an actor at once without
|
|
waiting for a reply, to which the actor must then reply in order. Any requests that must be performed asynchronously
|
|
(such as fetching the serialized DOM tree from the WebContent process) must block the actor from sending replies for
|
|
subsequent requests.
|
|
|
|
To log communication between the DevTools server and client, enable the `DEVTOOLS_DEBUG` flag:
|
|
|
|
```bash
|
|
cmake -B Build/release -D DEVTOOLS_DEBUG=ON
|
|
```
|
|
|
|
Logged messages beginning with "<<" were sent from the server to the client. Messages beginning with ">>" were sent from
|
|
the client to the server.
|
|
|
|
### Session initialization
|
|
|
|
Once the DevTools client has connected, communication begins with a [root actor](../Libraries/LibDevTools/Actors/RootActor.cpp).
|
|
The root actor initiates the session by sending a message to the client describing itself:
|
|
|
|
```jsonc
|
|
<< {"from":"root","applicationType":"browser","traits":{"sources":false,"highlightable":true,"customHighlighters":true,"networkMonitor":false}}
|
|
```
|
|
|
|
The client then sends a connection request, to which the server replies with an empty message:
|
|
|
|
```jsonc
|
|
>> {"type":"connect","frontendVersion":"135.0","to":"root"}
|
|
<< {"from":"root"}
|
|
```
|
|
|
|
The client then asks the root actor to describe other top-level actors. We are required to provide a device actor and a
|
|
preference actor:
|
|
|
|
```jsonc
|
|
>> {"type":"getRoot","to":"root"}
|
|
<< {"from":"root","selected":0,"deviceActor":"server0-device1","preferenceActor":"server0-preference2"}
|
|
```
|
|
|
|
The device actor provides information about the device for which the server is running, such as application version and
|
|
host OS. We provide a [subset of fields](https://github.com/mozilla/gecko-dev/blob/master/devtools/shared/system.js)
|
|
that Firefox itself provides, as the other fields seem optional, and may not even make sense for Ladybird.
|
|
|
|
The client will immediately request this information after the `getRoot` reply:
|
|
|
|
```jsonc
|
|
>> {"type":"getDescription","to":"server0-device1"}
|
|
<< {"from":"server0-device1","value":{"apptype":"ladybird","name":"Ladybird","brandName":"Ladybird","version":"1.0","appbuildid":"Version 1.0","platformbuildid":"Version 1.0","platformversion":"135.0","useragent":"Mozilla/5.0 (macOS; AArch64) Ladybird/1.0","os":"macOS","arch":"AArch64"}}
|
|
```
|
|
|
|
The preference actor is used to query and update configuration options that resemble those found in Firefox's
|
|
`about:config` page. We don't implement anything concrete here. The client will request a few boolean configuration
|
|
options after the device request, to which the server will just reply with `false`:
|
|
|
|
```jsonc
|
|
>> {"type":"getBoolPref","value":"devtools.debugger.prompt-connection","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
>> {"type":"getBoolPref","value":"browser.privatebrowsing.autostart","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
>> {"type":"getBoolPref","value":"dom.serviceWorkers.enabled","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
```
|
|
|
|
The client then asks for a list of add-ons, workers, and service workers, to which the server replies with an empty list:
|
|
|
|
```jsonc
|
|
>> {"type":"listTabs","to":"root"}
|
|
<< {"from":"root","addons":[]}
|
|
|
|
>> {"type":"listWorkers","to":"root"}
|
|
<< {"from":"root","workers":[]}
|
|
|
|
>> {"type":"listServiceWorkerRegistrations","to":"root"}
|
|
<< {"from":"root","registrations":[]}
|
|
```
|
|
|
|
Then client then asks for a list of processes, followed immediately by a request for the zeroth process. This zeroth
|
|
process is required, and seems to correspond to the browser's main process. We currently stub out this information:
|
|
|
|
```jsonc
|
|
>> {"type":"listProcesses","to":"root"}
|
|
>> {"type":"getProcess","id":0,"to":"root"}
|
|
|
|
<< {"from":"root","processes":[{"actor":"server0-process3","id":0,"isParent":true,"isWindowlessParent":false,"traits":{"watcher":true,"supportsReloadDescriptor":true}}]}
|
|
<< {"from":"root","processDescriptor":{"actor":"server0-process3","id":0,"isParent":true,"isWindowlessParent":false,"traits":{"watcher":true,"supportsReloadDescriptor":true}}}
|
|
```
|
|
|
|
The client then asks for a list of tabs. The server contains a [delegate interface](../Libraries/LibDevTools/DevToolsDelegate.h)
|
|
to be implemented by the WebView application. This interface includes a method to form a list open tabs. The server will
|
|
reply with this list:
|
|
|
|
```jsonc
|
|
>> {"type":"listTabs","to":"root"}
|
|
<< {"from":"root","tabs":[{"actor":"server0-tab4","title":"Ladybird","url":"https://ladybird.org/","browserId":1,"browsingContextID":1,"outerWindowID":1,"traits":{"watcher":true,"supportsReloadDescriptor":true}}]}
|
|
```
|
|
|
|
The client then asks for information about the tabs, including their favicon. The favicon is expected to be a URL, but
|
|
when provided, the DevTools client seems to hang. So we use a null value here, which Firefox itself also uses. The
|
|
server will reply with the same information provided in the `listTabs` request:
|
|
|
|
```jsonc
|
|
>> {"type":"getFavicon","to":"server0-tab4"}
|
|
<< {"from":"server0-tab4","favicon":null}
|
|
|
|
>> {"type":"getTab","browserId":1,"to":"root"}
|
|
<< {"from":"root","tab":{"actor":"server0-tab4","title":"Ladybird","url":"https://ladybird.org/","browserId":1,"browsingContextID":1,"outerWindowID":1,"traits":{"watcher":true,"supportsReloadDescriptor":true}}}
|
|
```
|
|
|
|
At this point, the session is established and Ladybird is listed on `about:debugging`.
|
|
|
|
### Inspecting a DOM tree
|
|
|
|
When the user inspects a tab, the client initiates the inspection with a request for a "watcher". Watchers are the
|
|
actors responsible for monitoring a wide variety of components. We currently only implement a "frame" watcher. The
|
|
server will create a watcher associated with the inspected tab, and reply with a set of watcher options indicating our
|
|
support of frame inspection:
|
|
|
|
```jsonc
|
|
>> {"type":"getWatcher","isServerTargetSwitchingEnabled":true,"isPopupDebuggingEnabled":false,"to":"server0-tab4"}
|
|
<< {"from":"server0-tab4","actor":"server0-watcher5","traits":{"shared_worker":false,"service_worker":false,"frame":true,"process":false,"worker":false,"resources":{"Cache":false,"console-message":false,"cookies":false,"css-change":false,"css-message":false,"css-registered-properties":false,"document-event":false,"error-message":false,"extension-storage":false,"indexed-db":false,"jstracer-state":false,"jstracer-trace":false,"last-private-context-exit":false,"local-storage":false,"network-event":false,"network-event-stacktrace":false,"platform-message":false,"reflow":false,"server-sent-event":false,"session-storage":false,"source":false,"stylesheet":false,"thread-state":false,"websocket":false}}}
|
|
```
|
|
|
|
The client then asks the server to watch the inspected tab's frame. The server must reply with multiple messages here.
|
|
The first message contains information about the inspected tab, as well as a list of other actors associated with the
|
|
watcher. We are required to have an inspector actor, a CSS properties actor, and a thread actor. We also have the optional
|
|
console actor. These actors are described below when they are requested by the client.
|
|
|
|
The second message contains a small set of information about the tab again. The third message is just an empty message,
|
|
which seems to indicate and end-of-transmission status:
|
|
|
|
```jsonc
|
|
>> {"type":"watchTargets","targetType":"frame","to":"server0-watcher5"}
|
|
<< {"from":"server0-watcher5","type":"target-available-form","target":{"actor":"server0-frame10","title":"xkcd: Scream Cipher","url":"https://xkcd.com/","browsingContextID":1,"outerWindowID":1,"isTopLevelTarget":true,"traits":{"frames":true,"isBrowsingContext":true,"logInPage":false,"navigation":true,"supportsTopLevelTargetFlag":true,"watchpoints":true},"cssPropertiesActor":"server0-css-properties6","consoleActor":"server0-console8","inspectorActor":"server0-inspector7","threadActor":"server0-thread9"}}
|
|
<< {"from":"server0-frame9","type":"frameUpdate","frames":[{"id":1,"title":"Ladybird","url":"https://ladybird.org/"}]}
|
|
<< {"from":"server0-watcher5"}
|
|
```
|
|
|
|
The client then asks for a couple of configuration actors, and sends some configuration options to those actors. These
|
|
actors are a target configuration actor and a thread configuration actor. The target configuration actor informs the
|
|
client about features supported by the target tab, such as an "offline mode". We currently stub these options to
|
|
indicate all features are not supported. The thread configuration actor is currently entirely a stub.
|
|
|
|
```jsonc
|
|
>> {"type":"getTargetConfigurationActor","to":"server0-watcher5"}
|
|
<< {"from":"server0-watcher5","configuration":{"actor":"server0-target-configuration10","configuration":{},"traits":{"supportedOptions":{"cacheDisabled":false,"colorSchemeSimulation":false,"customFormatters":false,"customUserAgent":false,"javascriptEnabled":false,"overrideDPPX":false,"printSimulationEnabled":false,"rdmPaneMaxTouchPoints":false,"rdmPaneOrientation":false,"recordAllocations":false,"reloadOnTouchSimulationToggle":false,"restoreFocus":false,"serviceWorkersTestingEnabled":false,"setTabOffline":false,"touchEventsOverride":false,"tracerOptions":false,"useSimpleHighlightersForReducedMotion":false}}}}
|
|
|
|
>> {"type":"updateConfiguration","configuration":{"cacheDisabled":true,"customFormatters":false,"serviceWorkersTestingEnabled":false,"useSimpleHighlightersForReducedMotion":false,"isTracerFeatureEnabled":false},"to":"server0-target-configuration10"}
|
|
<< {"from":"server0-target-configuration10"}
|
|
|
|
>> {"type":"getThreadConfigurationActor","to":"server0-watcher5"}
|
|
<< {"from":"server0-watcher5","configuration":{"actor":"server0-thread-configuration11"}}
|
|
|
|
>> {"type":"updateConfiguration","configuration":{"shouldPauseOnDebuggerStatement":true,"pauseOnExceptions":false,"ignoreCaughtExceptions":true,"shouldIncludeSavedFrames":true,"shouldIncludeAsyncLiveFrames":false,"skipBreakpoints":false,"logEventBreakpoints":false,"observeAsmJS":true,"pauseOverlay":true},"to":"server0-thread-configuration11"}
|
|
<< {"from":"server0-thread-configuration11"}
|
|
```
|
|
|
|
The client then asks for a list of frames, to which the server replies with an empty message:
|
|
|
|
```jsonc
|
|
>> {"type":"listFrames","to":"server0-frame9"}
|
|
<< {"from":"server0-frame9"}
|
|
```
|
|
|
|
The client then asks for a CSS database. The server must reply with a list of every CSS property that the rendering
|
|
engine supports. The delegate interface includes a method for the application to provide this information. LibWebView
|
|
will reply with the list of properties generated by [Properties.json](../Libraries/LibWeb/CSS/Properties.json).
|
|
|
|
(For brevity, only the `font` property is included here, as the list of all properties is very large.)
|
|
|
|
```jsonc
|
|
>> {"type":"getCSSDatabase","to":"server0-css-properties6"}
|
|
<< {"from":"server0-css-properties6","properties":{"font":{"isInherited":true,"supports":[],"values":[],"subproperties":["font"]}}}
|
|
```
|
|
|
|
The client then asks the inspector actor for a few more actors in a row. The inspector actor is essentially just a
|
|
container that serves to hold other actors that actually perform inspection.
|
|
|
|
First, the client asks for a walker actor. This walker is what will actually own and traverse the DOM tree. It is at
|
|
this point that the server fetches the DOM tree from the WebContent process. The delegate interface includes a method
|
|
for the application to asynchronously provide this information. The inspector actor's message queue is blocked until we
|
|
receive the DOM tree (or encounter an error). The server will reply with information about the document element.
|
|
|
|
Next, the client asks for a page style actor and a highlighter actor. The page style actor is what will provide layout
|
|
and style information about a particular node. The highlighter actor's responsibility varies depending on the type of
|
|
highlighter that is requested. Here, the client asks for a viewport resize highlighter, which we currently do not
|
|
support.
|
|
|
|
```jsonc
|
|
>> {"type":"getWalker","options":{"showAllAnonymousContent":false},"to":"server0-inspector7"}
|
|
>> {"type":"getPageStyle","to":"server0-inspector7"}
|
|
>> {"type":"getHighlighterByType","typeName":"ViewportSizeOnResizeHighlighter","to":"server0-inspector7"}
|
|
|
|
<< {"from":"server0-inspector7","walker":{"actor":"server0-walker14","root":{"actor":"server0-node15","attrs":[],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"#document","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":false,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":true,"nodeName":"#document","nodeType":9,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{}}}}
|
|
<< {"from":"server0-inspector7","pageStyle":{"actor":"server0-page-style12","traits":{"fontStyleLevel4":true,"fontWeightLevel4":true,"fontStretchLevel4":true,"fontVariations":true}}}
|
|
<< {"from":"server0-inspector7","highlighter":{"actor":"server0-highlighter13"}}
|
|
```
|
|
|
|
The client then asks for the frame's parent browsing context, to which the server currently replies with the same
|
|
context as the frame itself:
|
|
|
|
```jsonc
|
|
>> {"type":"getParentBrowsingContextID","browsingContextID":1,"to":"server0-watcher5"}
|
|
<< {"from":"server0-watcher5","browsingContextID":1}
|
|
```
|
|
|
|
The client then instructs the viewport resize highlighter actor to "show" the inspector actor, to which the server
|
|
currently replies with a nack:
|
|
|
|
```jsonc
|
|
>> {"type":"show","node":"server0-inspector7","to":"server0-highlighter13"}
|
|
<< {"from":"server0-highlighter13","value":false}
|
|
```
|
|
|
|
Then client then starts to ask for DOM node information. It begins by issuing a query selector request for the `<body>`
|
|
element, followed by requests for the children of the `<html>` element, the document element, and the `<body>` element.
|
|
The query selector indicates the actor name of the node from which to start searching, and the name of the requested
|
|
node. We do not currently create concrete actor objects for each node; rather, we just assign actor names to each node
|
|
in the DOM tree received from the WebContent process. The query selector reply includes a field to indicate that its
|
|
parent is not the parent from which the search started. Each serialized DOM node contains information such as its type,
|
|
display name, tag name, attributes, etc.
|
|
|
|
```jsonc
|
|
>> {"type":"querySelector","node":"server0-node15","selector":"body","to":"server0-walker14"}
|
|
<< {"from":"server0-walker14","node":{"actor":"server0-node32","attrs":[],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"body","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"BODY","nodeType":1,"nodeValue":null,"numChildren":10,"shadowRootMode":null,"traits":{},"parent":"server0-node16"},"newParents":[{"actor":"server0-node16","attrs":[{"name":"lang","value":"en"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"html","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"HTML","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node15"}]}
|
|
|
|
>> {"type":"children","node":"server0-node16","maxNodes":100,"center":"server0-node32","to":"server0-walker14"}
|
|
>> {"type":"children","node":"server0-node15","maxNodes":100,"center":"server0-node16","to":"server0-walker14"}
|
|
>> {"type":"children","node":"server0-node32","maxNodes":100,"to":"server0-walker14"}
|
|
|
|
<< {"from":"server0-walker14","hasFirst":true,"hasLast":true,"nodes":[{"actor":"server0-node17","attrs":[],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"head","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":false,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"HEAD","nodeType":1,"nodeValue":null,"numChildren":13,"shadowRootMode":null,"traits":{},"parent":"server0-node16"},{"actor":"server0-node32","attrs":[],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"body","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"BODY","nodeType":1,"nodeValue":null,"numChildren":10,"shadowRootMode":null,"traits":{},"parent":"server0-node16"}]}
|
|
<< {"from":"server0-walker14","hasFirst":true,"hasLast":true,"nodes":[{"actor":"server0-node16","attrs":[{"name":"lang","value":"en"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"html","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"HTML","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node15"}]}
|
|
<< {"from":"server0-walker14","hasFirst":true,"hasLast":true,"nodes":[{"actor":"server0-node33","attrs":[{"name":"class","value":"p-[20px] bg-[#000] relative"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"header","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"HEADER","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node53","attrs":[{"name":"class","value":"bg-[#000] relative flex flex-col items-center pt-[80px] px-5 pb-0 lg:flex-row lg:pt-0 lg:min-h-[892px]"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node70","attrs":[{"name":"id","value":"about"},{"name":"class","value":"text-[#fff] bg-[#000] pt-12 lg:pt-0 px-4"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node86","attrs":[{"name":"class","value":"why"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node127","attrs":[{"name":"class","value":"news"},{"name":"id","value":"news"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node189","attrs":[{"name":"id","value":"gi"},{"name":"class","value":"gi"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":2,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node212","attrs":[{"name":"class","value":"relative mb-24 px-5 lg:mb-28"},{"name":"id","value":"sponsors"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":3,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node337","attrs":[{"name":"class","value":"relative z-10 -top-4 text-[#fff] bg-[url('/assets/img/blurp.webp')] bg-center bg-cover mb-12 flex justify-center"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node356","attrs":[{"name":"id","value":"faq"},{"name":"class","value":"faq"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"section","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"SECTION","nodeType":1,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{},"parent":"server0-node32"},{"actor":"server0-node510","attrs":[{"name":"class","value":"bg-[#000] py-0 md:px-20"}],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"footer","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":true,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":false,"nodeName":"FOOTER","nodeType":1,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{},"parent":"server0-node32"}]}
|
|
```
|
|
|
|
The client then instructs the server to watch the root node. The server replies with information about the document
|
|
element again, followed by an empty message. We do not currently do anything more with this request.
|
|
|
|
```jsonc
|
|
>> {"type":"watchRootNode","to":"server0-walker14"}
|
|
<< {"from":"server0-walker14","type":"root-available","node":{"actor":"server0-node15","attrs":[],"baseURI":"https://ladybird.org/","causesOverflow":false,"containerType":null,"displayName":"#document","displayType":"block","host":null,"isAfterPseudoElement":false,"isAnonymous":false,"isBeforePseudoElement":false,"isDirectShadowHostChild":null,"isDisplayed":false,"isInHTMLDocument":true,"isMarkerPseudoElement":false,"isNativeAnonymous":false,"isScrollable":false,"isShadowHost":false,"isShadowRoot":false,"isTopLevelDocument":true,"nodeName":"#document","nodeType":9,"nodeValue":null,"numChildren":1,"shadowRootMode":null,"traits":{}}}
|
|
<< {"from":"server0-walker14"}
|
|
```
|
|
|
|
At this point, the DOM tree in the DevTools client is interactable. As the user interacts with the client, the client
|
|
will send similar requests as the above to retrieve more DOM node information. The server will log an error for features
|
|
we do not yet support.
|
|
|
|
### Inspecting a DOM node
|
|
|
|
After the DOM tree becomes interactable, the client will inspect a default node on the page (typically the `<body>`
|
|
element). The client will immediately ask for the layout of the inspected node and for a layout inspector. The layout
|
|
consists of the box model metrics for the node. The layout inspector is an actor that provides e.g. grid and flexbox
|
|
information, but we currently just sub this out.
|
|
|
|
```jsonc
|
|
>> {"type":"getLayout","node":"server0-node31","autoMargins":true,"to":"server0-page-style12"}
|
|
>> {"type":"getLayoutInspector","to":"server0-walker14"}
|
|
|
|
<< {"from":"server0-page-style12","autoMargins":{},"width":1514,"height":10236.8125,"border-top-width":"0px","border-right-width":"0px","border-bottom-width":"0px","border-left-width":"0px","margin-top":"0px","margin-right":"0px","margin-bottom":"0px","margin-left":"0px","padding-top":"0px","padding-right":"0px","padding-bottom":"0px","padding-left":"0px","box-sizing":"border-box","display":"block","float":"none","line-height":"1.5","position":"static","z-index":"auto"}
|
|
<< {"from":"server0-walker14","actor":{"actor":"server0-layout-inspector568"}}
|
|
```
|
|
|
|
The client then asks for a selector for the inspected node. The server replies with the display name of the node:
|
|
|
|
```jsonc
|
|
>> {"type":"getUniqueSelector","to":"server0-node32"}
|
|
<< {"from":"server0-node32","value":"body"}
|
|
```
|
|
|
|
The client then asks for the applied style for the inspected node. This would be for the pane in the inspector tab which
|
|
allows the user to toggle and edit styles on a live DOM node. We do not yet have support for this in LibWeb, so the
|
|
server replies with an empty list:
|
|
|
|
```jsonc
|
|
>> {"type":"getApplied","node":"server0-node32","inherited":true,"matchedSelectors":true,"to":"server0-page-style12"}
|
|
<< {"from":"server0-page-style12","entries":[]}
|
|
```
|
|
|
|
The client then asks the layout inspector for the inspect node's flexbox and grid information. As noted above, we have
|
|
just stubbed this for now. The server replies with empty values:
|
|
|
|
```jsonc
|
|
>> {"type":"getCurrentFlexbox","node":"server0-node32","onlyLookAtParents":false,"to":"server0-layout-inspector568"}
|
|
>> {"type":"getGrids","rootNode":"server0-node15","to":"server0-layout-inspector568"}
|
|
|
|
<< {"from":"server0-layout-inspector568","flexbox":null}
|
|
<< {"from":"server0-layout-inspector568","grids":[]}
|
|
```
|
|
|
|
The client then asks if the inspected node's box model metrics are editable, and for the node's "offset parent". We
|
|
currently don't support such edits, and have stubbed the offset parent. The server again replies with empty values:
|
|
|
|
```jsonc
|
|
>> {"type":"isPositionEditable","node":"server0-node32","to":"server0-page-style12"}
|
|
<< {"from":"server0-page-style12","value":false}
|
|
|
|
>> {"type":"getOffsetParent","node":"server0-node32","to":"server0-walker14"}
|
|
<< {"from":"server0-walker14","node":null}
|
|
```
|
|
|
|
If the user switches the inspector to the "Computed" pane, then the client will ask for the inspected node's computed
|
|
style. The server collects the computed style from the WebContent process, and replies with that style:
|
|
|
|
(For brevity, only the `color` property is included here, as the list of all styles is very large.)
|
|
|
|
```jsonc
|
|
>> {"type":"getComputed","node":"server0-node32","markMatched":true,"onlyMatched":true,"filter":"user","to":"server0-page-style12"}
|
|
<< {"from":"server0-page-style12","computed":{"color":{"matched":true,"value":"canvastext"}}}
|
|
```
|
|
|
|
The user may select any other DOM node to inspect. As the mouse hovers over other DOM nodes, the client will ask (once)
|
|
if the server supports highlighting DOM nodes. We do support this, and the server will reply:
|
|
|
|
```jsonc
|
|
>> {"type":"supportsHighlighters","to":"server0-inspector7"}
|
|
<< {"from":"server0-inspector7","value":true}
|
|
```
|
|
|
|
The client will then asks for box model highlighter. This highlighter indicates when the WebContent process should
|
|
render an overlay over a particular node, with information about that node's box model. The server will create this
|
|
highlighter type, and reply:
|
|
|
|
```jsonc
|
|
>> {"type":"getHighlighterByType","typeName":"BoxModelHighlighter","to":"server0-inspector7"}
|
|
<< {"from":"server0-inspector7","highlighter":{"actor":"server0-highlighter569"}}
|
|
```
|
|
|
|
When the user's mouse enters a DOM node, the client will instruct the server to show the box model overlay for that node:
|
|
```jsonc
|
|
>> {"type":"show","node":"server0-node16","to":"server0-highlighter569"}
|
|
<< {"from":"server0-highlighter569","value":true}
|
|
```
|
|
|
|
When the user's mouse leaves a DOM node, the client will instruct the server to stop showing the box model overlay for
|
|
that node:
|
|
|
|
```jsonc
|
|
>> {"type":"hide","to":"server0-highlighter569"}
|
|
<< {"from":"server0-highlighter569"}
|
|
```
|
|
|
|
The above highlighter requests are repeated as the user's mouse is moved between nodes. When the user clicks on a DOM
|
|
node, that node then becomes the inspected node, and the client will ask for the box model / computed style for that
|
|
node.
|
|
|
|
### JavaScript console
|
|
|
|
During the initialization of the watcher actor, the server advertises to the client that it supports the console. The
|
|
client will then ask the server to begin some console-related listeners, which we do not yet support:
|
|
|
|
```jsonc
|
|
>> {"type":"startListeners","listeners":["DocumentEvents"],"to":"server0-console8"}
|
|
<< {"from":"server0-console8","error":"unrecognizedPacketType","message":"Unrecognized packet type: 'startListeners'"}
|
|
|
|
>> {"type":"startListeners","listeners":["PageError"],"to":"server0-console8"}
|
|
<< {"from":"server0-console8","error":"unrecognizedPacketType","message":"Unrecognized packet type: 'startListeners'"}
|
|
```
|
|
|
|
When the user enters text into the console, the client will ask the server for autocompletions and eager evaluation of
|
|
the input text. We do not yet support either of these features, so the server replies with some stubbed information:
|
|
|
|
```jsonc
|
|
>> {"type":"autocomplete","text":"1+","frameActor":null,"authorizedEvaluations":[],"expressionVars":[],"to":"server0-console8"}
|
|
>> {"type":"evaluateJSAsync","text":"1+","eager":true,"to":"server0-console8"}
|
|
|
|
<< {"from":"server0-console8","matches":[],"matchProp":""}
|
|
<< {"from":"server0-console8","resultID":"server0-console8-1"}
|
|
```
|
|
|
|
When the script is finally executed by the user, the client will ask for a non-eager evaluation of the script. At this
|
|
point, the server will send the script to the WebContent process for evaluation. Its result is serialized to a JSON
|
|
value, which is called a [grip](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#grips). We
|
|
currently only support serialization of plain types; objects are serialized to a string for now. Since the request is
|
|
async, the server replies immediately with a pending result ID for the script. The actual result associated with that
|
|
ID is then sent once the script is complete:
|
|
|
|
```jsonc
|
|
>> {"type":"evaluateJSAsync","text":"1+1","disableBreaks":false,"to":"server0-console8"}
|
|
<< {"from":"server0-console8","resultID":"server0-console8-3"}
|
|
<< {"from":"server0-console8","type":"evaluationResult","timestamp":1740417141889,"resultID":"server0-console8-3","input":"1+1","result":2,"exception":null,"exceptionMessage":null,"helperResult":null}
|
|
```
|
|
|
|
Anytime console messages are evaluated by the page (`console.log`, `console.warn`, etc.), the server will independently
|
|
notify the client without any prompt. The same grip serialization used for console results is used here.
|
|
|
|
```jsonc
|
|
<< {"from":"server0-frame10","type":"resources-available-array","array":[["console-message",[{"level":"log","filename":"<eval>","line_number":1,"column_number":1,"time_stamp":1741096175644,"arguments":["hello!"]}]]]}
|
|
```
|
|
|
|
### Session termination
|
|
|
|
When the user disconnects from the DevTools server, the client will send a few cleanup messages. We currently do not
|
|
implement the first few that are sent:
|
|
|
|
```jsonc
|
|
>> {"type":"unwatchTargets","targetType":"frame","options":{},"to":"server0-watcher10"}
|
|
<< {"from":"server0-watcher10","error":"unrecognizedPacketType","message":"Unrecognized packet type: 'unwatchTargets'"}
|
|
|
|
>> {"type":"finalize","to":"server0-highlighter18"}
|
|
>> {"type":"finalize","to":"server0-highlighter573"}
|
|
<< {"from":"server0-highlighter18","error":"unrecognizedPacketType","message":"Unrecognized packet type: 'finalize'"}
|
|
<< {"from":"server0-highlighter573","error":"unrecognizedPacketType","message":"Unrecognized packet type: 'finalize'"}
|
|
```
|
|
|
|
We do implement the request to detach the frame, at which point we clear any inspected DOM node information from the
|
|
WebContent process:
|
|
|
|
```jsonc
|
|
>> {"type":"detach","to":"server0-frame14"}
|
|
<< {"from":"server0-frame14"}
|
|
```
|
|
|
|
## Inspecting the protocol with Firefox
|
|
|
|
When developing a DevTools feature, it's invaluable to see how the DevTools protocol is used by Firefox itself. First,
|
|
enable remote debugging in your Firefox profile's `about:config` page:
|
|
|
|
```
|
|
devtools.chrome.enabled = true
|
|
devtools.debugger.remote-enabled = true
|
|
```
|
|
|
|
You may wish to create a new profile for this, rather than modifying your default profile. To create a new profile from
|
|
the command line:
|
|
|
|
```bash
|
|
firefox -CreateProfile YOUR_PROFILE_NAME
|
|
```
|
|
|
|
If you are using your default profile, then close all Firefox windows. Restart Firefox via the command line, specifying
|
|
the DevTools server port:
|
|
|
|
```bash
|
|
firefox --start-debugger-server 6000
|
|
```
|
|
|
|
Or if you created a new profile, you do not need to close all Firefox windows. Instead, launch a new instance with the
|
|
`--new-instance` flag, and specify your new profile's path with the `--profile` flag:
|
|
|
|
```bash
|
|
firefox --start-debugger-server 6000 --new-instance --profile ~/snap/firefox/common/.mozilla/firefox/z20dtqwq.YOUR_PROFILE_NAME
|
|
```
|
|
|
|
After Firefox is running, we can now snoop the specified port. The Servo team has written a nice script using `tshark`
|
|
to listen on this port and pretty-print the JSON packets. You will have to install `tshark` before using the script.
|
|
|
|
https://github.com/servo/servo/blob/main/etc/devtools_parser.py
|
|
|
|
You may run into cases where the script does not log all captured packets. Since this occurs fairly regularly, it is
|
|
recommended to run this script in two phases. First, use the script to snoop on the DevTools port and write out a packet
|
|
capture. You may need to run this script with `sudo`.
|
|
|
|
```bash
|
|
./etc/devtools_parser.py -s /tmp/devtools.pcap -p 6000
|
|
```
|
|
|
|
(NOTE: It is important to only run this script *after* Firefox has started. Running the script sooner will not intercept
|
|
any DevTools packets.)
|
|
|
|
Now, use `about:debugging` to connect to the Firefox DevTools server and inspect a tab. Use the DevTools client to
|
|
exercise the feature you wish to implement.
|
|
|
|
Once complete, use `ctrl+c` on Servo's snooping script to complete the capture. The script will try to print all packets
|
|
at this point, but due to the aforementioned incomplete logging, it is likely that this will not print everything. So we
|
|
can use the script again to inspect the now-existing .pcap, and it should be able to print everything this way:
|
|
|
|
```bash
|
|
./etc/devtools_parser.py --use /tmp/devtools.pcap -p 6000
|
|
```
|
|
|
|
## Known issues
|
|
|
|
1. We occasionally fail to inspect a tab. There isn't any information logged by client or server, other than the client
|
|
indicating the session was disconnected. The following is the entire communication log from an instance of this error:
|
|
|
|
```jsonc
|
|
<< {"from":"root","applicationType":"browser","traits":{"sources":false,"highlightable":true,"customHighlighters":true,"networkMonitor":false}}
|
|
>> {"type":"connect","frontendVersion":"135.0","to":"root"}
|
|
<< {"from":"root"}
|
|
>> {"type":"getRoot","to":"root"}
|
|
<< {"from":"root","selected":0,"preferenceActor":"server0-preference2","deviceActor":"server0-device1"}
|
|
>> {"type":"getDescription","to":"server0-device1"}
|
|
<< {"from":"server0-device1","value":{"apptype":"ladybird","name":"Ladybird","brandName":"Ladybird","version":"1.0","appbuildid":"Version 1.0","platformbuildid":"Version 1.0","platformversion":"135.0","useragent":"Mozilla/5.0 (Linux; x86_64) Ladybird/1.0","os":"Linux","arch":"x86_64"}}
|
|
>> {"type":"getDescription","to":"server0-device1"}
|
|
<< {"from":"server0-device1","value":{"apptype":"ladybird","name":"Ladybird","brandName":"Ladybird","version":"1.0","appbuildid":"Version 1.0","platformbuildid":"Version 1.0","platformversion":"135.0","useragent":"Mozilla/5.0 (Linux; x86_64) Ladybird/1.0","os":"Linux","arch":"x86_64"}}
|
|
>> {"type":"getBoolPref","value":"devtools.debugger.prompt-connection","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
>> {"type":"getBoolPref","value":"browser.privatebrowsing.autostart","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
>> {"type":"getBoolPref","value":"dom.serviceWorkers.enabled","to":"server0-preference2"}
|
|
<< {"from":"server0-preference2","value":false}
|
|
>> {"type":"listAddons","iconDataURL":true,"to":"root"}
|
|
>> {"type":"listTabs","to":"root"}
|
|
<< {"from":"root","addons":[]}
|
|
<< {"from":"root","tabs":[{"actor":"server0-tab4","title":"xkcd: Atom","url":"https://xkcd.com/","browserId":1,"browsingContextID":1,"outerWindowID":1,"traits":{"watcher":true,"supportsReloadDescriptor":true}}]}
|
|
>> {"type":"getFavicon","to":"server0-tab4"}
|
|
<< {"from":"server0-tab4","favicon":null}
|
|
>> {"type":"listWorkers","to":"root"}
|
|
<< {"from":"root","workers":[]}
|
|
>> {"type":"listProcesses","to":"root"}
|
|
<< {"from":"root","processes":[{"actor":"server0-process3","id":0,"isParent":true,"isWindowlessParent":false,"traits":{"watcher":true,"supportsReloadDescriptor":true}}]}
|
|
>> {"type":"getProcess","id":0,"to":"root"}
|
|
<< {"from":"root","processDescriptor":{"actor":"server0-process3","id":0,"isParent":true,"isWindowlessParent":false,"traits":{"watcher":true,"supportsReloadDescriptor":true}}}
|
|
>> {"type":"listServiceWorkerRegistrations","to":"root"}
|
|
<< {"from":"root","registrations":[]}
|
|
```
|