createTargetForTab, step by step
-
DebuggerServer.init() This actually doesn't do much. It creates a _connections map/object, initializes the actor registry with this map (currently empty). Despite the name, this mostly does nothing.
-
DebuggerServer.registerAllActors() This does two things. First it registers all the browser and target actors in the actor-registry. This will allow to dynamically create any of those actors later on. Then it creates a factory for the root actor. In the same way as the other actor, the root actor is not created here, but a factory is prepared and we will invoke it as soon as we establish connections for this Server
-
new DebuggerClient(transport) We will now create a DebuggerClient. The DebuggerClient is one of the main entry points to request information from our DebuggerServer, before we get access to Fronts which can use protocol.js to avoid building complicated requests. Even though this DebuggerClient will live in the same process as our server, we will not directly call one from the other. In remote device debugging, the DebuggerClient will not be in the same process as the DebuggerServer. Using an abstraction layer in all situations makes the transition easier.
-
communication using transports Transports are classes implementing ready, startBulkSend, send and close. They are designed to exchange messages and always work as couples. You create two instances of a transport, put instance A in an environment, instance B in another environment, and they should be able to talk to each other. We have implementations that can talk using Message Managers, others are using WebSocket. Each transport normally has a hook property, which is an object that should implement onPacket() and onClosed(). If we want a real life metaphore, a transport is a phones, and a hook is a person talking.
-
DebuggerServer.connectPipe(): create transports ConnectPipe will create two instances of LocalTransport. One will be the "server" transport, the other will be the "client" transport. Their communication implementation is trivial, but they are consistent with the transports used in more complex scenarios.
-
DebuggerServer.connectPipe(): create connection After the transports are ready, we create a DebuggerServerConnection. This will be the hook for the "server" local transport. It implements onPacket, and has all the logic that will allow to forward packets from one server to another. We add this "connection" in our DebuggerServer's map of connections. The DebuggerServerConnection is also handling a map of actors
-
DebuggerServer.connectPipe(): create root actor Using the createRootFactory we built a few steps ago we finally create a root actor, and we immediately send a "greeting" packet on our transport. However right, no one is listening on the other side?
-
new DebuggerClient(transport) Using the "client" LocalTransport created by connectPipe(), we finish the instantiation of DebuggerClient, which will be the hook of this "client" transport. This means our LocalTransport setup will be connecting DebuggerClient and DebuggerServerConnection. Right now everything is still happening in the same process, we haven't started the fun yet.
-
client.connect() ? There is a very fragile sequence of events on which I would like to insist quickly. After building the DebuggerClient, we immediately await client.connect(). This "connect" will only resolve when we capture the "connected" event on the DebuggerClient, which is fired when we receive a first payload from the "root" actor, thanks to an "expectReply" call made in the constructor. When is this payload from "root" actor sent? Two steps ago! Thankfully the send method of LocalTransport is async, so the packet will be sent only after we create the DebuggerClient. But that is still very fragile. Once we get our first payload from the root actor, we create a root-client.
-
client.getTab() We will finally start trying to connect to the actual tab we want to debug! client.getTab() calls rootClient.getTab(). The split between the two classes is not really meaningful, but in the end create a request for the root actor, with a given tabId as parameter. Basically { to: "root", type: "getTab", tabId: 12 }. In the end this request is crafted, queued, unqueued by the debuggerClient, which finally calls send() on its "client" transport instance.
-
DebuggerServerConnection.onPacket Since DebuggerClient talks to DebuggerServerConnection, we receive the getTab request in the onPacket method of DebuggerServerConnection. Here we have a straightforward case. We have a target, "root", a method "getTab". The DebuggerServerConnection already knows the RootActor, so it simply calls the appropriate method on the actor.
-
RootActor.onGetTab Our root actor was created via webbrowser.js createRootActor, and the implementation of getTab relies on webbrowser.js's BrowserTabList class. And when you getTab on this class, you don't just create an actor to represent the tab(or frame or whatever). You create a FrameTargetActorProxy. The role of this FrameTargetActorProxy is to prepare the frame for debugging. To do so, it calls DebuggerServer.connectToFrame
-
DebuggerServer.connectToFrame We are finally about to leave our parent process. Keep in mind all the objects we have seen so far, they all live in the same process, they can all talk to each other as easily as possible. connectToFrame is a wild beast. A 250 lines js function that could easily be in its own file. The goal of this method will be to spawn another DebuggerServer in the content process for the tab we want to debug and to have transports ready to talk between the parent DebuggerServerConnection and the content DebuggerServerConnection.
-
mm.loadFrameScript("resource://devtools/server/startup/frame.js", false); Using the message manager, we load a script in the content process we want to debug. This script (frame.js) will initialize the DebuggerServer and register target actors. It will listen to a few messages on the message manager. We cannot use a transport to send information just yet, we first need to create the content process transport. First we listen to "debug:connect"
-
mm.sendAsyncMessage("debug:connect", { prefix, addonId }); Loading the framescript is synchronous, so after setting up some message manager listeners in the parent DebuggerServer, we send "debug:connect". When the framescript receives the message, it will call DebuggerServer.connectToParent. This will create a message-manager based transport and a DebuggerServerConnection linked to this transport. Once this is done, we create a FrameTargetActor and send its form back via the message-manager, using the "debug:actor" message
-
sendAsyncMessage("debug:actor", {actor: actor.form(), prefix: prefix}); We are back to the parent process connectToFrame method, with a message listener on "debug:actor". When receiving the message, we create the counterpart transport that will be able to communicate to the content process transport. We create a small "hooks" object that will call send on the DebuggerServerConnection when receiving a packet. This means that when a packet is received from the content process, our parent process DebuggerServerConnection will send them to the DebuggerClient via the LocalTransport.