summaryrefslogtreecommitdiffstats
path: root/devtools/shared/transport/local-transport.js
blob: bff83b766648740623799af37617909cfde07422 (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
194
195
196
197
198
199
200
201
202
203
204
/* 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/. */

"use strict";

const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
const { dumpn } = DevToolsUtils;
const flags = require("resource://devtools/shared/flags.js");
const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");

loader.lazyGetter(this, "Pipe", () => {
  return Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init");
});

/**
 * An adapter that handles data transfers between the devtools client and
 * server when they both run in the same process. It presents the same API as
 * DebuggerTransport, but instead of transmitting serialized messages across a
 * connection it merely calls the packet dispatcher of the other side.
 *
 * @param other LocalDebuggerTransport
 *        The other endpoint for this debugger connection.
 *
 * @see DebuggerTransport
 */
function LocalDebuggerTransport(other) {
  this.other = other;
  this.hooks = null;

  // A packet number, shared between this and this.other. This isn't used by the
  // protocol at all, but it makes the packet traces a lot easier to follow.
  this._serial = this.other ? this.other._serial : { count: 0 };
  this.close = this.close.bind(this);
}

LocalDebuggerTransport.prototype = {
  /**
   * Transmit a message by directly calling the onPacket handler of the other
   * endpoint.
   */
  send(packet) {
    const serial = this._serial.count++;
    if (flags.wantLogging) {
      // Check 'from' first, as 'echo' packets have both.
      if (packet.from) {
        dumpn("Packet " + serial + " sent from " + JSON.stringify(packet.from));
      } else if (packet.to) {
        dumpn("Packet " + serial + " sent to " + JSON.stringify(packet.to));
      }
    }
    this._deepFreeze(packet);
    const other = this.other;
    if (other) {
      DevToolsUtils.executeSoon(
        DevToolsUtils.makeInfallible(() => {
          // Avoid the cost of JSON.stringify() when logging is disabled.
          if (flags.wantLogging) {
            dumpn(
              "Received packet " +
                serial +
                ": " +
                JSON.stringify(packet, null, 2)
            );
          }
          if (other.hooks) {
            other.hooks.onPacket(packet);
          }
        }, "LocalDebuggerTransport instance's this.other.hooks.onPacket")
      );
    }
  },

  /**
   * Send a streaming bulk packet directly to the onBulkPacket handler of the
   * other endpoint.
   *
   * This case is much simpler than the full DebuggerTransport, since there is
   * no primary stream we have to worry about managing while we hand it off to
   * others temporarily.  Instead, we can just make a single use pipe and be
   * done with it.
   */
  startBulkSend({ actor, type, length }) {
    const serial = this._serial.count++;

    dumpn("Sent bulk packet " + serial + " for actor " + actor);
    if (!this.other) {
      const error = new Error("startBulkSend: other side of transport missing");
      return Promise.reject(error);
    }

    const pipe = new Pipe(true, true, 0, 0, null);

    DevToolsUtils.executeSoon(
      DevToolsUtils.makeInfallible(() => {
        dumpn("Received bulk packet " + serial);
        if (!this.other.hooks) {
          return;
        }

        // Receiver
        new Promise(receiverResolve => {
          const packet = {
            actor,
            type,
            length,
            copyTo: output => {
              const copying = StreamUtils.copyStream(
                pipe.inputStream,
                output,
                length
              );
              receiverResolve(copying);
              return copying;
            },
            stream: pipe.inputStream,
            done: receiverResolve,
          };

          this.other.hooks.onBulkPacket(packet);
        })
          // Await the result of reading from the stream
          .then(() => pipe.inputStream.close(), this.close);
      }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket")
    );

    // Sender
    return new Promise(senderResolve => {
      // The remote transport is not capable of resolving immediately here, so we
      // shouldn't be able to either.
      DevToolsUtils.executeSoon(() => {
        return (
          new Promise(copyResolve => {
            senderResolve({
              copyFrom: input => {
                const copying = StreamUtils.copyStream(
                  input,
                  pipe.outputStream,
                  length
                );
                copyResolve(copying);
                return copying;
              },
              stream: pipe.outputStream,
              done: copyResolve,
            });
          })
            // Await the result of writing to the stream
            .then(() => pipe.outputStream.close(), this.close)
        );
      });
    });
  },

  /**
   * Close the transport.
   */
  close() {
    if (this.other) {
      // Remove the reference to the other endpoint before calling close(), to
      // avoid infinite recursion.
      const other = this.other;
      this.other = null;
      other.close();
    }
    if (this.hooks) {
      try {
        if (this.hooks.onTransportClosed) {
          this.hooks.onTransportClosed();
        }
      } catch (ex) {
        console.error(ex);
      }
      this.hooks = null;
    }
  },

  /**
   * An empty method for emulating the DebuggerTransport API.
   */
  ready() {},

  /**
   * Helper function that makes an object fully immutable.
   */
  _deepFreeze(object) {
    Object.freeze(object);
    for (const prop in object) {
      // Freeze the properties that are objects, not on the prototype, and not
      // already frozen. Note that this might leave an unfrozen reference
      // somewhere in the object if there is an already frozen object containing
      // an unfrozen object.
      if (
        object.hasOwnProperty(prop) &&
        typeof object === "object" &&
        !Object.isFrozen(object)
      ) {
        this._deepFreeze(object[prop]);
      }
    }
  },
};

exports.LocalDebuggerTransport = LocalDebuggerTransport;