summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/fakeserver/Binaryd.jsm
blob: 7502ef1e551d0aa0a78a2c1c9da8ac53b62a036e (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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */

const EXPORTED_SYMBOLS = ["BinaryServer"];

const CC = Components.Constructor;

const ServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "init"
);
const BinaryInputStream = CC(
  "@mozilla.org/binaryinputstream;1",
  "nsIBinaryInputStream",
  "setInputStream"
);

const BinaryOutputStream = CC(
  "@mozilla.org/binaryoutputstream;1",
  "nsIBinaryOutputStream",
  "setOutputStream"
);

/**
 * A binary stream-based server.
 * Listens on a socket, and whenever a new connection is made it runs
 * a user-supplied handler function.
 *
 * Example:
 * A trivial echo server (with a null daemon, so no state shared between
 * connections):
 *
 *   let echoServer = new BinaryServer(function(conn, daemon) {
 *     while(1) {
 *       let data = conn.read(1);
 *       conn.write(data);
 *     }
 *   }, null);
 *
 */

class BinaryServer {
  /**
   * The handler function should be of the form:
   * async function handlerFn(conn, daemon)
   *
   * @async
   * @callback handlerFn
   * @param {Connection} conn
   * @param {object} daemon
   *
   * The handler function runs as long as it wants - reading and writing bytes
   * (via methods on conn) until it is finished with the connection.
   * The handler simply returns to indicate the connection is done, or throws
   * an exception to indicate that something went wrong.
   * The daemon is the object which holds the server data/state, shared with
   * all connection handler. The BinaryServer doesn't do anything with daemon
   * other than passing it directly on to the handler function.
   */

  /**
   * Construct a new BinaryServer.
   *
   * @param {handlerFn} handlerFn - Function to call to handle each new connection.
   * @param {object} daemon - Object to pass on to the handler, to share state
   *                 and functionality between across connections.
   */
  constructor(handlerFn, daemon) {
    this._port = -1;
    this._handlerFn = handlerFn;
    this._daemon = daemon;
    this._listener = null; // Listening socket to accept new connections.
    this._connections = new Set();
  }

  /**
   * Starts the server running.
   *
   * @param {number} port - The port to run on (or -1 to pick one automatically).
   */
  async start(port = -1) {
    if (this._listener) {
      throw Components.Exception(
        "Server already started",
        Cr.NS_ERROR_ALREADY_INITIALIZED
      );
    }

    let socket = new ServerSocket(
      port,
      true, // Loopback only.
      -1 // Default max pending connections.
    );

    let server = this;

    socket.asyncListen({
      async onSocketAccepted(socket, transport) {
        let conn = new Connection(transport);
        server._connections.add(conn);
        try {
          await server._handlerFn(conn, server._daemon);
          // If we get here, handler completed, without error.
        } catch (e) {
          if (conn.isClosed()) {
            // if we get here, assume the error occurred because we're
            // shutting down, and ignore it.
          } else {
            // if we get here, something went wrong.
            dump("ERROR " + e.toString());
          }
        }
        conn.close();
        server._connections.delete(conn);
      },
      onStopListening(socket, status) {
        // Server is stopping, time to close any outstanding connections.
        server._connections.forEach(conn => conn.close());
        server._connections.clear();
      },
      QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
    });
    // We're running!
    this._listener = socket;
  }

  /**
   * Provides port, a read-only attribute to get which port the server
   * server is listening upon. Behaviour is undefined if server is not
   * running.
   */
  get port() {
    return this._listener.port;
  }

  /**
   * Stops the server, if it is running.
   */
  stop() {
    if (!this._listener) {
      // Already stopped.
      return;
    }
    this._listener.close();
    this._listener = null;
    // We could still be accepting new connections at this point,
    // so we wait until the onStopListening callback to tear down the
    // connections.
  }
}

/**
 * Connection wraps a nsITransport with read/write functions that are
 * javascript async, to simplify writing server handers.
 * Handlers should only need to use read() and write() from here, leaving
 * all connection management up to the BinaryServer.
 */
class Connection {
  constructor(transport) {
    this._transport = transport;
    this._input = transport.openInputStream(0, 0, 0);
    let outStream = transport.openOutputStream(0, 0, 0);
    this._output = new BinaryOutputStream(outStream);
  }

  /**
   * @returns true if close() has been called.
   */
  isClosed() {
    return this._transport === null;
  }

  /**
   * Closes the connection. Can be safely called multiple times.
   * The BinaryServer will call this - handlers don't need to worry about
   * the connection status.
   */
  close() {
    if (this.isClosed()) {
      return;
    }
    this._input.close();
    this._output.close();
    this._transport.close(Cr.NS_OK);
    this._input = null;
    this._output = null;
    this._transport = null;
  }

  /**
   * Read exactly nBytes from the connection.
   *
   * @param {number} nBytes - The number of bytes required.
   * @returns {Array.<number>} - An array containing the requested bytes.
   */
  async read(nBytes) {
    let conn = this;
    let buf = [];
    while (buf.length < nBytes) {
      let want = nBytes - buf.length;
      // A slightly odd-looking construct to wrap the listener-based
      // asyncwait() into a javascript async function.
      await new Promise((resolve, reject) => {
        try {
          conn._input.asyncWait(
            {
              onInputStreamReady(stream) {
                // how many bytes are actually available?
                let n;
                try {
                  n = stream.available();
                } catch (e) {
                  // stream was closed.
                  reject(e);
                }
                if (n > want) {
                  n = want;
                }
                let chunk = new BinaryInputStream(stream).readByteArray(n);
                Array.prototype.push.apply(buf, chunk);
                resolve();
              },
            },
            0,
            want,
            Services.tm.mainThread
          );
        } catch (e) {
          // asyncwait() failed
          reject(e);
        }
      });
    }
    return buf;
  }

  /**
   * Write data to the connection.
   *
   * @param {Array.<number>} data - The bytes to send.
   */
  async write(data) {
    // TODO: need to check outputstream for writeability here???
    // Might be an issue if we start throwing bigger chunks of data about...
    await this._output.writeByteArray(data);
  }
}