summaryrefslogtreecommitdiffstats
path: root/devtools/server/startup/frame.js
blob: db14f03c1546a019c37a8cc63b780ac94f911baa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env mozilla/frame-script */

"use strict";

/* global addEventListener */

/*
 * Frame script that listens for requests to start a `DevToolsServer` for a frame in a
 * content process.  Loaded into content process frames by the main process during
 * frame-connector.js' connectToFrame.
 */

try {
  var chromeGlobal = this;

  // Encapsulate in its own scope to allows loading this frame script more than once.
  (function () {
    // In most cases, we are debugging a tab in content process, without chrome
    // privileges. But in some tests, we are attaching to privileged document.
    // Because the debugger can't be running in the same compartment than its debuggee,
    // we have to load the server in a dedicated Loader, flagged with
    // invisibleToDebugger, which will force it to be loaded in another compartment.
    let loader,
      customLoader = false;
    if (content.document.nodePrincipal.isSystemPrincipal) {
      const { useDistinctSystemPrincipalLoader } = ChromeUtils.importESModule(
        "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
      );
      loader = useDistinctSystemPrincipalLoader(chromeGlobal);
      customLoader = true;
    } else {
      // Otherwise, use the shared loader.
      loader = ChromeUtils.importESModule(
        "resource://devtools/shared/loader/Loader.sys.mjs"
      );
    }
    const { require } = loader;

    const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
    const {
      DevToolsServer,
    } = require("resource://devtools/server/devtools-server.js");

    DevToolsServer.init();
    // We want a special server without any root actor and only target-scoped actors.
    // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
    // it is going to act like a root actor without being one.
    DevToolsServer.registerActors({ target: true });

    const connections = new Map();

    const onConnect = DevToolsUtils.makeInfallible(function (msg) {
      const mm = msg.target;
      const prefix = msg.data.prefix;
      const addonId = msg.data.addonId;
      const addonBrowsingContextGroupId = msg.data.addonBrowsingContextGroupId;

      // If we try to create several frame targets simultaneously, the frame script will be loaded several times.
      // In this case a single "debug:connect" message might be received by all the already loaded frame scripts.
      // Check if the DevToolsServer already knows the provided connection prefix,
      // because it means that another framescript instance already handled this message.
      // Another "debug:connect" message is guaranteed to be emitted for another prefix,
      // so we keep the message listener and wait for this next message.
      if (DevToolsServer.hasConnectionForPrefix(prefix)) {
        return;
      }
      removeMessageListener("debug:connect", onConnect);

      const conn = DevToolsServer.connectToParent(prefix, mm);
      connections.set(prefix, conn);

      let actor;

      if (addonId) {
        const {
          WebExtensionTargetActor,
        } = require("resource://devtools/server/actors/targets/webextension.js");
        const {
          createWebExtensionSessionContext,
        } = require("resource://devtools/server/actors/watcher/session-context.js");
        const { browsingContext } = docShell;
        actor = new WebExtensionTargetActor(conn, {
          addonId,
          addonBrowsingContextGroupId,
          chromeGlobal,
          isTopLevelTarget: true,
          prefix,
          sessionContext: createWebExtensionSessionContext(
            {
              addonId,
              browsingContextID: browsingContext.id,
              innerWindowId: browsingContext.currentWindowContext.innerWindowId,
            },
            {
              isServerTargetSwitchingEnabled:
                msg.data.isServerTargetSwitchingEnabled,
            }
          ),
        });
      } else {
        const {
          WindowGlobalTargetActor,
        } = require("resource://devtools/server/actors/targets/window-global.js");
        const {
          createBrowserElementSessionContext,
        } = require("resource://devtools/server/actors/watcher/session-context.js");

        const { docShell } = chromeGlobal;
        // For a script loaded via loadFrameScript, the global is the content
        // message manager.
        // All WindowGlobalTarget actors created via the framescript are top-level
        // targets. Non top-level WindowGlobalTarget actors are all created by the
        // DevToolsFrameChild actor.
        //
        // createBrowserElementSessionContext only reads browserId attribute
        const fakeBrowserElement = {
          browserId: docShell.browsingContext.browserId,
        };
        actor = new WindowGlobalTargetActor(conn, {
          docShell,
          isTopLevelTarget: true,
          // This is only used when server target switching is off and we create
          // the target from TabDescriptor. So all config attributes are false.
          sessionContext: createBrowserElementSessionContext(
            fakeBrowserElement,
            {}
          ),
        });
      }
      actor.manage(actor);

      sendAsyncMessage("debug:actor", { actor: actor.form(), prefix });
    });

    addMessageListener("debug:connect", onConnect);

    const onDisconnect = DevToolsUtils.makeInfallible(function (msg) {
      const prefix = msg.data.prefix;
      const conn = connections.get(prefix);
      if (!conn) {
        // Several copies of this frame script can be running for a single frame since it
        // is loaded once for each DevTools connection to the frame.  If this disconnect
        // request doesn't match a connection known here, ignore it.
        return;
      }

      removeMessageListener("debug:disconnect", onDisconnect);
      // Call DevToolsServerConnection.close to destroy all child actors. It should end up
      // calling DevToolsServerConnection.onTransportClosed that would actually cleanup all actor
      // pools.
      conn.close();
      connections.delete(prefix);
    });
    addMessageListener("debug:disconnect", onDisconnect);

    // In non-e10s mode, the "debug:disconnect" message isn't always received before the
    // messageManager connection goes away.  Watching for "unload" here ensures we close
    // any connections when the frame is unloaded.
    addEventListener("unload", () => {
      for (const conn of connections.values()) {
        conn.close();
      }
      connections.clear();
    });

    // Destroy the server once its last connection closes. Note that multiple frame
    // scripts may be running in parallel and reuse the same server.
    function destroyLoader() {
      // Only destroy the server if there is no more connections to it. It may be used
      // to debug another tab running in the same process.
      if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
        return;
      }
      DevToolsServer.off("connectionchange", destroyLoader);

      // When debugging chrome pages, we initialized a dedicated loader, also destroy it
      if (customLoader) {
        const { releaseDistinctSystemPrincipalLoader } =
          ChromeUtils.importESModule(
            "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
          );
        releaseDistinctSystemPrincipalLoader(chromeGlobal);
      }
    }
    DevToolsServer.on("connectionchange", destroyLoader);
  })();
} catch (e) {
  dump(`Exception in DevTools frame startup: ${e}\n`);
}