From eaade0bb8cb754ff4cec97fb7fbd469b65cb8a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 9 Nov 2021 14:24:22 +0100 Subject: [PATCH 01/81] Switch to `streamx` and port tests to Brittle --- .gitignore | 2 + README.md | 4 +- index.js | 405 +++++++++++++++++++++++----------------------- lib/connection.js | 355 ++++++++++++++++++++-------------------- package.json | 10 +- test/net.js | 128 +++++++-------- test/sockets.js | 87 +++++----- test/timeouts.js | 21 +-- test/udp.js | 65 ++++---- 9 files changed, 543 insertions(+), 534 deletions(-) diff --git a/.gitignore b/.gitignore index 8f8bb55..c749896 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules +package-lock.json build +coverage sandbox.js sandbox/ diff --git a/README.md b/README.md index a56e180..62901b0 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ receives a client message because of that. The socket api allows you to reuse the same underlying UDP socket to both connect to other clients on accept incoming connections. It also mimicks the node core [dgram socket](https://nodejs.org/api/dgram.html#dgram_class_dgram_socket) api. -#### `socket = utp([options])` +#### `socket = new utp.Socket([options])` Create a new utp socket. @@ -249,7 +249,7 @@ npm install To rebuild it simply do: ```sh -node-gyp build +npx node-gyp-build ``` ## License diff --git a/index.js b/index.js index d6a4414..b791c9e 100644 --- a/index.js +++ b/index.js @@ -1,265 +1,270 @@ const binding = require('./lib/binding') const Connection = require('./lib/connection') -const util = require('util') -const events = require('events') +const EventEmitter = require('events') const dns = require('dns') const set = require('unordered-set') const EMPTY = Buffer.alloc(0) const IPv4Pattern = /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ -module.exports = UTP - -function UTP (opts) { - if (!(this instanceof UTP)) return new UTP(opts) - events.EventEmitter.call(this) - - this.connections = [] - - this._sending = [] - this._sent = [] - this._offset = 0 - this._buffer = Buffer.allocUnsafe(2 * 65536) - this._handle = Buffer.alloc(binding.sizeof_utp_napi_t) - this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) - this._address = null - this._inited = false - this._refed = true - this._closing = false - this._closed = false - this._allowHalfOpen = !opts || opts.allowHalfOpen !== false - this._acceptConnections = new Uint32Array(this._handle.buffer, this._handle.byteOffset + binding.offsetof_utp_napi_t_accept_connections, 1) - this.maxConnections = 0 -} +class Socket extends EventEmitter { + constructor (opts) { + super() -util.inherits(UTP, events.EventEmitter) + this.connections = [] -UTP.createServer = function (opts, onconnection) { - if (typeof opts === 'function') return UTP.createServer(null, opts) - const server = new UTP(opts) - if (onconnection) server.on('connection', onconnection) - return server -} + this._sending = [] + this._sent = [] + this._offset = 0 + this._buffer = Buffer.allocUnsafe(2 * 65536) + this._handle = Buffer.alloc(binding.sizeof_utp_napi_t) + this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) + this._address = null + this._inited = false + this._refed = true + this._closing = false + this._closed = false + this._allowHalfOpen = !opts || opts.allowHalfOpen !== false + this._acceptConnections = new Uint32Array(this._handle.buffer, this._handle.byteOffset + binding.offsetof_utp_napi_t_accept_connections, 1) + this.maxConnections = 0 + } -UTP.connect = function (port, host, opts) { - const udp = new UTP(opts) - return udp.connect(port, host).on('close', ononeoffclose) -} + _init () { + this._inited = true -UTP.prototype._init = function () { - this._inited = true + binding.utp_napi_init(this._handle, this, + this._nextConnection, + this._buffer, + this._onmessage, + this._onsend, + this._onconnection, + this._onclose, + this._realloc + ) - binding.utp_napi_init(this._handle, this, - this._nextConnection, - this._buffer, - this._onmessage, - this._onsend, - this._onconnection, - this._onclose, - this._realloc - ) + if (!this._refed) this.unref() + } - if (!this._refed) this.unref() -} + firewall (yes) { + this._acceptConnections[0] = yes ? 0 : 1 + } -UTP.prototype.firewall = function (yes) { - this._acceptConnections[0] = yes ? 0 : 1 -} + ref () { + if (this._inited) binding.utp_napi_ref(this._handle) + this._refed = true + } -UTP.prototype.ref = function () { - if (this._inited) binding.utp_napi_ref(this._handle) - this._refed = true -} + unref () { + if (this._inited) binding.utp_napi_unref(this._handle) + this._refed = false + } -UTP.prototype.unref = function () { - if (this._inited) binding.utp_napi_unref(this._handle) - this._refed = false -} + address () { + if (!this._address || this._closing) throw new Error('Socket not bound') + return { + address: this._address, + family: 'IPv4', + port: binding.utp_napi_local_port(this._handle) + } + } -UTP.prototype.address = function () { - if (!this._address || this._closing) throw new Error('Socket not bound') - return { - address: this._address, - family: 'IPv4', - port: binding.utp_napi_local_port(this._handle) + getRecvBufferSize () { + if (!this._inited) throw new Error('getRecvBufferSize EBADF') + if (this._closing) return 0 + return binding.utp_napi_recv_buffer(this._handle, 0) } -} -UTP.prototype.getRecvBufferSize = function () { - if (!this._inited) throw new Error('getRecvBufferSize EBADF') - if (this._closing) return 0 - return binding.utp_napi_recv_buffer(this._handle, 0) -} + setRecvBufferSize (n) { + if (!this._inited) throw new Error('setRecvBufferSize EBADF') + if (this._closing) return 0 + return binding.utp_napi_recv_buffer(this._handle, n) + } -UTP.prototype.setRecvBufferSize = function (n) { - if (!this._inited) throw new Error('setRecvBufferSize EBADF') - if (this._closing) return 0 - return binding.utp_napi_recv_buffer(this._handle, n) -} + getSendBufferSize () { + if (!this._inited) throw new Error('getSendBufferSize EBADF') + if (this._closing) return 0 + return binding.utp_napi_send_buffer(this._handle, 0) + } -UTP.prototype.getSendBufferSize = function () { - if (!this._inited) throw new Error('getSendBufferSize EBADF') - if (this._closing) return 0 - return binding.utp_napi_send_buffer(this._handle, 0) -} + setSendBufferSize (n) { + if (!this._inited) throw new Error('setSendBufferSize EBADF') + if (this._closing) return 0 + return binding.utp_napi_send_buffer(this._handle, n) + } -UTP.prototype.setSendBufferSize = function (n) { - if (!this._inited) throw new Error('setSendBufferSize EBADF') - if (this._closing) return 0 - return binding.utp_napi_send_buffer(this._handle, n) -} + setTTL (ttl) { + if (!this._inited) throw new Error('setTTL EBADF') + if (this._closing) return + binding.utp_napi_set_ttl(this._handle, ttl) + } -UTP.prototype.setTTL = function (ttl) { - if (!this._inited) throw new Error('setTTL EBADF') - if (this._closing) return - binding.utp_napi_set_ttl(this._handle, ttl) -} + send (buf, offset, len, port, host, cb) { + if (!cb) cb = noop + if (!isIP(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) + if (this._closing) return process.nextTick(cb, new Error('Socket is closed')) + if (!this._address) this.bind(0) -UTP.prototype.send = function (buf, offset, len, port, host, cb) { - if (!cb) cb = noop - if (!isIP(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) - if (this._closing) return process.nextTick(cb, new Error('Socket is closed')) - if (!this._address) this.bind(0) + var send = this._sent.pop() + if (!send) { + send = new SendRequest() + binding.utp_napi_send_request_init(send._handle, send) + } - var send = this._sent.pop() - if (!send) { - send = new SendRequest() - binding.utp_napi_send_request_init(send._handle, send) + send._index = this._sending.push(send) - 1 + send._buffer = buf + send._callback = cb + + binding.utp_napi_send(this._handle, send._handle, send._buffer, offset, len, port, host) } - send._index = this._sending.push(send) - 1 - send._buffer = buf - send._callback = cb + _resolveAndSend (buf, offset, len, port, host, cb) { + const self = this - binding.utp_napi_send(this._handle, send._handle, send._buffer, offset, len, port, host) -} + dns.lookup(host, { family: 4 }, onlookup) -UTP.prototype._resolveAndSend = function (buf, offset, len, port, host, cb) { - const self = this + function onlookup (err, ip) { + if (err) return cb(err) + if (!ip) return cb(new Error('Could not resolve ' + host)) + self.send(buf, offset, len, port, ip, cb) + } + } - dns.lookup(host, onlookup) + close (onclose) { + if (this._closed) return process.nextTick(callOnClose, this, onclose) + if (onclose) this.once('close', onclose) + if (this._closing) return + this._closing = true + this._closeMaybe() + } - function onlookup (err, ip) { - if (err) return cb(err) - if (!ip) return cb(new Error('Could not resolve ' + host)) - self.send(buf, offset, len, port, ip, cb) + _closeMaybe () { + if (this._closing && !this.connections.length && !this._sending.length && this._inited && !this._closed) { + this._closed = true + binding.utp_napi_close(this._handle) + } } -} -UTP.prototype.close = function (onclose) { - if (this._closed) return process.nextTick(callOnClose, this, onclose) - if (onclose) this.once('close', onclose) - if (this._closing) return - this._closing = true - this._closeMaybe() -} + connect (port, ip) { + if (!this._inited) this.bind() + if (!ip) ip = '127.0.0.1' + const conn = new Connection(this, port, ip, null, this._allowHalfOpen) + if (!isIP(ip)) conn._resolveAndConnect(port, ip) + else conn._connect(port, ip || '127.0.0.1') + return conn + } -UTP.prototype._closeMaybe = function () { - if (this._closing && !this.connections.length && !this._sending.length && this._inited && !this._closed) { - this._closed = true - binding.utp_napi_close(this._handle) + listen (port, ip, onlistening) { + if (!this._address) this.bind(port, ip, onlistening) + this.firewall(false) } -} -UTP.prototype.connect = function (port, ip) { - if (!this._inited) this.bind() - if (!ip) ip = '127.0.0.1' - const conn = new Connection(this, port, ip, null, this._allowHalfOpen) - if (!isIP(ip)) conn._resolveAndConnect(port, ip) - else conn._connect(port, ip || '127.0.0.1') - return conn -} + bind (port, ip, onlistening) { + if (typeof port === 'function') return this.bind(0, null, port) + if (typeof ip === 'function') return this.bind(port, null, ip) + if (!port) port = 0 + if (!ip) ip = '0.0.0.0' -UTP.prototype.listen = function (port, ip, onlistening) { - if (!this._address) this.bind(port, ip, onlistening) - this.firewall(false) -} + if (!this._inited) this._init() + if (this._closing) return + + if (this._address) { + this.emit('error', new Error('Socket already bound')) + return + } -UTP.prototype.bind = function (port, ip, onlistening) { - if (typeof port === 'function') return this.bind(0, null, port) - if (typeof ip === 'function') return this.bind(port, null, ip) - if (!port) port = 0 - if (!ip) ip = '0.0.0.0' + if (onlistening) this.once('listening', onlistening) + if (!isIP(ip)) return this._resolveAndBind(port, ip) - if (!this._inited) this._init() - if (this._closing) return + this._address = ip - if (this._address) { - this.emit('error', new Error('Socket already bound')) - return + try { + binding.utp_napi_bind(this._handle, port, ip) + } catch (err) { + this._address = null + process.nextTick(emitError, this, err) + return + } + + process.nextTick(emitListening, this) } - if (onlistening) this.once('listening', onlistening) - if (!isIP(ip)) return this._resolveAndBind(port, ip) + _resolveAndBind (port, host) { + const self = this - this._address = ip + dns.lookup(host, { family: 4 }, function (err, ip) { + if (err) return self.emit('error', err) + self.bind(port, ip) + }) + } - try { - binding.utp_napi_bind(this._handle, port, ip) - } catch (err) { - this._address = null - process.nextTick(emitError, this, err) - return + _realloc () { + this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer } - process.nextTick(emitListening, this) -} + _onmessage (size, port, address) { + if (size < 0) { + this.emit('error', new Error('Read failed (status: ' + size + ')')) + return EMPTY + } -UTP.prototype._resolveAndBind = function (port, host) { - const self = this + const message = this._buffer.slice(this._offset, this._offset += size) + this.emit('message', message, { address, family: 'IPv4', port }) - dns.lookup(host, function (err, ip) { - if (err) return self.emit('error', err) - self.bind(port, ip) - }) -} + if (this._buffer.length - this._offset <= 65536) { + this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer + } -UTP.prototype._realloc = function () { - this._buffer = Buffer.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer -} - -UTP.prototype._onmessage = function (size, port, address) { - if (size < 0) { - this.emit('error', new Error('Read failed (status: ' + size + ')')) return EMPTY } - const message = this._buffer.slice(this._offset, this._offset += size) - this.emit('message', message, { address, family: 'IPv4', port }) + _onsend (send, status) { + const cb = send._callback - if (this._buffer.length - this._offset <= 65536) { - this._buffer = Buffer.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer - } + send._callback = send._buffer = null + set.remove(this._sending, send) + this._sent.push(send) + if (this._closing) this._closeMaybe() - return EMPTY -} + cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + } -UTP.prototype._onsend = function (send, status) { - const cb = send._callback + _onconnection (port, addr) { + const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) + process.nextTick(emitConnection, this, conn) + this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) + return this._nextConnection + } - send._callback = send._buffer = null - set.remove(this._sending, send) - this._sent.push(send) - if (this._closing) this._closeMaybe() + _onclose () { + binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) + this._handle = null + this.emit('close') + } +} - cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) +function createServer (opts, onconnection) { + if (typeof opts === 'function') { + onconnection = opts + opts = {} + } + const server = new Socket(opts) + if (onconnection) server.on('connection', onconnection) + return server } -UTP.prototype._onconnection = function (port, addr) { - const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) - process.nextTick(emitConnection, this, conn) - this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) - return this._nextConnection +function connect (port, host, opts) { + const udp = new Socket(opts) + return udp.connect(port, host).on('close', ononeoffclose) } -UTP.prototype._onclose = function () { - binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) - this._handle = null - this.emit('close') +module.exports = { + Socket, + createServer, + connect } function SendRequest () { diff --git a/lib/connection.js b/lib/connection.js index 122f0bc..6a5287a 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,6 +1,5 @@ const binding = require('./binding') -const stream = require('readable-stream') -const util = require('util') +const { Duplex } = require('streamx') const unordered = require('unordered-set') const dns = require('dns') const timeout = require('timeout-refresh') @@ -13,220 +12,218 @@ const UTP_ERRORS = [ 'UTP_UNKNOWN' ] -module.exports = Connection - -function Connection (utp, port, address, handle, halfOpen) { - stream.Duplex.call(this) - - this.remoteAddress = address - this.remoteFamily = 'IPv4' - this.remotePort = port - this.destroyed = false - - this._index = -1 - this._utp = utp - this._handle = handle || Buffer.alloc(binding.sizeof_utp_napi_connection_t) - this._buffer = Buffer.allocUnsafe(65536 * 2) - this._offset = 0 - this._view = new Uint32Array(this._handle.buffer, this._handle.byteOffset, 2) - this._callback = null - this._writing = null - this._error = null - this._connected = false - this._needsConnect = !handle - this._timeout = null - this._contentSize = 0 - this._allowOpen = halfOpen ? 2 : 1 - - this.on('finish', this._shutdown) - - binding.utp_napi_connection_init(this._handle, this, this._buffer, - this._onread, - this._ondrain, - this._onend, - this._onerror, - this._onclose, - this._onconnect, - this._realloc - ) - - unordered.add(utp.connections, this) - if (utp.maxConnections && utp.connections.length >= utp.maxConnections) { - utp.firewall(true) - } -} - -util.inherits(Connection, stream.Duplex) - -Connection.prototype.setTimeout = function (ms, ontimeout) { - if (ontimeout) this.once('timeout', ontimeout) - if (this._timeout) this._timeout.destroy() - this._timeout = timeout(ms, this._ontimeout, this) -} - -Connection.prototype._ontimeout = function () { - this.emit('timeout') -} +module.exports = class Connection extends Duplex { + constructor (utp, port, address, handle, halfOpen) { + super() -Connection.prototype.setInteractive = function (interactive) { - this.setPacketSize(this.interactive ? 0 : 65536) -} + this.remoteAddress = address + this.remoteFamily = 'IPv4' + this.remotePort = port -Connection.prototype.setContentSize = function (size) { - this._view[0] = size < 65536 ? (size >= 0 ? size : 0) : 65536 - this._contentSize = size -} - -Connection.prototype.setPacketSize = function (size) { - if (size > 65536) size = 65536 - this._view[0] = size - this._contentSize = 0 -} + this._index = -1 + this._utp = utp + this._handle = handle || Buffer.alloc(binding.sizeof_utp_napi_connection_t) + this._buffer = Buffer.allocUnsafe(65536 * 2) + this._offset = 0 + this._view = new Uint32Array(this._handle.buffer, this._handle.byteOffset, 2) + this._callback = null + this._writing = null + this._error = null + this._connected = false + this._needsConnect = !handle + this._timeout = null + this._contentSize = 0 + this._allowOpen = halfOpen ? 2 : 1 + + this.on('finish', this._shutdown) + + binding.utp_napi_connection_init(this._handle, this, this._buffer, + this._onread.bind(this), + this._ondrain.bind(this), + this._onend.bind(this), + this._onerror.bind(this), + this._onclose.bind(this), + this._onconnect.bind(this), + this._realloc.bind(this) + ) + + unordered.add(utp.connections, this) + if (utp.maxConnections && utp.connections.length >= utp.maxConnections) { + utp.firewall(true) + } + } -Connection.prototype.address = function () { - if (this.destroyed) return null - return this._utp.address() -} + setTimeout (ms, ontimeout) { + if (ontimeout) this.once('timeout', ontimeout) + if (this._timeout) this._timeout.destroy() + this._timeout = timeout(ms, this._ontimeout, this) + } -Connection.prototype._read = function () { - // TODO: backpressure -} + _ontimeout () { + this.emit('timeout') + } -Connection.prototype._write = function (data, enc, cb) { - if (this.destroyed) return + setInteractive (interactive) { + this.setPacketSize(interactive ? 0 : 65536) + } - if (!this._connected || !binding.utp_napi_connection_write(this._handle, data)) { - this._callback = cb - this._writing = new Array(1) - this._writing[0] = data - return + setContentSize (size) { + this._view[0] = size < 65536 ? (size >= 0 ? size : 0) : 65536 + this._contentSize = size } - cb(null) -} + setPacketSize (size) { + if (size > 65536) size = 65536 + this._view[0] = size + this._contentSize = 0 + } -Connection.prototype._writev = function (datas, cb) { - if (this.destroyed) return + address () { + if (this.destroyed) return null + return this._utp.address() + } - const bufs = new Array(datas.length) - for (var i = 0; i < datas.length; i++) bufs[i] = datas[i].chunk + _read (cb) { + // TODO: backpressure + // cb(null) + } - if (bufs.length > 256) return this._write(Buffer.concat(bufs), null, cb) + _write (data, enc, cb) { + if (typeof data === 'string') data = Buffer.from(data, enc) + if (!this._connected || !binding.utp_napi_connection_write(this._handle, data)) { + this._callback = cb + this._writing = new Array(1) + this._writing[0] = data + return + } - if (!binding.utp_napi_connection_writev(this._handle, bufs)) { - this._callback = cb - this._writing = bufs - return + cb(null) } - cb(null) -} + _writev (datas, cb) { + const bufs = new Array(datas.length) + for (var i = 0; i < datas.length; i++) { + const data = datas[i].chunk + bufs[i] = typeof data === 'string' ? Buffer.from(data) : data + } -Connection.prototype._realloc = function () { - this._buffer = Buffer.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer -} + if (bufs.length > 256) return this._write(Buffer.concat(bufs), null, cb) -Connection.prototype._onread = function (size) { - if (!this._connected) this._onconnect() // makes the server wait for reads before writes - if (this._timeout) this._timeout.refresh() + if (!binding.utp_napi_connection_writev(this._handle, bufs)) { + this._callback = cb + this._writing = bufs + return + } - const buf = this._buffer.slice(this._offset, this._offset += size) - - if (this._contentSize) { - if (size > this._contentSize) size = this._contentSize - this._contentSize -= size - if (this._contentSize < 65536) this._view[0] = this._contentSize + cb(null) } - this.push(buf) - - // 64kb + 4kb as max package buffer is 64kb and we wanna make sure we have room for that - // plus the next udp package - if (this._buffer.length - this._offset <= 69632) { + _realloc () { this._buffer = Buffer.allocUnsafe(this._buffer.length) this._offset = 0 return this._buffer } - return EMPTY -} + _onread (size) { + if (!this._connected) this._onconnect() // makes the server wait for reads before writes + if (this._timeout) this._timeout.refresh() -Connection.prototype._ondrain = function () { - this._writing = null - const cb = this._callback - this._callback = null - cb(null) -} + const buf = this._buffer.slice(this._offset, this._offset += size) + + if (this._contentSize) { + if (size > this._contentSize) size = this._contentSize + this._contentSize -= size + if (this._contentSize < 65536) this._view[0] = this._contentSize + } + + this.push(buf) + + // 64kb + 4kb as max package buffer is 64kb and we wanna make sure we have room for that + // plus the next udp package + if (this._buffer.length - this._offset <= 69632) { + this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer + } -Connection.prototype._onclose = function () { - unordered.remove(this._utp.connections, this) - if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { - this._utp.firewall(false) + return EMPTY } - this._handle = null - if (this._error) this.emit('error', this._error) - this.emit('close') - this._utp._closeMaybe() -} -Connection.prototype._onerror = function (status) { - this.destroy(createUTPError(status)) -} + _ondrain () { + this._writing = null + const cb = this._callback + this._callback = null + cb(null) + } -Connection.prototype._onend = function () { - if (this._timeout) this._timeout.destroy() - this.push(null) - this._destroyMaybe() -} + _onclose () { + unordered.remove(this._utp.connections, this) + if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { + this._utp.firewall(false) + } + this._handle = null + if (this._error) this.emit('error', this._error) + this.emit('close') + this._utp._closeMaybe() + } -Connection.prototype._resolveAndConnect = function (port, host) { - const self = this - dns.lookup(host, function (err, ip) { - if (err) return self.destroy(err) - if (!ip) return self.destroy(new Error('Could not resolve ' + host)) - self._connect(port, ip) - }) -} + _onerror (status) { + this.destroy(createUTPError(status)) + } -Connection.prototype._connect = function (port, ip) { - if (this.destroyed) return - this._needsConnect = false - this.remoteAddress = ip - binding.utp_napi_connect(this._utp._handle, this._handle, port, ip) -} + _onend () { + if (this._timeout) this._timeout.destroy() + this.push(null) + this._destroyMaybe() + } -Connection.prototype._onconnect = function () { - if (this._timeout) this._timeout.refresh() + _resolveAndConnect (port, host) { + const self = this + dns.lookup(host, { family: 4 }, function (err, ip) { + if (err) return self.destroy(err) + if (!ip) return self.destroy(new Error('Could not resolve ' + host)) + self._connect(port, ip) + }) + } - this._connected = true - if (this._writing) { - const cb = this._callback - const data = this._writing[0] - this._callback = null - this._writing = null - this._write(data, null, cb) + _connect (port, ip) { + if (this.destroyed) return + this._needsConnect = false + this.remoteAddress = ip + binding.utp_napi_connect(this._utp._handle, this._handle, port, ip) } - this.emit('connect') -} -Connection.prototype.destroy = function (err) { - if (this.destroyed) return - this.destroyed = true - if (err) this._error = err - if (this._needsConnect) return process.nextTick(onbindingclose, this) - binding.utp_napi_connection_close(this._handle) -} + _onconnect () { + if (this._timeout) this._timeout.refresh() + + this._connected = true + if (this._writing) { + const cb = this._callback + const data = this._writing[0] + this._callback = null + this._writing = null + this._write(data, null, cb) + } + this.emit('connect') + } -Connection.prototype._destroyMaybe = function () { - if (this._allowOpen && !--this._allowOpen) this.destroy() -} + destroy (err) { + if (this.destroyed) return + super.destroy(err) + if (err) this._error = err + if (this._needsConnect) return process.nextTick(onbindingclose, this) + binding.utp_napi_connection_close(this._handle) + } -Connection.prototype._shutdown = function () { - if (this.destroyed) return - binding.utp_napi_connection_shutdown(this._handle) - this._destroyMaybe() + _destroyMaybe () { + if (this._allowOpen && !--this._allowOpen) this.destroy() + } + + _shutdown () { + if (this.destroyed) return + binding.utp_napi_connection_shutdown(this._handle) + this._destroyMaybe() + } } function onbindingclose (self) { diff --git a/package.json b/package.json index cb8edea..401fd64 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "index.js", "gypfile": true, "scripts": { - "test-timeouts": "tape test/timeouts.js", - "test": "standard && tape test/net.js test/sockets.js test/udp.js", + "test-timeouts": "brittle test/timeouts.js", + "test": "standard && brittle test/net.js test/sockets.js test/udp.js", "install": "node-gyp-build", "fetch-libutp": "git submodule update --recursive --init", "prebuild": "prebuildify --napi --strip", @@ -21,14 +21,14 @@ "dependencies": { "napi-macros": "^2.0.0", "node-gyp-build": "^4.2.0", - "readable-stream": "^3.0.2", + "streamx": "^2.11.3", "timeout-refresh": "^1.0.0", "unordered-set": "^2.0.1" }, "devDependencies": { + "brittle": "^1.6.0", "prebuildify": "^4.1.2", - "standard": "^14.3.1", - "tape": "^4.11.0" + "standard": "^14.3.1" }, "repository": { "type": "git", diff --git a/test/net.js b/test/net.js index 77fcf6a..a1e6830 100644 --- a/test/net.js +++ b/test/net.js @@ -1,7 +1,9 @@ -const tape = require('tape') +const test = require('brittle') const utp = require('../') -tape('server + connect', function (t) { +test('server + connect', function (t) { + t.plan(1) + var connected = false const server = utp.createServer(function (socket) { @@ -17,14 +19,15 @@ tape('server + connect', function (t) { socket.destroy() server.close() t.ok(connected, 'connected successfully') - t.end() }) socket.write('hello joe') }) }) -tape('server + connect with resolve', function (t) { +test('server + connect with resolve', function (t) { + t.plan(1) + var connected = false const server = utp.createServer(function (socket) { @@ -40,15 +43,14 @@ tape('server + connect with resolve', function (t) { socket.destroy() server.close() t.ok(connected, 'connected successfully') - t.end() }) socket.write('hello joe') }) }) -tape('bad resolve', function (t) { - t.plan(2) +test('bad resolve', function (t) { + t.plan(4) const socket = utp.connect(10000, 'domain.does-not-exist') @@ -62,12 +64,11 @@ tape('bad resolve', function (t) { socket.on('close', function () { t.pass('closed') - t.end() }) }) -tape('server immediate close', function (t) { - t.plan(2) +test('server immediate close', function (t) { + t.plan(3) const server = utp.createServer(function (socket) { socket.write('hi') @@ -91,7 +92,7 @@ tape('server immediate close', function (t) { }) }) -tape.skip('only server sends', function (t) { +test.skip('only server sends', function (t) { // this is skipped because it doesn't work. // utpcat has the same issue so this seems to be a bug // in libutp it self @@ -105,19 +106,15 @@ tape.skip('only server sends', function (t) { var socket = utp.connect(server.address().port) socket.on('data', function (data) { - t.same(data, Buffer.from('hi')) + t.alike(data, Buffer.from('hi')) socket.destroy() server.close() }) }) }) -tape('server listens on a port in use', function (t) { - if (Number(process.versions.node.split('.')[0]) === 0) { - t.pass('skipping since node 0.10 forces SO_REUSEADDR') - t.end() - return - } +test('server listens on a port in use', function (t) { + t.plan(1) const server = utp.createServer() server.listen(0, function () { @@ -129,16 +126,17 @@ tape('server listens on a port in use', function (t) { server.close() server2.close() t.pass('had error') - t.end() }) }) }) -tape('echo server', function (t) { +test('echo server', function (t) { + t.plan(2) + const server = utp.createServer(function (socket) { socket.pipe(socket) socket.on('data', function (data) { - t.same(data, Buffer.from('hello')) + t.alike(data, Buffer.from('hello')) }) socket.on('end', function () { socket.end() @@ -152,20 +150,21 @@ tape('echo server', function (t) { socket.on('data', function (data) { socket.end() server.close() - t.same(data, Buffer.from('hello')) - t.end() + t.alike(data, Buffer.from('hello')) }) }) }) -tape('echo server back and fourth', function (t) { +test('echo server back and fourth', function (t) { + t.plan(12) + var echoed = 0 const server = utp.createServer(function (socket) { socket.pipe(socket) socket.on('data', function (data) { echoed++ - t.same(data, Buffer.from('hello')) + t.alike(data, Buffer.from('hello')) }) }) @@ -179,14 +178,15 @@ tape('echo server back and fourth', function (t) { if (--rounds) return socket.write(data) socket.end() server.close() - t.same(echoed, 10) - t.same(Buffer.from('hello'), data) - t.end() + t.is(echoed, 10) + t.alike(Buffer.from('hello'), data) }) }) }) -tape('echo big message', function (t) { +test('echo big message', function (t) { + t.plan(2) + var packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) @@ -212,15 +212,16 @@ tape('echo big message', function (t) { if (big.length === ptr) { socket.end() server.close() - t.same(buffer, big) + t.alike(buffer, big) t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') - t.end() } }) }) }) -tape('echo big message with setContentSize', function (t) { +test('echo big message with setContentSize', function (t) { + t.plan(2) + var packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) @@ -248,15 +249,16 @@ tape('echo big message with setContentSize', function (t) { if (big.length === ptr) { socket.end() server.close() - t.same(buffer, big) + t.alike(buffer, big) t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') - t.end() } }) }) }) -tape('two connections', function (t) { +test('two connections', function (t) { + t.plan(5) + var count = 0 var gotA = false var gotB = false @@ -275,13 +277,13 @@ tape('two connections', function (t) { socket1.on('data', function (data) { gotA = true - t.same(data, Buffer.from('a')) + t.alike(data, Buffer.from('a')) if (gotB) done() }) socket2.on('data', function (data) { gotB = true - t.same(data, Buffer.from('b')) + t.alike(data, Buffer.from('b')) if (gotA) done() }) @@ -291,13 +293,14 @@ tape('two connections', function (t) { server.close() t.ok(gotA) t.ok(gotB) - t.same(count, 2) - t.end() + t.alike(count, 2) } }) }) -tape('emits close', function (t) { +test('emits close', function (t) { + t.plan(6) + var serverClosed = false var clientClosed = false @@ -327,11 +330,12 @@ tape('emits close', function (t) { server.close() t.ok(serverClosed) t.ok(clientClosed) - t.end() } }) -tape('flushes', function (t) { +test('flushes', function (t) { + t.plan(1) + var sent = '' const server = utp.createServer(function (socket) { var buf = '' @@ -342,8 +346,7 @@ tape('flushes', function (t) { socket.on('end', function () { server.close() socket.end() - t.same(buf, sent) - t.end() + t.alike(buf, sent) }) }) @@ -357,7 +360,9 @@ tape('flushes', function (t) { }) }) -tape('close waits for connections to close', function (t) { +test('close waits for connections to close', function (t) { + t.plan(1) + var sent = '' const server = utp.createServer(function (socket) { var buf = '' @@ -367,8 +372,7 @@ tape('close waits for connections to close', function (t) { }) socket.on('end', function () { socket.end() - t.same(buf, sent) - t.end() + t.alike(buf, sent) }) server.close() }) @@ -383,11 +387,11 @@ tape('close waits for connections to close', function (t) { }) }) -tape('disable half open', function (t) { - t.plan(2) +test('disable half open', function (t) { + t.plan(3) const server = utp.createServer({ allowHalfOpen: false }, function (socket) { socket.on('data', function (data) { - t.same(data, Buffer.from('a')) + t.alike(data, Buffer.from('a')) }) socket.on('close', function () { server.close(function () { @@ -404,12 +408,9 @@ tape('disable half open', function (t) { }) }) -tape('timeout', function (t) { - t.plan(3) - - var serverClosed = false - var clientClosed = false - var missing = 2 +test('timeout', async function (t) { + const close = t.test('close') + close.plan(4) const server = utp.createServer(function (socket) { socket.setTimeout(100, function () { @@ -419,8 +420,7 @@ tape('timeout', function (t) { socket.resume() socket.write('hi') socket.on('close', function () { - serverClosed = true - done() + close.pass('server closed') }) }) @@ -432,16 +432,10 @@ tape('timeout', function (t) { socket.destroy() }) socket.on('close', function () { - clientClosed = true - done() + close.pass('client closed') }) }) - function done () { - if (--missing) return - server.close() - t.ok(clientClosed) - t.ok(serverClosed) - t.end() - } + await close + server.close() }) diff --git a/test/sockets.js b/test/sockets.js index af9a7b0..174dcb1 100644 --- a/test/sockets.js +++ b/test/sockets.js @@ -1,16 +1,17 @@ -const tape = require('tape') +const test = require('brittle') const dgram = require('dgram') const utp = require('../') -tape('dgram-like socket', function (t) { - const socket = utp() +test('dgram-like socket', function (t) { + t.plan(3) + + const socket = new utp.Socket() socket.on('message', function (buf, rinfo) { - t.same(rinfo.port, socket.address().port) - t.same(rinfo.address, '127.0.0.1') - t.same(buf, Buffer.from('hello')) + t.is(rinfo.port, socket.address().port) + t.is(rinfo.address, '127.0.0.1') + t.alike(buf, Buffer.from('hello')) socket.close() - t.end() }) socket.bind(function () { @@ -18,13 +19,14 @@ tape('dgram-like socket', function (t) { }) }) -tape('double close', function (t) { - const socket = utp() +test('double close', function (t) { + t.plan(1) + + const socket = new utp.Socket() socket.on('close', function () { socket.close(function () { t.pass('closed twice') - t.end() }) }) @@ -33,8 +35,10 @@ tape('double close', function (t) { }) }) -tape('echo socket', function (t) { - const socket = utp() +test('echo socket', function (t) { + t.plan(3) + + const socket = new utp.Socket() socket.on('message', function (buf, rinfo) { socket.send(buf, 0, buf.length, rinfo.port, rinfo.address) @@ -43,19 +47,20 @@ tape('echo socket', function (t) { socket.bind(function () { var other = dgram.createSocket('udp4') other.on('message', function (buf, rinfo) { - t.same(rinfo.port, socket.address().port) - t.same(rinfo.address, '127.0.0.1') - t.same(buf, Buffer.from('hello')) + t.is(rinfo.port, socket.address().port) + t.is(rinfo.address, '127.0.0.1') + t.alike(buf, Buffer.from('hello')) socket.close() other.close() - t.end() }) other.send(Buffer.from('hello'), 0, 5, socket.address().port, '127.0.0.1') }) }) -tape('echo socket with resolve', function (t) { - const socket = utp() +test('echo socket with resolve', function (t) { + t.plan(3) + + const socket = new utp.Socket() socket.on('message', function (buf, rinfo) { socket.send(buf, 0, buf.length, rinfo.port, 'localhost') @@ -64,25 +69,24 @@ tape('echo socket with resolve', function (t) { socket.bind(function () { const other = dgram.createSocket('udp4') other.on('message', function (buf, rinfo) { - t.same(rinfo.port, socket.address().port) - t.same(rinfo.address, '127.0.0.1') - t.same(buf, Buffer.from('hello')) + t.is(rinfo.port, socket.address().port) + t.is(rinfo.address, '127.0.0.1') + t.alike(buf, Buffer.from('hello')) socket.close() other.close() - t.end() }) other.send(Buffer.from('hello'), 0, 5, socket.address().port, '127.0.0.1') }) }) -tape('combine server and connection', function (t) { - const socket = utp() - var gotClient = false +test('combine server and connection', function (t) { + t.plan(3) + + const socket = new utp.Socket() socket.on('connection', function (client) { - gotClient = true - t.same(client.remotePort, socket.address().port) - t.same(client.remoteAddress, '127.0.0.1') + t.is(client.remotePort, socket.address().port) + t.is(client.remoteAddress, '127.0.0.1') client.pipe(client) }) @@ -90,25 +94,24 @@ tape('combine server and connection', function (t) { var client = socket.connect(socket.address().port) client.write('hi') client.on('data', function (data) { + client.end() socket.close() - client.destroy() - t.same(data, Buffer.from('hi')) - t.ok(gotClient) - t.end() + t.alike(data, Buffer.from('hi')) }) }) }) -tape('both ends write first', function (t) { - var missing = 2 - const socket = utp() +test('both ends write first', async function (t) { + const close = t.test('close') + close.plan(2) + + const socket = new utp.Socket() socket.on('connection', function (connection) { connection.write('a') connection.on('data', function (data) { - t.same(data, Buffer.from('b')) + close.alike(data, Buffer.from('b')) connection.end() - done() }) }) @@ -116,15 +119,11 @@ tape('both ends write first', function (t) { var connection = socket.connect(socket.address().port) connection.write('b') connection.on('data', function (data) { - t.same(data, Buffer.from('a')) + close.alike(data, Buffer.from('a')) connection.end() - done() }) }) - function done () { - if (--missing) return - socket.close() - t.end() - } + await close + socket.close() }) diff --git a/test/timeouts.js b/test/timeouts.js index 42f2f91..bc541fe 100644 --- a/test/timeouts.js +++ b/test/timeouts.js @@ -1,20 +1,23 @@ -const tape = require('tape') +const test = require('brittle') const dgram = require('dgram') const utp = require('../') -tape('connection timeout. this may take >20s', function (t) { +test('connection timeout. this may take >20s', function (t) { + t.plan(1) + const socket = dgram.createSocket('udp4') socket.bind(0, function () { const connection = utp.connect(socket.address().port) connection.on('error', function (err) { socket.close() - t.same(err.message, 'UTP_ETIMEDOUT') - t.end() + t.is(err.message, 'UTP_ETIMEDOUT') }) }) }) -tape('write timeout. this may take >20s', function (t) { +test('write timeout. this may take >20s', function (t) { + t.plan(3) + const server = utp.createServer() var connection @@ -34,13 +37,14 @@ tape('write timeout. this may take >20s', function (t) { t.pass('connected to server') }) connection.on('error', function (err) { - t.same(err.message, 'UTP_ETIMEDOUT') - t.end() + t.is(err.message, 'UTP_ETIMEDOUT') }) }) }) -tape('server max connections. this may take >20s', function (t) { +test('server max connections. this may take >20s', function (t) { + t.plan(4) + var inc = 0 const server = utp.createServer({ allowHalfOpen: false }, function (socket) { inc++ @@ -67,7 +71,6 @@ tape('server max connections. this may take >20s', function (t) { c.destroy() server.close() t.pass('should error') - t.end() }) }) }) diff --git a/test/udp.js b/test/udp.js index 047a61a..0c2c4cc 100644 --- a/test/udp.js +++ b/test/udp.js @@ -1,84 +1,93 @@ -const tape = require('tape') +const test = require('brittle') const utp = require('../') -tape('bind', function (t) { - const sock = utp() +test('bind', function (t) { + t.plan(4) + + const sock = new utp.Socket() sock.bind(function () { const { port, address } = sock.address() - t.same(address, '0.0.0.0') - t.same(typeof port, 'number') + t.is(address, '0.0.0.0') + t.is(typeof port, 'number') t.ok(port > 0 && port < 65536) - sock.close(() => t.end()) + sock.close(() => t.pass()) }) }) -tape('bind, close, bind', function (t) { - const sock = utp() +test('bind, close, bind', function (t) { + t.plan(6) + + const sock = new utp.Socket() sock.bind(function () { const { port, address } = sock.address() - t.same(address, '0.0.0.0') - t.same(typeof port, 'number') + t.is(address, '0.0.0.0') + t.is(typeof port, 'number') t.ok(port > 0 && port < 65536) sock.close(function () { - const otherSock = utp() + const otherSock = new utp.Socket() otherSock.bind(port, function () { const addr = otherSock.address() - t.same(addr.port, port) - t.same(addr.address, address) - otherSock.close(() => t.end()) + t.is(addr.port, port) + t.is(addr.address, address) + otherSock.close(() => t.pass()) }) }) }) }) -tape('bind after error', function (t) { - const a = utp() - const b = utp() +test('bind after error', function (t) { + t.plan(3) + + const a = new utp.Socket() + const b = new utp.Socket() a.listen(function () { b.once('error', function (err) { t.ok(err, 'should error') b.listen(function () { t.pass('should still bind') - a.close(() => b.close(() => t.end())) + a.close(() => b.close(() => t.pass())) }) }) b.listen(a.address().port) }) }) -tape('send message', function (t) { - const sock = utp() +test('send message', function (t) { + t.plan(4) + + const sock = new utp.Socket() sock.bind(0, '127.0.0.1', function () { const addr = sock.address() sock.on('message', function (message, rinfo) { - t.same(rinfo, addr) - t.same(message, Buffer.from('hello')) - sock.close(() => t.end()) + t.alike(rinfo, addr) + t.alike(message, Buffer.from('hello')) + sock.close(() => t.pass()) }) sock.send(Buffer.from('hello'), 0, 5, addr.port, addr.address, function (err) { - t.error(err, 'no error') + t.absent(err, 'no error') }) }) }) -tape('send after close', function (t) { - const sock = utp() +test('send after close', function (t) { + t.plan(2) + + const sock = new utp.Socket() sock.bind(0, '127.0.0.1', function () { const { port, address } = sock.address() sock.send(Buffer.from('hello'), 0, 5, port, address, function (err) { - t.error(err, 'no error') + t.absent(err, 'no error') sock.close(function () { sock.send(Buffer.from('world'), 0, 5, port, address, function (err) { t.ok(err, 'should error') - t.end() }) }) }) From ba054df635bf302ff5e5e0569ce6ef758599ab43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 9 Nov 2021 14:29:20 +0100 Subject: [PATCH 02/81] `streamx.Duplex` doesn't pass `enc` --- lib/connection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 6a5287a..9e638b9 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -88,8 +88,8 @@ module.exports = class Connection extends Duplex { // cb(null) } - _write (data, enc, cb) { - if (typeof data === 'string') data = Buffer.from(data, enc) + _write (data, cb) { + if (typeof data === 'string') data = Buffer.from(data) if (!this._connected || !binding.utp_napi_connection_write(this._handle, data)) { this._callback = cb this._writing = new Array(1) @@ -107,7 +107,7 @@ module.exports = class Connection extends Duplex { bufs[i] = typeof data === 'string' ? Buffer.from(data) : data } - if (bufs.length > 256) return this._write(Buffer.concat(bufs), null, cb) + if (bufs.length > 256) return this._write(Buffer.concat(bufs), cb) if (!binding.utp_napi_connection_writev(this._handle, bufs)) { this._callback = cb @@ -202,7 +202,7 @@ module.exports = class Connection extends Duplex { const data = this._writing[0] this._callback = null this._writing = null - this._write(data, null, cb) + this._write(data, cb) } this.emit('connect') } From 8dd29ba5e9b022e695980e3e30334c6c6beb4dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 9 Nov 2021 19:57:05 +0100 Subject: [PATCH 03/81] Skip segfaulting tests for now --- test/net.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/net.js b/test/net.js index a1e6830..9a5c445 100644 --- a/test/net.js +++ b/test/net.js @@ -333,7 +333,7 @@ test('emits close', function (t) { } }) -test('flushes', function (t) { +test.skip('flushes', function (t) { t.plan(1) var sent = '' @@ -360,7 +360,7 @@ test('flushes', function (t) { }) }) -test('close waits for connections to close', function (t) { +test.skip('close waits for connections to close', function (t) { t.plan(1) var sent = '' From 8a2e4dff355cecce511f6887e107d8dc29e058c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 9 Nov 2021 20:00:09 +0100 Subject: [PATCH 04/81] Feedback from @mafintosh --- index.js | 32 ++++++++++++++------------------ lib/connection.js | 25 ++++--------------------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/index.js b/index.js index b791c9e..1e9c862 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const set = require('unordered-set') const EMPTY = Buffer.alloc(0) const IPv4Pattern = /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ -class Socket extends EventEmitter { +const Socket = module.exports = class Socket extends EventEmitter { constructor (opts) { super() @@ -244,28 +244,24 @@ class Socket extends EventEmitter { this._handle = null this.emit('close') } -} -function createServer (opts, onconnection) { - if (typeof opts === 'function') { - onconnection = opts - opts = {} + static createServer (opts, onconnection) { + if (typeof opts === 'function') { + onconnection = opts + opts = {} + } + const server = new Socket(opts) + if (onconnection) server.on('connection', onconnection) + return server } - const server = new Socket(opts) - if (onconnection) server.on('connection', onconnection) - return server -} -function connect (port, host, opts) { - const udp = new Socket(opts) - return udp.connect(port, host).on('close', ononeoffclose) + static connect (port, host, opts) { + const udp = new Socket(opts) + return udp.connect(port, host).on('close', ononeoffclose) + } } -module.exports = { - Socket, - createServer, - connect -} +Socket.Socket = Socket function SendRequest () { this._handle = Buffer.alloc(binding.sizeof_utp_napi_send_request_t) diff --git a/lib/connection.js b/lib/connection.js index 9e638b9..80bb702 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -83,33 +83,16 @@ module.exports = class Connection extends Duplex { return this._utp.address() } - _read (cb) { - // TODO: backpressure - // cb(null) - } - - _write (data, cb) { - if (typeof data === 'string') data = Buffer.from(data) - if (!this._connected || !binding.utp_napi_connection_write(this._handle, data)) { - this._callback = cb - this._writing = new Array(1) - this._writing[0] = data - return - } - - cb(null) - } - _writev (datas, cb) { - const bufs = new Array(datas.length) + let bufs = new Array(datas.length) for (var i = 0; i < datas.length; i++) { - const data = datas[i].chunk + const data = datas[i] bufs[i] = typeof data === 'string' ? Buffer.from(data) : data } - if (bufs.length > 256) return this._write(Buffer.concat(bufs), cb) + if (bufs.length > 256) bufs = [Buffer.concat(bufs)] - if (!binding.utp_napi_connection_writev(this._handle, bufs)) { + if (!this._connected || !binding.utp_napi_connection_writev(this._handle, bufs)) { this._callback = cb this._writing = bufs return From 6b198d8f02155ce74a054d4e459d26be49a2a92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 09:44:31 +0100 Subject: [PATCH 05/81] Replace `Buffer` with `b4a` --- index.js | 19 ++++++++++--------- lib/connection.js | 17 +++++++++-------- package.json | 1 + 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 1e9c862..6bdd6ab 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,9 @@ const Connection = require('./lib/connection') const EventEmitter = require('events') const dns = require('dns') const set = require('unordered-set') +const b4a = require('b4a') -const EMPTY = Buffer.alloc(0) +const EMPTY = b4a.alloc(0) const IPv4Pattern = /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ const Socket = module.exports = class Socket extends EventEmitter { @@ -16,9 +17,9 @@ const Socket = module.exports = class Socket extends EventEmitter { this._sending = [] this._sent = [] this._offset = 0 - this._buffer = Buffer.allocUnsafe(2 * 65536) - this._handle = Buffer.alloc(binding.sizeof_utp_napi_t) - this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) + this._buffer = b4a.allocUnsafe(2 * 65536) + this._handle = b4a.alloc(binding.sizeof_utp_napi_t) + this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) this._address = null this._inited = false this._refed = true @@ -198,7 +199,7 @@ const Socket = module.exports = class Socket extends EventEmitter { } _realloc () { - this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._buffer = b4a.allocUnsafe(this._buffer.length) this._offset = 0 return this._buffer } @@ -209,11 +210,11 @@ const Socket = module.exports = class Socket extends EventEmitter { return EMPTY } - const message = this._buffer.slice(this._offset, this._offset += size) + const message = this._buffer.subarray(this._offset, this._offset += size) this.emit('message', message, { address, family: 'IPv4', port }) if (this._buffer.length - this._offset <= 65536) { - this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._buffer = b4a.allocUnsafe(this._buffer.length) this._offset = 0 return this._buffer } @@ -235,7 +236,7 @@ const Socket = module.exports = class Socket extends EventEmitter { _onconnection (port, addr) { const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) process.nextTick(emitConnection, this, conn) - this._nextConnection = Buffer.alloc(binding.sizeof_utp_napi_connection_t) + this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) return this._nextConnection } @@ -264,7 +265,7 @@ const Socket = module.exports = class Socket extends EventEmitter { Socket.Socket = Socket function SendRequest () { - this._handle = Buffer.alloc(binding.sizeof_utp_napi_send_request_t) + this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) this._buffer = null this._callback = null this._index = null diff --git a/lib/connection.js b/lib/connection.js index 80bb702..195781e 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -3,8 +3,9 @@ const { Duplex } = require('streamx') const unordered = require('unordered-set') const dns = require('dns') const timeout = require('timeout-refresh') +const b4a = require('b4a') -const EMPTY = Buffer.alloc(0) +const EMPTY = b4a.alloc(0) const UTP_ERRORS = [ 'UTP_ECONNREFUSED', 'UTP_ECONNRESET', @@ -22,8 +23,8 @@ module.exports = class Connection extends Duplex { this._index = -1 this._utp = utp - this._handle = handle || Buffer.alloc(binding.sizeof_utp_napi_connection_t) - this._buffer = Buffer.allocUnsafe(65536 * 2) + this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) + this._buffer = b4a.allocUnsafe(65536 * 2) this._offset = 0 this._view = new Uint32Array(this._handle.buffer, this._handle.byteOffset, 2) this._callback = null @@ -87,10 +88,10 @@ module.exports = class Connection extends Duplex { let bufs = new Array(datas.length) for (var i = 0; i < datas.length; i++) { const data = datas[i] - bufs[i] = typeof data === 'string' ? Buffer.from(data) : data + bufs[i] = typeof data === 'string' ? b4a.from(data) : data } - if (bufs.length > 256) bufs = [Buffer.concat(bufs)] + if (bufs.length > 256) bufs = [b4a.concat(bufs)] if (!this._connected || !binding.utp_napi_connection_writev(this._handle, bufs)) { this._callback = cb @@ -102,7 +103,7 @@ module.exports = class Connection extends Duplex { } _realloc () { - this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._buffer = b4a.allocUnsafe(this._buffer.length) this._offset = 0 return this._buffer } @@ -111,7 +112,7 @@ module.exports = class Connection extends Duplex { if (!this._connected) this._onconnect() // makes the server wait for reads before writes if (this._timeout) this._timeout.refresh() - const buf = this._buffer.slice(this._offset, this._offset += size) + const buf = this._buffer.subarray(this._offset, this._offset += size) if (this._contentSize) { if (size > this._contentSize) size = this._contentSize @@ -124,7 +125,7 @@ module.exports = class Connection extends Duplex { // 64kb + 4kb as max package buffer is 64kb and we wanna make sure we have room for that // plus the next udp package if (this._buffer.length - this._offset <= 69632) { - this._buffer = Buffer.allocUnsafe(this._buffer.length) + this._buffer = b4a.allocUnsafe(this._buffer.length) this._offset = 0 return this._buffer } diff --git a/package.json b/package.json index 401fd64..c10401e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "node": ">=8.12" }, "dependencies": { + "b4a": "^1.1.1", "napi-macros": "^2.0.0", "node-gyp-build": "^4.2.0", "streamx": "^2.11.3", From ccc402ef6e964182149071e834001d04ff4fae79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 09:58:06 +0100 Subject: [PATCH 06/81] Replace `process.nextTick` with `queueTick` --- index.js | 31 ++++++++----------------------- lib/connection.js | 7 ++----- package.json | 1 + 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index 6bdd6ab..49ceeb8 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const EventEmitter = require('events') const dns = require('dns') const set = require('unordered-set') const b4a = require('b4a') +const queueTick = require('queue-tick') const EMPTY = b4a.alloc(0) const IPv4Pattern = /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ @@ -102,7 +103,7 @@ const Socket = module.exports = class Socket extends EventEmitter { send (buf, offset, len, port, host, cb) { if (!cb) cb = noop if (!isIP(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) - if (this._closing) return process.nextTick(cb, new Error('Socket is closed')) + if (this._closing) return queueTick(() => cb(new Error('Socket is closed'))) if (!this._address) this.bind(0) var send = this._sent.pop() @@ -130,9 +131,9 @@ const Socket = module.exports = class Socket extends EventEmitter { } } - close (onclose) { - if (this._closed) return process.nextTick(callOnClose, this, onclose) - if (onclose) this.once('close', onclose) + close (cb) { + if (this._closed) return queueTick(() => cb && cb()) + if (cb) this.once('close', cb) if (this._closing) return this._closing = true this._closeMaybe() @@ -182,11 +183,11 @@ const Socket = module.exports = class Socket extends EventEmitter { binding.utp_napi_bind(this._handle, port, ip) } catch (err) { this._address = null - process.nextTick(emitError, this, err) + queueTick(() => this.emit('error', err)) return } - process.nextTick(emitListening, this) + queueTick(() => this.emit('listening')) } _resolveAndBind (port, host) { @@ -235,7 +236,7 @@ const Socket = module.exports = class Socket extends EventEmitter { _onconnection (port, addr) { const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) - process.nextTick(emitConnection, this, conn) + queueTick(() => this.emit('connection', conn)) this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) return this._nextConnection } @@ -281,22 +282,6 @@ function toHandle (obj) { return obj._handle } -function callOnClose (self, onclose) { - if (onclose) onclose.call(self) -} - -function emitListening (self) { - self.emit('listening') -} - -function emitConnection (self, connection) { - self.emit('connection', connection) -} - -function emitError (self, err) { - self.emit('error', err) -} - function ononeoffclose () { this._utp.close() } diff --git a/lib/connection.js b/lib/connection.js index 195781e..5d94df5 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -4,6 +4,7 @@ const unordered = require('unordered-set') const dns = require('dns') const timeout = require('timeout-refresh') const b4a = require('b4a') +const queueTick = require('queue-tick') const EMPTY = b4a.alloc(0) const UTP_ERRORS = [ @@ -195,7 +196,7 @@ module.exports = class Connection extends Duplex { if (this.destroyed) return super.destroy(err) if (err) this._error = err - if (this._needsConnect) return process.nextTick(onbindingclose, this) + if (this._needsConnect) return queueTick(() => binding.utp_napi_connection_on_close(this._handle)) binding.utp_napi_connection_close(this._handle) } @@ -210,10 +211,6 @@ module.exports = class Connection extends Duplex { } } -function onbindingclose (self) { - binding.utp_napi_connection_on_close(self._handle) -} - function createUTPError (code) { const str = UTP_ERRORS[code < 0 ? 3 : code] const err = new Error(str) diff --git a/package.json b/package.json index c10401e..1dbfde5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "b4a": "^1.1.1", "napi-macros": "^2.0.0", "node-gyp-build": "^4.2.0", + "queue-tick": "^1.0.0", "streamx": "^2.11.3", "timeout-refresh": "^1.0.0", "unordered-set": "^2.0.1" From bb97d37ca6fb2725485b8b33d96e32c6cc10b76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 10:14:25 +0100 Subject: [PATCH 07/81] Minimal segfault reproduction --- test/net.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/net.js b/test/net.js index 9a5c445..4eae2d9 100644 --- a/test/net.js +++ b/test/net.js @@ -333,15 +333,14 @@ test('emits close', function (t) { } }) -test.skip('flushes', function (t) { +test('flushes', function (t) { t.plan(1) var sent = '' const server = utp.createServer(function (socket) { var buf = '' - socket.setEncoding('utf-8') socket.on('data', function (data) { - buf += data + buf += data.toString() }) socket.on('end', function () { server.close() @@ -360,15 +359,14 @@ test.skip('flushes', function (t) { }) }) -test.skip('close waits for connections to close', function (t) { +test('close waits for connections to close', function (t) { t.plan(1) var sent = '' const server = utp.createServer(function (socket) { var buf = '' - socket.setEncoding('utf-8') socket.on('data', function (data) { - buf += data + buf += data.toString() }) socket.on('end', function () { socket.end() @@ -439,3 +437,15 @@ test('timeout', async function (t) { await close server.close() }) + +test('abrupt disconnect', async function (t) { + const server = utp.createServer(function () { + throw new Error() + }) + + server.listen(0, function () { + utp.connect(server.address().port).end() + }) + + t.pass() +}) From 0d2671e9402706556503d8d12e0b2bfaa252dcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 11:49:46 +0100 Subject: [PATCH 08/81] Handle connection callback exceptions --- binding.cc | 15 ++++++++++----- index.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/binding.cc b/binding.cc index 46db4f6..cf5c57d 100644 --- a/binding.cc +++ b/binding.cc @@ -315,11 +315,16 @@ on_utp_accept (utp_callback_arguments *a) { napi_create_uint32(env, port, &(argv[0])); napi_create_string_utf8(env, ip, NAPI_AUTO_LENGTH, &(argv[1])); napi_value next; - NAPI_MAKE_CALLBACK(env, NULL, ctx, callback, 2, argv, &next) // will never throw due to the event being NTed in js - utp_napi_connection_t *connection; - size_t connection_size; - napi_get_buffer_info(env, next, (void **) &connection, &connection_size); - self->next_connection = connection; + if (napi_make_callback(env, NULL, ctx, callback, 2, argv, &next) == napi_pending_exception) { + napi_value fatal_exception; + napi_get_and_clear_last_exception(env, &fatal_exception); + napi_fatal_exception(env, fatal_exception); + } else { + utp_napi_connection_t *connection; + size_t connection_size; + napi_get_buffer_info(env, next, (void **) &connection, &connection_size); + self->next_connection = connection; + } }) return 0; diff --git a/index.js b/index.js index 49ceeb8..698e7f4 100644 --- a/index.js +++ b/index.js @@ -236,7 +236,7 @@ const Socket = module.exports = class Socket extends EventEmitter { _onconnection (port, addr) { const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) - queueTick(() => this.emit('connection', conn)) + this.emit('connection', conn) this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) return this._nextConnection } From 7d44659a27897713feb15f352fc55f038f66b4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 11:50:24 +0100 Subject: [PATCH 09/81] Improve test case --- test/net.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/net.js b/test/net.js index 4eae2d9..5682c15 100644 --- a/test/net.js +++ b/test/net.js @@ -439,13 +439,19 @@ test('timeout', async function (t) { }) test('abrupt disconnect', async function (t) { - const server = utp.createServer(function () { - throw new Error() + t.plan(1) + + const server = utp.createServer(function (socket) { + socket.destroy() + throw new Error('disconnect') }) - server.listen(0, function () { - utp.connect(server.address().port).end() + process.once('uncaughtException', () => { + server.close() + t.pass() }) - t.pass() + server.listen(0, function () { + utp.connect(server.address().port).destroy() + }) }) From a42ad539ef419ed94dee1c581c560d73c5eb1e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 11:55:32 +0100 Subject: [PATCH 10/81] Adjust test title --- test/net.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/net.js b/test/net.js index 5682c15..55c5d45 100644 --- a/test/net.js +++ b/test/net.js @@ -438,7 +438,7 @@ test('timeout', async function (t) { server.close() }) -test('abrupt disconnect', async function (t) { +test('exception in connection listener', async function (t) { t.plan(1) const server = utp.createServer(function (socket) { From bad38880a44e65eeaf69daea91df92f48d3ba434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 12:00:01 +0100 Subject: [PATCH 11/81] Skip test case for now --- test/net.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/net.js b/test/net.js index 55c5d45..58eeeaa 100644 --- a/test/net.js +++ b/test/net.js @@ -438,7 +438,7 @@ test('timeout', async function (t) { server.close() }) -test('exception in connection listener', async function (t) { +test.skip('exception in connection listener', async function (t) { t.plan(1) const server = utp.createServer(function (socket) { From 0e3418a4763c1ef3d3e9790949db338de4f9ab64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 13:07:57 +0100 Subject: [PATCH 12/81] Feedback from @mafintosh --- test/net.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/net.js b/test/net.js index 58eeeaa..a343c0e 100644 --- a/test/net.js +++ b/test/net.js @@ -336,24 +336,25 @@ test('emits close', function (t) { test('flushes', function (t) { t.plan(1) - var sent = '' + const sent = [] const server = utp.createServer(function (socket) { - var buf = '' + const recv = [] socket.on('data', function (data) { - buf += data.toString() + recv.push(data) }) socket.on('end', function () { server.close() socket.end() - t.alike(buf, sent) + t.alike(Buffer.concat(recv), Buffer.concat(sent)) }) }) server.listen(0, function () { const socket = utp.connect(server.address().port) for (var i = 0; i < 50; i++) { - socket.write(i + '\n') - sent += i + '\n' + const data = Buffer.from([0x30 + i]) + socket.write(data) + sent.push(data) } socket.end() }) @@ -362,15 +363,15 @@ test('flushes', function (t) { test('close waits for connections to close', function (t) { t.plan(1) - var sent = '' + const sent = [] const server = utp.createServer(function (socket) { - var buf = '' + const recv = [] socket.on('data', function (data) { - buf += data.toString() + recv.push(data) }) socket.on('end', function () { socket.end() - t.alike(buf, sent) + t.alike(Buffer.concat(recv), Buffer.concat(sent)) }) server.close() }) @@ -378,8 +379,9 @@ test('close waits for connections to close', function (t) { server.listen(0, function () { const socket = utp.connect(server.address().port) for (var i = 0; i < 50; i++) { - socket.write(i + '\n') - sent += i + '\n' + const data = Buffer.from([0x30 + i]) + socket.write(data) + sent.push(data) } socket.end() }) From 5efdcd7fec1c5d195df0145e9695ba0c761a4124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 10 Nov 2021 15:25:27 +0100 Subject: [PATCH 13/81] Write all data on connect --- lib/connection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 5d94df5..6a63277 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -184,10 +184,10 @@ module.exports = class Connection extends Duplex { this._connected = true if (this._writing) { const cb = this._callback - const data = this._writing[0] + const data = this._writing this._callback = null this._writing = null - this._write(data, cb) + this._writev(data, cb) } this.emit('connect') } From e736ed063ae81dec2ca76827396152e08e8f78fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 11 Nov 2021 10:58:14 +0100 Subject: [PATCH 14/81] Don't bind callbacks --- lib/connection.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 6a63277..ff58b0e 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -40,13 +40,13 @@ module.exports = class Connection extends Duplex { this.on('finish', this._shutdown) binding.utp_napi_connection_init(this._handle, this, this._buffer, - this._onread.bind(this), - this._ondrain.bind(this), - this._onend.bind(this), - this._onerror.bind(this), - this._onclose.bind(this), - this._onconnect.bind(this), - this._realloc.bind(this) + this._onread, + this._ondrain, + this._onend, + this._onerror, + this._onclose, + this._onconnect, + this._realloc ) unordered.add(utp.connections, this) From 433c5c0eb002fd72a9b17dba05dfa91316efac42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 11 Nov 2021 11:26:28 +0100 Subject: [PATCH 15/81] Clean up tests --- test/net.js | 281 ++++++++++++++++++++++++++-------------------------- 1 file changed, 139 insertions(+), 142 deletions(-) diff --git a/test/net.js b/test/net.js index a343c0e..d769a4b 100644 --- a/test/net.js +++ b/test/net.js @@ -1,70 +1,66 @@ const test = require('brittle') const utp = require('../') -test('server + connect', function (t) { - t.plan(1) - - var connected = false +test('server + connect', async function (t) { + const connect = t.test('connect') + connect.plan(1) const server = utp.createServer(function (socket) { - connected = true socket.write('hello mike') socket.end() }) server.listen(function () { - var socket = utp.connect(server.address().port) - - socket.on('connect', function () { - socket.destroy() - server.close() - t.ok(connected, 'connected successfully') - }) - - socket.write('hello joe') + const socket = utp.connect(server.address().port) + socket + .on('connect', function () { + socket.destroy() + connect.pass('client connected') + }) + .write('hello joe') }) -}) -test('server + connect with resolve', function (t) { - t.plan(1) + await connect + server.close() +}) - var connected = false +test('server + connect with resolve', async function (t) { + const connect = t.test('connect') + connect.plan(1) const server = utp.createServer(function (socket) { - connected = true socket.write('hello mike') socket.end() }) server.listen(function () { const socket = utp.connect(server.address().port, 'localhost') - - socket.on('connect', function () { - socket.destroy() - server.close() - t.ok(connected, 'connected successfully') - }) - - socket.write('hello joe') + socket + .on('connect', function () { + socket.destroy() + connect.pass('client connected') + }) + .write('hello joe') }) + + await connect + server.close() }) test('bad resolve', function (t) { t.plan(4) const socket = utp.connect(10000, 'domain.does-not-exist') - - socket.on('connect', function () { - t.fail('should not connect') - }) - - socket.on('error', function () { - t.pass('errored') - }) - - socket.on('close', function () { - t.pass('closed') - }) + socket + .on('connect', function () { + t.fail('should not connect') + }) + .on('error', function () { + t.pass('errored') + }) + .on('close', function () { + t.pass('closed') + }) }) test('server immediate close', function (t) { @@ -79,16 +75,15 @@ test('server immediate close', function (t) { }) server.listen(0, function () { - var socket = utp.connect(server.address().port) - - socket.write('hi') - socket.once('connect', function () { - socket.end() - }) - - socket.on('close', function () { - t.pass('client closed') - }) + const socket = utp.connect(server.address().port) + socket + .on('connect', function () { + socket.end() + }) + .on('close', function () { + t.pass('client closed') + }) + .write('hi') }) }) @@ -98,13 +93,12 @@ test.skip('only server sends', function (t) { // in libutp it self // in practice this is less of a problem as most protocols // exchange a handshake message. would be great to get fixed though - var server = utp.createServer(function (socket) { + const server = utp.createServer(function (socket) { socket.write('hi') }) server.listen(0, function () { - var socket = utp.connect(server.address().port) - + const socket = utp.connect(server.address().port) socket.on('data', function (data) { t.alike(data, Buffer.from('hi')) socket.destroy() @@ -135,30 +129,31 @@ test('echo server', function (t) { const server = utp.createServer(function (socket) { socket.pipe(socket) - socket.on('data', function (data) { - t.alike(data, Buffer.from('hello')) - }) - socket.on('end', function () { - socket.end() - }) + socket + .on('data', function (data) { + t.alike(data, Buffer.from('hello')) + }) + .on('end', function () { + socket.end() + }) }) server.listen(0, function () { const socket = utp.connect(server.address().port) - - socket.write('hello') - socket.on('data', function (data) { - socket.end() - server.close() - t.alike(data, Buffer.from('hello')) - }) + socket + .on('data', function (data) { + socket.end() + server.close() + t.alike(data, Buffer.from('hello')) + }) + .write('hello') }) }) test('echo server back and fourth', function (t) { t.plan(12) - var echoed = 0 + let echoed = 0 const server = utp.createServer(function (socket) { socket.pipe(socket) @@ -171,23 +166,24 @@ test('echo server back and fourth', function (t) { server.listen(0, function () { const socket = utp.connect(server.address().port) - var rounds = 10 + let rounds = 10 - socket.write('hello') - socket.on('data', function (data) { - if (--rounds) return socket.write(data) - socket.end() - server.close() - t.is(echoed, 10) - t.alike(Buffer.from('hello'), data) - }) + socket + .on('data', function (data) { + if (--rounds) return socket.write(data) + socket.end() + server.close() + t.is(echoed, 10) + t.alike(Buffer.from('hello'), data) + }) + .write('hello') }) }) test('echo big message', function (t) { t.plan(2) - var packets = 0 + let packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') @@ -202,7 +198,7 @@ test('echo big message', function (t) { const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) - var ptr = 0 + let ptr = 0 socket.write(big) socket.on('data', function (data) { @@ -222,7 +218,7 @@ test('echo big message', function (t) { test('echo big message with setContentSize', function (t) { t.plan(2) - var packets = 0 + let packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') @@ -238,7 +234,7 @@ test('echo big message with setContentSize', function (t) { const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) - var ptr = 0 + let ptr = 0 socket.setContentSize(big.length) socket.write(big) @@ -259,9 +255,9 @@ test('echo big message with setContentSize', function (t) { test('two connections', function (t) { t.plan(5) - var count = 0 - var gotA = false - var gotB = false + let count = 0 + let gotA = false + let gotB = false const server = utp.createServer(function (socket) { count++ @@ -298,39 +294,34 @@ test('two connections', function (t) { }) }) -test('emits close', function (t) { - t.plan(6) - - var serverClosed = false - var clientClosed = false +test('emits close', async function (t) { + const close = t.test('close') + close.plan(4) const server = utp.createServer(function (socket) { - socket.resume() - socket.on('end', function () { - socket.end() - }) - socket.on('close', function () { - serverClosed = true - if (clientClosed) done() - }) + socket + .on('end', function () { + socket.end() + }) + .on('close', function () { + close.pass('server closed') + }) + .resume() }) server.listen(0, function () { const socket = utp.connect(server.address().port) socket.write('hi') socket.end() // utp does not support half open - socket.resume() - socket.on('close', function () { - clientClosed = true - if (serverClosed) done() - }) + socket + .on('close', function () { + close.pass('client closed') + }) + .resume() }) - function done () { - server.close() - t.ok(serverClosed) - t.ok(clientClosed) - } + await close + server.close() }) test('flushes', function (t) { @@ -339,19 +330,20 @@ test('flushes', function (t) { const sent = [] const server = utp.createServer(function (socket) { const recv = [] - socket.on('data', function (data) { - recv.push(data) - }) - socket.on('end', function () { - server.close() - socket.end() - t.alike(Buffer.concat(recv), Buffer.concat(sent)) - }) + socket + .on('data', function (data) { + recv.push(data) + }) + .on('end', function () { + server.close() + socket.end() + t.alike(Buffer.concat(recv), Buffer.concat(sent)) + }) }) server.listen(0, function () { const socket = utp.connect(server.address().port) - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) socket.write(data) sent.push(data) @@ -366,19 +358,20 @@ test('close waits for connections to close', function (t) { const sent = [] const server = utp.createServer(function (socket) { const recv = [] - socket.on('data', function (data) { - recv.push(data) - }) - socket.on('end', function () { - socket.end() - t.alike(Buffer.concat(recv), Buffer.concat(sent)) - }) + socket + .on('data', function (data) { + recv.push(data) + }) + .on('end', function () { + socket.end() + t.alike(Buffer.concat(recv), Buffer.concat(sent)) + }) server.close() }) server.listen(0, function () { const socket = utp.connect(server.address().port) - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) socket.write(data) sent.push(data) @@ -389,15 +382,17 @@ test('close waits for connections to close', function (t) { test('disable half open', function (t) { t.plan(3) + const server = utp.createServer({ allowHalfOpen: false }, function (socket) { - socket.on('data', function (data) { - t.alike(data, Buffer.from('a')) - }) - socket.on('close', function () { - server.close(function () { - t.pass('everything closed') + socket + .on('data', function (data) { + t.alike(data, Buffer.from('a')) + }) + .on('close', function () { + server.close(function () { + t.pass('everything closed') + }) }) - }) }) server.listen(0, function () { @@ -417,23 +412,25 @@ test('timeout', async function (t) { t.pass('timed out') socket.destroy() }) - socket.resume() - socket.write('hi') - socket.on('close', function () { - close.pass('server closed') - }) + socket + .on('close', function () { + close.pass('server closed') + }) + .resume() + .write('hi') }) server.listen(0, function () { const socket = utp.connect(server.address().port) - socket.write('hi') - socket.resume() - socket.on('end', function () { - socket.destroy() - }) - socket.on('close', function () { - close.pass('client closed') - }) + socket + .on('end', function () { + socket.destroy() + }) + .on('close', function () { + close.pass('client closed') + }) + .resume() + .write('hi') }) await close From 8f8fe7d5a16286e49a5f03e72c40269b060a8cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 11 Nov 2021 12:02:42 +0100 Subject: [PATCH 16/81] Use `_realloc()` --- lib/connection.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index ff58b0e..136955d 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -125,11 +125,7 @@ module.exports = class Connection extends Duplex { // 64kb + 4kb as max package buffer is 64kb and we wanna make sure we have room for that // plus the next udp package - if (this._buffer.length - this._offset <= 69632) { - this._buffer = b4a.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer - } + if (this._buffer.length - this._offset <= 69632) return this._realloc() return EMPTY } From ce78792fde314d2bf689764c3fbc06092a1fe4f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 11 Nov 2021 12:27:40 +0100 Subject: [PATCH 17/81] Use `_final()` --- lib/connection.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 136955d..7cb38b6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -37,8 +37,6 @@ module.exports = class Connection extends Duplex { this._contentSize = 0 this._allowOpen = halfOpen ? 2 : 1 - this.on('finish', this._shutdown) - binding.utp_napi_connection_init(this._handle, this, this._buffer, this._onread, this._ondrain, @@ -200,10 +198,10 @@ module.exports = class Connection extends Duplex { if (this._allowOpen && !--this._allowOpen) this.destroy() } - _shutdown () { - if (this.destroyed) return + _final (cb) { binding.utp_napi_connection_shutdown(this._handle) this._destroyMaybe() + cb(null) } } From 64322b1a346ce3802c955e675a7df188dc05275a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 11 Nov 2021 21:01:50 +0100 Subject: [PATCH 18/81] Progress on refactor --- lib/connection.js | 91 ++++++------ test/net.js | 370 ++++++++++++++++++++++------------------------ 2 files changed, 219 insertions(+), 242 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 7cb38b6..d49aa6a 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -4,7 +4,6 @@ const unordered = require('unordered-set') const dns = require('dns') const timeout = require('timeout-refresh') const b4a = require('b4a') -const queueTick = require('queue-tick') const EMPTY = b4a.alloc(0) const UTP_ERRORS = [ @@ -28,11 +27,9 @@ module.exports = class Connection extends Duplex { this._buffer = b4a.allocUnsafe(65536 * 2) this._offset = 0 this._view = new Uint32Array(this._handle.buffer, this._handle.byteOffset, 2) - this._callback = null this._writing = null - this._error = null - this._connected = false - this._needsConnect = !handle + this._connected = !!handle + this._destroying = null this._timeout = null this._contentSize = 0 this._allowOpen = halfOpen ? 2 : 1 @@ -47,9 +44,10 @@ module.exports = class Connection extends Duplex { this._realloc ) - unordered.add(utp.connections, this) - if (utp.maxConnections && utp.connections.length >= utp.maxConnections) { - utp.firewall(true) + unordered.add(this._utp.connections, this) + + if (this._utp.maxConnections && this._utp.connections.length >= this._utp.maxConnections) { + this._utp.firewall(true) } } @@ -59,10 +57,6 @@ module.exports = class Connection extends Duplex { this._timeout = timeout(ms, this._ontimeout, this) } - _ontimeout () { - this.emit('timeout') - } - setInteractive (interactive) { this.setPacketSize(interactive ? 0 : 65536) } @@ -92,13 +86,10 @@ module.exports = class Connection extends Duplex { if (bufs.length > 256) bufs = [b4a.concat(bufs)] - if (!this._connected || !binding.utp_napi_connection_writev(this._handle, bufs)) { - this._callback = cb - this._writing = bufs - return - } + const drained = binding.utp_napi_connection_writev(this._handle, bufs) - cb(null) + if (drained) cb(null) + else this._writing = [cb, bufs] } _realloc () { @@ -107,8 +98,11 @@ module.exports = class Connection extends Duplex { return this._buffer } + _ontimeout () { + this.emit('timeout') + } + _onread (size) { - if (!this._connected) this._onconnect() // makes the server wait for reads before writes if (this._timeout) this._timeout.refresh() const buf = this._buffer.subarray(this._offset, this._offset += size) @@ -129,20 +123,16 @@ module.exports = class Connection extends Duplex { } _ondrain () { + const cb = this._writing[0] this._writing = null - const cb = this._callback - this._callback = null cb(null) } _onclose () { - unordered.remove(this._utp.connections, this) - if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { - this._utp.firewall(false) - } - this._handle = null - if (this._error) this.emit('error', this._error) - this.emit('close') + const cb = this._destroying + this._destroying = null + cb(null) + this._utp._closeMaybe() } @@ -157,18 +147,17 @@ module.exports = class Connection extends Duplex { } _resolveAndConnect (port, host) { - const self = this - dns.lookup(host, { family: 4 }, function (err, ip) { - if (err) return self.destroy(err) - if (!ip) return self.destroy(new Error('Could not resolve ' + host)) - self._connect(port, ip) + dns.lookup(host, { family: 4 }, (err, ip) => { + if (err) return this.destroy(err) + if (!ip) return this.destroy(new Error('Could not resolve ' + host)) + this._connect(port, ip) }) } _connect (port, ip) { - if (this.destroyed) return - this._needsConnect = false + this.remotePort = port this.remoteAddress = ip + binding.utp_napi_connect(this._utp._handle, this._handle, port, ip) } @@ -176,22 +165,28 @@ module.exports = class Connection extends Duplex { if (this._timeout) this._timeout.refresh() this._connected = true - if (this._writing) { - const cb = this._callback - const data = this._writing - this._callback = null - this._writing = null - this._writev(data, cb) - } this.emit('connect') } - destroy (err) { - if (this.destroyed) return - super.destroy(err) - if (err) this._error = err - if (this._needsConnect) return queueTick(() => binding.utp_napi_connection_on_close(this._handle)) - binding.utp_napi_connection_close(this._handle) + _open (cb) { + if (this._connected) cb(null) + else this.once('connect', cb) + } + + _destroy (cb) { + unordered.remove(this._utp.connections, this) + + if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { + this._utp.firewall(false) + } + + this._destroying = cb + + if (this._connected) { + binding.utp_napi_connection_close(this._handle) + } else { + binding.utp_napi_connection_on_close(this._handle) + } } _destroyMaybe () { diff --git a/test/net.js b/test/net.js index d769a4b..58ec343 100644 --- a/test/net.js +++ b/test/net.js @@ -1,186 +1,161 @@ const test = require('brittle') const utp = require('../') -test('server + connect', async function (t) { - const connect = t.test('connect') - connect.plan(1) +test('server + connect', (t) => withServer(t, async (server) => { + const close = t.test('connect and close sockets') + close.plan(2) - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { socket.write('hello mike') - socket.end() + socket + .on('close', () => close.pass('server socket closed')) + .destroy() // .end() hangs? }) - server.listen(function () { + server.listen(() => { const socket = utp.connect(server.address().port) socket - .on('connect', function () { - socket.destroy() - connect.pass('client connected') - }) + .on('connect', () => + socket.destroy() // .end() hangs? + ) + .on('close', () => close.pass('client socket closed')) .write('hello joe') }) - await connect - server.close() -}) + await close +})) -test('server + connect with resolve', async function (t) { - const connect = t.test('connect') - connect.plan(1) +test('server + connect with resolve', (t) => withServer(t, async (server) => { + const close = t.test('connect and close sockets') + close.plan(2) - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { socket.write('hello mike') - socket.end() + socket + .on('close', () => close.pass('server socket closed')) + .destroy() // .end() hangs? }) - server.listen(function () { + server.listen(() => { const socket = utp.connect(server.address().port, 'localhost') socket - .on('connect', function () { - socket.destroy() - connect.pass('client connected') - }) + .on('connect', () => + socket.destroy() // .end() hangs? + ) + .on('close', () => close.pass('client socket closed')) .write('hello joe') }) - await connect - server.close() -}) + await close +})) -test('bad resolve', function (t) { - t.plan(4) +test('bad resolve', (t) => { + t.plan(2) const socket = utp.connect(10000, 'domain.does-not-exist') socket - .on('connect', function () { - t.fail('should not connect') - }) - .on('error', function () { - t.pass('errored') - }) - .on('close', function () { - t.pass('closed') - }) + .on('connect', () => t.fail('should not connect')) + .on('error', () => t.pass('errored')) + .on('close', () => t.pass('closed')) }) -test('server immediate close', function (t) { - t.plan(3) +test.skip('server immediate close', (t) => withServer(t, async (server) => { + const close = t.test('connect and close server') + close.plan(2) - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { socket.write('hi') - socket.end() - server.close(function () { - t.pass('closed') - }) + socket.destroy() // .end() does not remove connection? + server.close(() => close.pass('server closed')) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) socket - .on('connect', function () { - socket.end() - }) - .on('close', function () { - t.pass('client closed') - }) + .on('connect', () => + socket.destroy() // .end() hangs? + ) + .on('close', () => close.pass('client closed')) .write('hi') }) -}) - -test.skip('only server sends', function (t) { - // this is skipped because it doesn't work. - // utpcat has the same issue so this seems to be a bug - // in libutp it self - // in practice this is less of a problem as most protocols - // exchange a handshake message. would be great to get fixed though - const server = utp.createServer(function (socket) { - socket.write('hi') - }) - server.listen(0, function () { - const socket = utp.connect(server.address().port) - socket.on('data', function (data) { - t.alike(data, Buffer.from('hi')) - socket.destroy() - server.close() - }) - }) -}) + await close +})) -test('server listens on a port in use', function (t) { - t.plan(1) +test('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { + const error = t.test('error on listen') + error.plan(1) - const server = utp.createServer() - server.listen(0, function () { - const server2 = utp.createServer() - server2.listen(server.address().port, function () { - t.fail('should not be listening') - }) - server2.on('error', function () { - server.close() - server2.close() - t.pass('had error') - }) + a.listen(0, () => { + b + .on('error', () => error.pass('had error')) + .listen(a.address().port, () => { + error.fail('should not be listening') + }) }) -}) -test('echo server', function (t) { - t.plan(2) + await error +}))) - const server = utp.createServer(function (socket) { +test('echo server', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(4) + + server.on('connection', (socket) => { socket.pipe(socket) socket - .on('data', function (data) { - t.alike(data, Buffer.from('hello')) - }) - .on('end', function () { - socket.end() - }) + .on('data', (data) => writes.alike(data, Buffer.from('hello'))) + .on('end', () => socket.destroy()) + .on('close', () => writes.pass('server socket closed')) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) socket - .on('data', function (data) { - socket.end() - server.close() - t.alike(data, Buffer.from('hello')) - }) - .write('hello') + .on('data', (data) => writes.alike(data, Buffer.from('hello'))) + .on('close', () => writes.pass('client socket closed')) + .end('hello') }) -}) -test('echo server back and fourth', function (t) { - t.plan(12) + await writes +})) + +test('echo server back and fourth', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(14) let echoed = 0 - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { socket.pipe(socket) - socket.on('data', function (data) { - echoed++ - t.alike(data, Buffer.from('hello')) - }) + socket + .on('data', (data) => { + echoed++ + writes.alike(data, Buffer.from('hello')) + }) + .on('close', () => writes.pass('server socket closed')) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) let rounds = 10 socket - .on('data', function (data) { + .on('data', (data) => { if (--rounds) return socket.write(data) socket.end() - server.close() - t.is(echoed, 10) - t.alike(Buffer.from('hello'), data) + writes.is(echoed, 10) + writes.alike(Buffer.from('hello'), data) }) + .on('close', () => writes.pass('client socket closed')) .write('hello') }) -}) -test('echo big message', function (t) { + await writes +})) + +test('echo big message', (t) => { t.plan(2) let packets = 0 @@ -188,12 +163,12 @@ test('echo big message', function (t) { const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') - const server = utp.createServer(function (socket) { + const server = utp.createServer((socket) => { socket.on('data', () => packets++) socket.pipe(socket) }) - server.listen(0, function () { + server.listen(0, () => { const then = Date.now() const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) @@ -201,7 +176,7 @@ test('echo big message', function (t) { let ptr = 0 socket.write(big) - socket.on('data', function (data) { + socket.on('data', (data) => { packets++ data.copy(buffer, ptr) ptr += data.length @@ -215,7 +190,7 @@ test('echo big message', function (t) { }) }) -test('echo big message with setContentSize', function (t) { +test('echo big message with setContentSize', (t) => { t.plan(2) let packets = 0 @@ -223,13 +198,13 @@ test('echo big message with setContentSize', function (t) { const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') - const server = utp.createServer(function (socket) { + const server = utp.createServer((socket) => { socket.setContentSize(big.length) socket.on('data', () => packets++) socket.pipe(socket) }) - server.listen(0, function () { + server.listen(0, () => { const then = Date.now() const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) @@ -238,7 +213,7 @@ test('echo big message with setContentSize', function (t) { socket.setContentSize(big.length) socket.write(big) - socket.on('data', function (data) { + socket.on('data', (data) => { packets++ data.copy(buffer, ptr) ptr += data.length @@ -252,96 +227,82 @@ test('echo big message with setContentSize', function (t) { }) }) -test('two connections', function (t) { - t.plan(5) - - let count = 0 - let gotA = false - let gotB = false +test.skip('two connections', async (t) => { + const writes = t.test('writes') + writes.plan(5) - const server = utp.createServer(function (socket) { - count++ + const server = utp.createServer((socket) => { socket.pipe(socket) }) - server.listen(0, function () { - const socket1 = utp.connect(server.address().port) - const socket2 = utp.connect(server.address().port) + server.listen(0, () => { + const a = utp.connect(server.address().port) + const b = utp.connect(server.address().port) - socket1.write('a') - socket2.write('b') + a.write('a') + b.write('b') - socket1.on('data', function (data) { - gotA = true - t.alike(data, Buffer.from('a')) - if (gotB) done() + a.on('data', (data) => { + writes.alike(data, Buffer.from('a')) + a.end() }) - socket2.on('data', function (data) { - gotB = true - t.alike(data, Buffer.from('b')) - if (gotA) done() + b.on('data', (data) => { + writes.alike(data, Buffer.from('b')) + b.end() }) - - function done () { - socket1.end() - socket2.end() - server.close() - t.ok(gotA) - t.ok(gotB) - t.alike(count, 2) - } }) + + await writes.then(() => server.close()) }) -test('emits close', async function (t) { +test('emits close', (t) => withServer(t, async (server) => { const close = t.test('close') - close.plan(4) + close.plan(2) - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { socket - .on('end', function () { + .on('end', () => { socket.end() }) - .on('close', function () { + .on('close', () => { close.pass('server closed') }) .resume() }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) socket.write('hi') socket.end() // utp does not support half open socket - .on('close', function () { + .on('close', () => { close.pass('client closed') }) .resume() }) await close - server.close() -}) +})) -test('flushes', function (t) { +test('flushes', (t) => { t.plan(1) const sent = [] - const server = utp.createServer(function (socket) { + const server = utp.createServer((socket) => { const recv = [] socket - .on('data', function (data) { + .on('data', (data) => { recv.push(data) }) - .on('end', function () { + .on('end', () => { server.close() socket.end() t.alike(Buffer.concat(recv), Buffer.concat(sent)) }) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) @@ -352,24 +313,25 @@ test('flushes', function (t) { }) }) -test('close waits for connections to close', function (t) { - t.plan(1) +test.skip('close waits for connections to close', (t) => withServer(t, async (server) => { + const close = t.test('close') + close.plan(2) const sent = [] - const server = utp.createServer(function (socket) { + server.on('connection', (socket) => { const recv = [] socket - .on('data', function (data) { + .on('data', (data) => { recv.push(data) }) - .on('end', function () { + .on('end', () => { socket.end() t.alike(Buffer.concat(recv), Buffer.concat(sent)) }) - server.close() + server.close(() => close.pass('server closed')) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) @@ -378,24 +340,26 @@ test('close waits for connections to close', function (t) { } socket.end() }) -}) -test('disable half open', function (t) { - t.plan(3) + await close +})) + +test('disable half open', (t) => { + t.plan(2) - const server = utp.createServer({ allowHalfOpen: false }, function (socket) { + const server = utp.createServer({ allowHalfOpen: false }, (socket) => { socket - .on('data', function (data) { + .on('data', (data) => { t.alike(data, Buffer.from('a')) }) - .on('close', function () { - server.close(function () { + .on('close', () => { + server.close(() => { t.pass('everything closed') }) }) }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port, '127.0.0.1', { allowHalfOpen: true }) socket.write('a') @@ -403,30 +367,30 @@ test('disable half open', function (t) { }) }) -test('timeout', async function (t) { +test.skip('timeout', (t) => withServer(t, async (server) => { const close = t.test('close') close.plan(4) - const server = utp.createServer(function (socket) { - socket.setTimeout(100, function () { + server.on('connection', (socket) => { + socket.setTimeout(100, () => { t.pass('timed out') socket.destroy() }) socket - .on('close', function () { + .on('close', () => { close.pass('server closed') }) .resume() .write('hi') }) - server.listen(0, function () { + server.listen(0, () => { const socket = utp.connect(server.address().port) socket - .on('end', function () { + .on('end', () => { socket.destroy() }) - .on('close', function () { + .on('close', () => { close.pass('client closed') }) .resume() @@ -434,13 +398,12 @@ test('timeout', async function (t) { }) await close - server.close() -}) +})) -test.skip('exception in connection listener', async function (t) { +test.skip('exception in connection listener', async (t) => { t.plan(1) - const server = utp.createServer(function (socket) { + const server = utp.createServer((socket) => { socket.destroy() throw new Error('disconnect') }) @@ -450,7 +413,26 @@ test.skip('exception in connection listener', async function (t) { t.pass() }) - server.listen(0, function () { + server.listen(0, () => { utp.connect(server.address().port).destroy() }) }) + +async function withServer (t, cb) { + const server = utp.createServer() + + try { + await cb(server) + } finally { + const close = t.test('close server') + close.plan(2) + + close.is(server.connections.length, 0, 'connections closed') + + for (const connection of server.connections) connection.destroy() + + server.close(() => close.pass('server closed')) + + await close + } +} From 1f669fcb0fd842da2e51ae522449de1f04f166af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 12 Nov 2021 16:19:11 +0100 Subject: [PATCH 19/81] Add `UTPConnection` class --- lib/connection.js | 270 ++++++++++++++++++++++++++++++++-------------- test/net.js | 187 +++++++++++--------------------- 2 files changed, 249 insertions(+), 208 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index d49aa6a..0b68a22 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,17 +1,12 @@ -const binding = require('./binding') +const EventEmitter = require('events') const { Duplex } = require('streamx') const unordered = require('unordered-set') const dns = require('dns') const timeout = require('timeout-refresh') const b4a = require('b4a') +const binding = require('./binding') const EMPTY = b4a.alloc(0) -const UTP_ERRORS = [ - 'UTP_ECONNREFUSED', - 'UTP_ECONNRESET', - 'UTP_ETIMEDOUT', - 'UTP_UNKNOWN' -] module.exports = class Connection extends Duplex { constructor (utp, port, address, handle, halfOpen) { @@ -23,32 +18,22 @@ module.exports = class Connection extends Duplex { this._index = -1 this._utp = utp - this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) - this._buffer = b4a.allocUnsafe(65536 * 2) - this._offset = 0 - this._view = new Uint32Array(this._handle.buffer, this._handle.byteOffset, 2) - this._writing = null - this._connected = !!handle - this._destroying = null + this._connection = new UTPConnection(utp, handle) + this._view = new Uint32Array(this._connection._handle.buffer, this._connection._handle.byteOffset, 2) this._timeout = null this._contentSize = 0 this._allowOpen = halfOpen ? 2 : 1 - binding.utp_napi_connection_init(this._handle, this, this._buffer, - this._onread, - this._ondrain, - this._onend, - this._onerror, - this._onclose, - this._onconnect, - this._realloc - ) - unordered.add(this._utp.connections, this) if (this._utp.maxConnections && this._utp.connections.length >= this._utp.maxConnections) { this._utp.firewall(true) } + + this._connection + .on('data', (buffer) => this._onread(buffer)) + .on('end', () => this._onend()) + .on('connect', () => this._onconnect()) } setTimeout (ms, ontimeout) { @@ -86,26 +71,17 @@ module.exports = class Connection extends Duplex { if (bufs.length > 256) bufs = [b4a.concat(bufs)] - const drained = binding.utp_napi_connection_writev(this._handle, bufs) - - if (drained) cb(null) - else this._writing = [cb, bufs] - } - - _realloc () { - this._buffer = b4a.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer + this._connection.writev(bufs, cb) } _ontimeout () { this.emit('timeout') } - _onread (size) { + _onread (buffer) { if (this._timeout) this._timeout.refresh() - const buf = this._buffer.subarray(this._offset, this._offset += size) + let size = buffer.byteLength if (this._contentSize) { if (size > this._contentSize) size = this._contentSize @@ -113,31 +89,7 @@ module.exports = class Connection extends Duplex { if (this._contentSize < 65536) this._view[0] = this._contentSize } - this.push(buf) - - // 64kb + 4kb as max package buffer is 64kb and we wanna make sure we have room for that - // plus the next udp package - if (this._buffer.length - this._offset <= 69632) return this._realloc() - - return EMPTY - } - - _ondrain () { - const cb = this._writing[0] - this._writing = null - cb(null) - } - - _onclose () { - const cb = this._destroying - this._destroying = null - cb(null) - - this._utp._closeMaybe() - } - - _onerror (status) { - this.destroy(createUTPError(status)) + this.push(buffer) } _onend () { @@ -146,6 +98,10 @@ module.exports = class Connection extends Duplex { this._destroyMaybe() } + _onconnect () { + this.emit('connect') + } + _resolveAndConnect (port, host) { dns.lookup(host, { family: 4 }, (err, ip) => { if (err) return this.destroy(err) @@ -158,35 +114,33 @@ module.exports = class Connection extends Duplex { this.remotePort = port this.remoteAddress = ip - binding.utp_napi_connect(this._utp._handle, this._handle, port, ip) - } - - _onconnect () { - if (this._timeout) this._timeout.refresh() - - this._connected = true - this.emit('connect') + this._connection.connect(port, ip) } _open (cb) { - if (this._connected) cb(null) - else this.once('connect', cb) + if (this._connection.connected) cb(null) + else { + this._connection.once('connect', () => { + if (this._timeout) this._timeout.refresh() + cb(null) + }) + } } _destroy (cb) { - unordered.remove(this._utp.connections, this) + this._connection.close((err) => { + if (err) return cb(err) - if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { - this._utp.firewall(false) - } + unordered.remove(this._utp.connections, this) - this._destroying = cb + if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { + this._utp.firewall(false) + } - if (this._connected) { - binding.utp_napi_connection_close(this._handle) - } else { - binding.utp_napi_connection_on_close(this._handle) - } + this._utp._closeMaybe() + + cb(null) + }) } _destroyMaybe () { @@ -194,12 +148,164 @@ module.exports = class Connection extends Duplex { } _final (cb) { - binding.utp_napi_connection_shutdown(this._handle) + this._connection.shutdown() this._destroyMaybe() cb(null) } } +const INITIALIZED = 1 +const CONNECTING = 1 << 1 +const CONNECTED = 1 << 2 +const CLOSING = 1 << 3 +const CLOSED = 1 << 4 +const SHUTDOWN = 1 << 5 + +class UTPConnection extends EventEmitter { + constructor (socket, handle) { + super() + + this._socket = socket + this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) + + this._state = 0 + this._buffer = b4a.allocUnsafe(65536 * 2) + this._offset = 0 + this._writing = null + + if (handle) { + this._init() + this._state |= CONNECTED + } + } + + get connected () { + return (this._state & CONNECTED) !== 0 + } + + connect (port, ip, cb) { + if ((this._state & (CONNECTED | CONNECTING)) !== 0) return cb && cb(new Error('Already connected')) + if ((this._state & INITIALIZED) === 0) this._init() + + if (cb) this.once('connect', cb) + + this._state |= CONNECTING + + binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) + } + + write (data, cb) { + if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) + + const drained = binding.utp_napi_connection_write(this._handle, data) === 1 + + if (drained) cb(null) + else this._writing = [cb, data] + + return drained + } + + writev (batch, cb) { + if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) + + const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 + + if (drained) cb(null) + else this._writing = [cb, batch] + + return drained + } + + shutdown () { + if ((this._state & SHUTDOWN) !== 0) return + + this._state |= SHUTDOWN + + binding.utp_napi_connection_shutdown(this._handle) + } + + close (cb) { + if ((this._state & INITIALIZED) === 0) return cb && cb(null) + + if (cb) this.once('close', cb) + + if ((this._state & (CLOSED | CLOSING)) === 0) { + if ((this._state & CONNECTED) === 0) { + binding.utp_napi_connection_on_close(this._handle) + } else { + binding.utp_napi_connection_close(this._handle) + } + + this._state |= CLOSING + } + } + + _init () { + if (this._state & INITIALIZED) return + + this._state |= INITIALIZED + + binding.utp_napi_connection_init(this._handle, this, this._buffer, + this._onread, + this._ondrain, + this._onend, + this._onerror, + this._onclose, + this._onconnect, + this._realloc + ) + } + + _onread (size) { + const buffer = this._buffer.subarray(this._offset, this._offset += size) + + this.emit('data', buffer) + + if (this._buffer.length - this._offset <= 69632) return this._realloc() + + return EMPTY + } + + _ondrain () { + const cb = this._writing[0] + this._writing = null + cb(null) + } + + _onend () { + this.emit('end') + } + + _onerror (code) { + this.emit('error', createUTPError(code)) + } + + _onclose () { + this._state &= ~CLOSING + this._state |= CLOSED + this.emit('close') + } + + _onconnect () { + this._state &= ~CONNECTING + this._state |= CONNECTED + this.emit('connect') + } + + _realloc () { + this._buffer = b4a.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer + } +} + +const UTP_ERRORS = [ + 'UTP_ECONNREFUSED', + 'UTP_ECONNRESET', + 'UTP_ETIMEDOUT', + 'UTP_UNKNOWN' +] + function createUTPError (code) { const str = UTP_ERRORS[code < 0 ? 3 : code] const err = new Error(str) diff --git a/test/net.js b/test/net.js index 58ec343..fdf89b0 100644 --- a/test/net.js +++ b/test/net.js @@ -1,25 +1,25 @@ const test = require('brittle') -const utp = require('../') +const utp = require('..') test('server + connect', (t) => withServer(t, async (server) => { const close = t.test('connect and close sockets') - close.plan(2) + close.plan(4) server.on('connection', (socket) => { - socket.write('hello mike') + close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .destroy() // .end() hangs? + .end() // .destroy() causes ECONNRESET? }) server.listen(() => { const socket = utp.connect(server.address().port) socket - .on('connect', () => - socket.destroy() // .end() hangs? - ) + .on('connect', () => { + socket.end() // .destroy hangs? + close.pass('client socket connected') + }) .on('close', () => close.pass('client socket closed')) - .write('hello joe') }) await close @@ -27,23 +27,23 @@ test('server + connect', (t) => withServer(t, async (server) => { test('server + connect with resolve', (t) => withServer(t, async (server) => { const close = t.test('connect and close sockets') - close.plan(2) + close.plan(4) server.on('connection', (socket) => { - socket.write('hello mike') + close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .destroy() // .end() hangs? + .end() // .destroy() causes ECONNRESET? }) server.listen(() => { const socket = utp.connect(server.address().port, 'localhost') socket - .on('connect', () => - socket.destroy() // .end() hangs? - ) + .on('connect', () => { + socket.end() // .destroy() hangs? + close.pass('client socket connected') + }) .on('close', () => close.pass('client socket closed')) - .write('hello joe') }) await close @@ -59,34 +59,11 @@ test('bad resolve', (t) => { .on('close', () => t.pass('closed')) }) -test.skip('server immediate close', (t) => withServer(t, async (server) => { - const close = t.test('connect and close server') - close.plan(2) - - server.on('connection', (socket) => { - socket.write('hi') - socket.destroy() // .end() does not remove connection? - server.close(() => close.pass('server closed')) - }) - - server.listen(0, () => { - const socket = utp.connect(server.address().port) - socket - .on('connect', () => - socket.destroy() // .end() hangs? - ) - .on('close', () => close.pass('client closed')) - .write('hi') - }) - - await close -})) - test('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { const error = t.test('error on listen') error.plan(1) - a.listen(0, () => { + a.listen(() => { b .on('error', () => error.pass('had error')) .listen(a.address().port, () => { @@ -105,11 +82,10 @@ test('echo server', (t) => withServer(t, async (server) => { socket.pipe(socket) socket .on('data', (data) => writes.alike(data, Buffer.from('hello'))) - .on('end', () => socket.destroy()) .on('close', () => writes.pass('server socket closed')) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port) socket .on('data', (data) => writes.alike(data, Buffer.from('hello'))) @@ -122,7 +98,7 @@ test('echo server', (t) => withServer(t, async (server) => { test('echo server back and fourth', (t) => withServer(t, async (server) => { const writes = t.test('write and close sockets') - writes.plan(14) + writes.plan(22) let echoed = 0 @@ -136,23 +112,24 @@ test('echo server back and fourth', (t) => withServer(t, async (server) => { .on('close', () => writes.pass('server socket closed')) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port) let rounds = 10 socket .on('data', (data) => { - if (--rounds) return socket.write(data) - socket.end() - writes.is(echoed, 10) - writes.alike(Buffer.from('hello'), data) + writes.alike(data, Buffer.from('hello')) + if (--rounds) socket.write(data) + else socket.end() }) .on('close', () => writes.pass('client socket closed')) .write('hello') }) await writes + + t.is(echoed, 10) })) test('echo big message', (t) => { @@ -168,7 +145,7 @@ test('echo big message', (t) => { socket.pipe(socket) }) - server.listen(0, () => { + server.listen(() => { const then = Date.now() const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) @@ -204,7 +181,7 @@ test('echo big message with setContentSize', (t) => { socket.pipe(socket) }) - server.listen(0, () => { + server.listen(() => { const then = Date.now() const socket = utp.connect(server.address().port) const buffer = Buffer.alloc(big.length) @@ -228,81 +205,47 @@ test('echo big message with setContentSize', (t) => { }) test.skip('two connections', async (t) => { - const writes = t.test('writes') - writes.plan(5) + const writes = t.test('write and close sockets') + writes.plan(4) const server = utp.createServer((socket) => { socket.pipe(socket) }) - server.listen(0, () => { + server.listen(() => { const a = utp.connect(server.address().port) const b = utp.connect(server.address().port) - a.write('a') - b.write('b') + a + .on('data', (data) => writes.alike(data, Buffer.from('a'))) + .on('close', () => writes.pass('a closed')) + .end('a') - a.on('data', (data) => { - writes.alike(data, Buffer.from('a')) - a.end() - }) - - b.on('data', (data) => { - writes.alike(data, Buffer.from('b')) - b.end() - }) + b + .on('data', (data) => writes.alike(data, Buffer.from('b'))) + .on('close', () => writes.pass('b closed')) + .end('b') }) - await writes.then(() => server.close()) + await writes }) -test('emits close', (t) => withServer(t, async (server) => { - const close = t.test('close') - close.plan(2) - - server.on('connection', (socket) => { - socket - .on('end', () => { - socket.end() - }) - .on('close', () => { - close.pass('server closed') - }) - .resume() - }) - - server.listen(0, () => { - const socket = utp.connect(server.address().port) - socket.write('hi') - socket.end() // utp does not support half open - socket - .on('close', () => { - close.pass('client closed') - }) - .resume() - }) - - await close -})) - -test('flushes', (t) => { - t.plan(1) +test('flushes', (t) => withServer(t, async (server) => { + const writes = t.test('writes') + writes.plan(1) const sent = [] - const server = utp.createServer((socket) => { + server.on('connection', (socket) => { const recv = [] socket - .on('data', (data) => { - recv.push(data) - }) + .on('data', (data) => recv.push(data)) .on('end', () => { - server.close() socket.end() - t.alike(Buffer.concat(recv), Buffer.concat(sent)) + writes.alike(Buffer.concat(recv), Buffer.concat(sent)) }) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port) for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) @@ -311,7 +254,9 @@ test('flushes', (t) => { } socket.end() }) -}) + + await writes +})) test.skip('close waits for connections to close', (t) => withServer(t, async (server) => { const close = t.test('close') @@ -331,7 +276,7 @@ test.skip('close waits for connections to close', (t) => withServer(t, async (se server.close(() => close.pass('server closed')) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port) for (let i = 0; i < 50; i++) { const data = Buffer.from([0x30 + i]) @@ -359,7 +304,7 @@ test('disable half open', (t) => { }) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port, '127.0.0.1', { allowHalfOpen: true }) socket.write('a') @@ -367,34 +312,24 @@ test('disable half open', (t) => { }) }) -test.skip('timeout', (t) => withServer(t, async (server) => { +test('timeout', (t) => withServer(t, async (server) => { const close = t.test('close') - close.plan(4) + close.plan(2) server.on('connection', (socket) => { - socket.setTimeout(100, () => { - t.pass('timed out') - socket.destroy() - }) socket - .on('close', () => { - close.pass('server closed') - }) - .resume() - .write('hi') + .on('close', () => close.pass('server closed')) + .setTimeout(100, () => + socket.end() // .destroy() causes ECONNRESET + ) }) - server.listen(0, () => { + server.listen(() => { const socket = utp.connect(server.address().port) socket - .on('end', () => { - socket.destroy() - }) - .on('close', () => { - close.pass('client closed') - }) - .resume() - .write('hi') + .on('end', () => socket.end()) // no .end() hangs? + .on('close', () => close.pass('client closed')) + .write('hello') // why required? }) await close @@ -413,7 +348,7 @@ test.skip('exception in connection listener', async (t) => { t.pass() }) - server.listen(0, () => { + server.listen(() => { utp.connect(server.address().port).destroy() }) }) From 4f82c2b7c9bc1941645756a25ee7fea50389c004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 12 Nov 2021 17:11:14 +0100 Subject: [PATCH 20/81] Reorganize --- lib/connection.js | 103 +++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 0b68a22..e8e040c 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -62,6 +62,42 @@ module.exports = class Connection extends Duplex { return this._utp.address() } + _open (cb) { + if (this._connection.connected) cb(null) + else { + this._connection.once('connect', () => { + if (this._timeout) this._timeout.refresh() + cb(null) + }) + } + } + + _destroy (cb) { + this._connection.close((err) => { + if (err) return cb(err) + + unordered.remove(this._utp.connections, this) + + if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { + this._utp.firewall(false) + } + + this._utp._closeMaybe() + + cb(null) + }) + } + + _destroyMaybe () { + if (this._allowOpen && !--this._allowOpen) this.destroy() + } + + _final (cb) { + this._connection.shutdown() + this._destroyMaybe() + cb(null) + } + _writev (datas, cb) { let bufs = new Array(datas.length) for (var i = 0; i < datas.length; i++) { @@ -74,6 +110,20 @@ module.exports = class Connection extends Duplex { this._connection.writev(bufs, cb) } + _connect (port, ip) { + this.remotePort = port + this.remoteAddress = ip + + this._connection.connect(port, ip) + } + + _resolveAndConnect (port, host) { + dns.lookup(host, { family: 4 }, (err, ip) => { + if (err) this.destroy(err) + else this._connect(port, ip) + }) + } + _ontimeout () { this.emit('timeout') } @@ -101,57 +151,6 @@ module.exports = class Connection extends Duplex { _onconnect () { this.emit('connect') } - - _resolveAndConnect (port, host) { - dns.lookup(host, { family: 4 }, (err, ip) => { - if (err) return this.destroy(err) - if (!ip) return this.destroy(new Error('Could not resolve ' + host)) - this._connect(port, ip) - }) - } - - _connect (port, ip) { - this.remotePort = port - this.remoteAddress = ip - - this._connection.connect(port, ip) - } - - _open (cb) { - if (this._connection.connected) cb(null) - else { - this._connection.once('connect', () => { - if (this._timeout) this._timeout.refresh() - cb(null) - }) - } - } - - _destroy (cb) { - this._connection.close((err) => { - if (err) return cb(err) - - unordered.remove(this._utp.connections, this) - - if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { - this._utp.firewall(false) - } - - this._utp._closeMaybe() - - cb(null) - }) - } - - _destroyMaybe () { - if (this._allowOpen && !--this._allowOpen) this.destroy() - } - - _final (cb) { - this._connection.shutdown() - this._destroyMaybe() - cb(null) - } } const INITIALIZED = 1 @@ -241,7 +240,7 @@ class UTPConnection extends EventEmitter { } _init () { - if (this._state & INITIALIZED) return + if ((this._state & INITIALIZED) !== 0) return this._state |= INITIALIZED From 9f85a388e1a48a890920ca68fe4d238b8b151bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 13:36:51 +0100 Subject: [PATCH 21/81] Progress on refactor --- binding.cc | 5 + index.js | 374 ++++++++++++++++++++++-------------------- lib/connection.js | 314 ----------------------------------- lib/utp-connection.js | 191 +++++++++++++++++++++ lib/utp-socket.js | 253 ++++++++++++++++++++++++++++ 5 files changed, 648 insertions(+), 489 deletions(-) delete mode 100644 lib/connection.js create mode 100644 lib/utp-connection.js create mode 100644 lib/utp-socket.js diff --git a/binding.cc b/binding.cc index cf5c57d..f92617e 100644 --- a/binding.cc +++ b/binding.cc @@ -71,6 +71,7 @@ typedef struct { napi_ref on_close; napi_ref on_connect; napi_ref realloc; + bool destroyed; } utp_napi_connection_t; typedef struct { @@ -227,6 +228,8 @@ on_utp_firewall (utp_callback_arguments *a) { inline static void utp_napi_connection_destroy (utp_napi_connection_t *self) { + if (self->destroyed) return; + UTP_NAPI_CALLBACK(self->on_close, { NAPI_MAKE_CALLBACK(env, NULL, ctx, callback, 0, NULL, NULL) }) @@ -234,6 +237,7 @@ utp_napi_connection_destroy (utp_napi_connection_t *self) { self->env = env; self->buf.base = NULL; self->buf.len = 0; + self->destroyed = true; napi_delete_reference(self->env, self->ctx); napi_delete_reference(self->env, self->on_read); @@ -266,6 +270,7 @@ on_utp_state_change (utp_callback_arguments *a) { } case UTP_STATE_EOF: { + if (self->destroyed) return 0; if (self->recv_packet_size) { UTP_NAPI_CALLBACK(self->on_read, { napi_value ret; diff --git a/index.js b/index.js index 698e7f4..9a07aed 100644 --- a/index.js +++ b/index.js @@ -1,64 +1,30 @@ -const binding = require('./lib/binding') -const Connection = require('./lib/connection') const EventEmitter = require('events') +const net = require('net') const dns = require('dns') -const set = require('unordered-set') +const { Duplex } = require('streamx') +const timeout = require('timeout-refresh') const b4a = require('b4a') -const queueTick = require('queue-tick') - -const EMPTY = b4a.alloc(0) -const IPv4Pattern = /^((?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])[.]){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ +const binding = require('./lib/binding') +const UTPConnection = require('./lib/utp-connection') +const UTPSocket = require('./lib/utp-socket') const Socket = module.exports = class Socket extends EventEmitter { constructor (opts) { super() this.connections = [] + this.maxConnections = 0 - this._sending = [] - this._sent = [] - this._offset = 0 - this._buffer = b4a.allocUnsafe(2 * 65536) - this._handle = b4a.alloc(binding.sizeof_utp_napi_t) - this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) this._address = null - this._inited = false - this._refed = true - this._closing = false - this._closed = false + this._socket = new UTPSocket() this._allowHalfOpen = !opts || opts.allowHalfOpen !== false - this._acceptConnections = new Uint32Array(this._handle.buffer, this._handle.byteOffset + binding.offsetof_utp_napi_t_accept_connections, 1) - this.maxConnections = 0 - } - - _init () { - this._inited = true - binding.utp_napi_init(this._handle, this, - this._nextConnection, - this._buffer, - this._onmessage, - this._onsend, - this._onconnection, - this._onclose, - this._realloc - ) - - if (!this._refed) this.unref() - } - - firewall (yes) { - this._acceptConnections[0] = yes ? 0 : 1 + this._socket + .on('connection', this._onconnection.bind(this)) } - ref () { - if (this._inited) binding.utp_napi_ref(this._handle) - this._refed = true - } - - unref () { - if (this._inited) binding.utp_napi_unref(this._handle) - this._refed = false + firewall (enable) { + this._socket.firewall(enable) } address () { @@ -66,108 +32,76 @@ const Socket = module.exports = class Socket extends EventEmitter { return { address: this._address, family: 'IPv4', - port: binding.utp_napi_local_port(this._handle) + port: binding.utp_napi_local_port(this._socket._handle) } } getRecvBufferSize () { if (!this._inited) throw new Error('getRecvBufferSize EBADF') if (this._closing) return 0 - return binding.utp_napi_recv_buffer(this._handle, 0) + return binding.utp_napi_recv_buffer(this._socket._handle, 0) } setRecvBufferSize (n) { if (!this._inited) throw new Error('setRecvBufferSize EBADF') if (this._closing) return 0 - return binding.utp_napi_recv_buffer(this._handle, n) + return binding.utp_napi_recv_buffer(this._socket._handle, n) } getSendBufferSize () { if (!this._inited) throw new Error('getSendBufferSize EBADF') if (this._closing) return 0 - return binding.utp_napi_send_buffer(this._handle, 0) + return binding.utp_napi_send_buffer(this._socket._handle, 0) } setSendBufferSize (n) { if (!this._inited) throw new Error('setSendBufferSize EBADF') if (this._closing) return 0 - return binding.utp_napi_send_buffer(this._handle, n) + return binding.utp_napi_send_buffer(this._socket._handle, n) } setTTL (ttl) { if (!this._inited) throw new Error('setTTL EBADF') if (this._closing) return - binding.utp_napi_set_ttl(this._handle, ttl) + binding.utp_napi_set_ttl(this._socket._handle, ttl) } - send (buf, offset, len, port, host, cb) { - if (!cb) cb = noop - if (!isIP(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) - if (this._closing) return queueTick(() => cb(new Error('Socket is closed'))) - if (!this._address) this.bind(0) - - var send = this._sent.pop() - if (!send) { - send = new SendRequest() - binding.utp_napi_send_request_init(send._handle, send) - } - - send._index = this._sending.push(send) - 1 - send._buffer = buf - send._callback = cb - - binding.utp_napi_send(this._handle, send._handle, send._buffer, offset, len, port, host) - } - - _resolveAndSend (buf, offset, len, port, host, cb) { - const self = this - - dns.lookup(host, { family: 4 }, onlookup) - - function onlookup (err, ip) { - if (err) return cb(err) - if (!ip) return cb(new Error('Could not resolve ' + host)) - self.send(buf, offset, len, port, ip, cb) - } - } + connect (port, ip) { + if (!this._socket.bound) this.bind() + if (!ip) ip = '127.0.0.1' - close (cb) { - if (this._closed) return queueTick(() => cb && cb()) - if (cb) this.once('close', cb) - if (this._closing) return - this._closing = true - this._closeMaybe() - } + const connection = new Connection( + this, + null, + port, + ip, + this._allowHalfOpen + ) - _closeMaybe () { - if (this._closing && !this.connections.length && !this._sending.length && this._inited && !this._closed) { - this._closed = true - binding.utp_napi_close(this._handle) - } - } + if (!net.isIPv4(ip)) connection._resolveAndConnect(port, ip) + else connection._connect(port, ip) - connect (port, ip) { - if (!this._inited) this.bind() - if (!ip) ip = '127.0.0.1' - const conn = new Connection(this, port, ip, null, this._allowHalfOpen) - if (!isIP(ip)) conn._resolveAndConnect(port, ip) - else conn._connect(port, ip || '127.0.0.1') - return conn + return connection } listen (port, ip, onlistening) { - if (!this._address) this.bind(port, ip, onlistening) + if (!this._socket.bound) this.bind(port, ip, onlistening) this.firewall(false) } + send (buf, offset, len, port, host, cb) { + if (!this._socket.bound) this._socket.bind() + if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) + if (this._closing) return cb(new Error('Socket is closed')) + } + bind (port, ip, onlistening) { if (typeof port === 'function') return this.bind(0, null, port) if (typeof ip === 'function') return this.bind(port, null, ip) if (!port) port = 0 if (!ip) ip = '0.0.0.0' - if (!this._inited) this._init() - if (this._closing) return + if (this._socket.closing) return if (this._address) { this.emit('error', new Error('Socket already bound')) @@ -175,76 +109,57 @@ const Socket = module.exports = class Socket extends EventEmitter { } if (onlistening) this.once('listening', onlistening) - if (!isIP(ip)) return this._resolveAndBind(port, ip) + if (!net.isIPv4(ip)) return this._resolveAndBind(port, ip) this._address = ip - try { - binding.utp_napi_bind(this._handle, port, ip) - } catch (err) { - this._address = null - queueTick(() => this.emit('error', err)) - return - } - - queueTick(() => this.emit('listening')) + this._socket.bind(port, ip, (err) => { + if (err) { + this._address = null + this.emit('error', err) + } else { + this.emit('listening') + } + }) } - _resolveAndBind (port, host) { - const self = this + close (cb) { + if (this._socket.closed) return cb && cb(null) + if (cb) this.once('close', cb) + if (this._socket.closing) return - dns.lookup(host, { family: 4 }, function (err, ip) { - if (err) return self.emit('error', err) - self.bind(port, ip) + this._socket.close((err) => { + if (err) this.emit('error', err) }) } - _realloc () { - this._buffer = b4a.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer + _closeMaybe () { + // if (this._closing && !this.connections.length && !this._sending.length && this._inited && !this._closed) { + // this._closed = true + // binding.utp_napi_close(this._handle) + // } } - _onmessage (size, port, address) { - if (size < 0) { - this.emit('error', new Error('Read failed (status: ' + size + ')')) - return EMPTY - } - - const message = this._buffer.subarray(this._offset, this._offset += size) - this.emit('message', message, { address, family: 'IPv4', port }) - - if (this._buffer.length - this._offset <= 65536) { - this._buffer = b4a.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer - } - - return EMPTY + _resolveAndSend (buf, offset, len, port, host, cb) { + dns.lookup(host, { family: 4 }, (err, ip) => { + if (err) cb(err) + else this.send(buf, offset, len, port, ip, cb) + }) } - _onsend (send, status) { - const cb = send._callback - - send._callback = send._buffer = null - set.remove(this._sending, send) - this._sent.push(send) - if (this._closing) this._closeMaybe() - - cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + _resolveAndBind (port, host) { + dns.lookup(host, { family: 4 }, (err, ip) => { + if (err) this.emit('error', err) + else this.bind(port, ip) + }) } - _onconnection (port, addr) { - const conn = new Connection(this, port, addr, this._nextConnection, this._allowHalfOpen) - this.emit('connection', conn) - this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) - return this._nextConnection + _onsend () { + if (this._socket.closing) this._closeMaybe() } - _onclose () { - binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) - this._handle = null - this.emit('close') + _onconnection (port, ip, handle) { + this.emit('connection', new Connection(this, handle, port, ip, this._allowHalfOpen)) } static createServer (opts, onconnection) { @@ -258,30 +173,139 @@ const Socket = module.exports = class Socket extends EventEmitter { } static connect (port, host, opts) { - const udp = new Socket(opts) - return udp.connect(port, host).on('close', ononeoffclose) + const socket = new Socket(opts) + return socket + .connect(port, host) + .on('close', () => socket.close()) } } Socket.Socket = Socket -function SendRequest () { - this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) - this._buffer = null - this._callback = null - this._index = null -} +class Connection extends Duplex { + constructor (socket, connection, port, address, halfOpen) { + super() -function noop () {} + this.remoteAddress = address + this.remoteFamily = 'IPv4' + this.remotePort = port + + this._index = -1 + this._socket = socket + this._connection = connection || new UTPConnection(this._socket._socket) + this._view = new Uint32Array(this._connection._handle.buffer, this._connection._handle.byteOffset, 2) + this._timeout = null + this._contentSize = 0 + this._allowOpen = halfOpen ? 2 : 1 + + this._connection + .on('data', (buffer) => this._onread(buffer)) + .on('end', () => this._onend()) + .on('connect', () => this._onconnect()) + } -function isIP (ip) { - return IPv4Pattern.test(ip) -} + setTimeout (ms, ontimeout) { + if (ontimeout) this.once('timeout', ontimeout) + if (this._timeout) this._timeout.destroy() + this._timeout = timeout(ms, this._ontimeout, this) + } -function toHandle (obj) { - return obj._handle -} + setInteractive (interactive) { + this.setPacketSize(interactive ? 0 : 65536) + } + + setContentSize (size) { + this._view[0] = size < 65536 ? (size >= 0 ? size : 0) : 65536 + this._contentSize = size + } + + setPacketSize (size) { + if (size > 65536) size = 65536 + this._view[0] = size + this._contentSize = 0 + } + + address () { + return this.destroyed ? null : this._socket.address() + } + + _open (cb) { + if (this._connection.connected) cb(null) + else { + this._connection.once('connect', () => { + if (this._timeout) this._timeout.refresh() + cb(null) + }) + } + } + + _destroy (cb) { + this._connection.close(cb) + } + + _destroyMaybe () { + if (this._allowOpen && !--this._allowOpen) this.destroy() + } + + _final (cb) { + this._connection.shutdown() + this._destroyMaybe() + cb(null) + } + + _writev (datas, cb) { + let bufs = new Array(datas.length) + for (var i = 0; i < datas.length; i++) { + const data = datas[i] + bufs[i] = typeof data === 'string' ? b4a.from(data) : data + } + + if (bufs.length > 256) bufs = [b4a.concat(bufs)] + + this._connection.writev(bufs, cb) + } + + _connect (port, ip) { + this.remotePort = port + this.remoteAddress = ip + + this._connection.connect(port, ip, (err) => { + if (err) this.emit('error', err) + }) + } + + _resolveAndConnect (port, host) { + dns.lookup(host, { family: 4 }, (err, ip) => { + if (err) this.destroy(err) + else this._connect(port, ip) + }) + } + + _ontimeout () { + this.emit('timeout') + } -function ononeoffclose () { - this._utp.close() + _onread (buffer) { + if (this._timeout) this._timeout.refresh() + + let size = buffer.byteLength + + if (this._contentSize) { + if (size > this._contentSize) size = this._contentSize + this._contentSize -= size + if (this._contentSize < 65536) this._view[0] = this._contentSize + } + + this.push(buffer) + } + + _onend () { + if (this._timeout) this._timeout.destroy() + this.push(null) + this._destroyMaybe() + } + + _onconnect () { + this.emit('connect') + } } diff --git a/lib/connection.js b/lib/connection.js deleted file mode 100644 index e8e040c..0000000 --- a/lib/connection.js +++ /dev/null @@ -1,314 +0,0 @@ -const EventEmitter = require('events') -const { Duplex } = require('streamx') -const unordered = require('unordered-set') -const dns = require('dns') -const timeout = require('timeout-refresh') -const b4a = require('b4a') -const binding = require('./binding') - -const EMPTY = b4a.alloc(0) - -module.exports = class Connection extends Duplex { - constructor (utp, port, address, handle, halfOpen) { - super() - - this.remoteAddress = address - this.remoteFamily = 'IPv4' - this.remotePort = port - - this._index = -1 - this._utp = utp - this._connection = new UTPConnection(utp, handle) - this._view = new Uint32Array(this._connection._handle.buffer, this._connection._handle.byteOffset, 2) - this._timeout = null - this._contentSize = 0 - this._allowOpen = halfOpen ? 2 : 1 - - unordered.add(this._utp.connections, this) - - if (this._utp.maxConnections && this._utp.connections.length >= this._utp.maxConnections) { - this._utp.firewall(true) - } - - this._connection - .on('data', (buffer) => this._onread(buffer)) - .on('end', () => this._onend()) - .on('connect', () => this._onconnect()) - } - - setTimeout (ms, ontimeout) { - if (ontimeout) this.once('timeout', ontimeout) - if (this._timeout) this._timeout.destroy() - this._timeout = timeout(ms, this._ontimeout, this) - } - - setInteractive (interactive) { - this.setPacketSize(interactive ? 0 : 65536) - } - - setContentSize (size) { - this._view[0] = size < 65536 ? (size >= 0 ? size : 0) : 65536 - this._contentSize = size - } - - setPacketSize (size) { - if (size > 65536) size = 65536 - this._view[0] = size - this._contentSize = 0 - } - - address () { - if (this.destroyed) return null - return this._utp.address() - } - - _open (cb) { - if (this._connection.connected) cb(null) - else { - this._connection.once('connect', () => { - if (this._timeout) this._timeout.refresh() - cb(null) - }) - } - } - - _destroy (cb) { - this._connection.close((err) => { - if (err) return cb(err) - - unordered.remove(this._utp.connections, this) - - if (!this._utp.maxConnections || this._utp.connections.length < this._utp.maxConnections) { - this._utp.firewall(false) - } - - this._utp._closeMaybe() - - cb(null) - }) - } - - _destroyMaybe () { - if (this._allowOpen && !--this._allowOpen) this.destroy() - } - - _final (cb) { - this._connection.shutdown() - this._destroyMaybe() - cb(null) - } - - _writev (datas, cb) { - let bufs = new Array(datas.length) - for (var i = 0; i < datas.length; i++) { - const data = datas[i] - bufs[i] = typeof data === 'string' ? b4a.from(data) : data - } - - if (bufs.length > 256) bufs = [b4a.concat(bufs)] - - this._connection.writev(bufs, cb) - } - - _connect (port, ip) { - this.remotePort = port - this.remoteAddress = ip - - this._connection.connect(port, ip) - } - - _resolveAndConnect (port, host) { - dns.lookup(host, { family: 4 }, (err, ip) => { - if (err) this.destroy(err) - else this._connect(port, ip) - }) - } - - _ontimeout () { - this.emit('timeout') - } - - _onread (buffer) { - if (this._timeout) this._timeout.refresh() - - let size = buffer.byteLength - - if (this._contentSize) { - if (size > this._contentSize) size = this._contentSize - this._contentSize -= size - if (this._contentSize < 65536) this._view[0] = this._contentSize - } - - this.push(buffer) - } - - _onend () { - if (this._timeout) this._timeout.destroy() - this.push(null) - this._destroyMaybe() - } - - _onconnect () { - this.emit('connect') - } -} - -const INITIALIZED = 1 -const CONNECTING = 1 << 1 -const CONNECTED = 1 << 2 -const CLOSING = 1 << 3 -const CLOSED = 1 << 4 -const SHUTDOWN = 1 << 5 - -class UTPConnection extends EventEmitter { - constructor (socket, handle) { - super() - - this._socket = socket - this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) - - this._state = 0 - this._buffer = b4a.allocUnsafe(65536 * 2) - this._offset = 0 - this._writing = null - - if (handle) { - this._init() - this._state |= CONNECTED - } - } - - get connected () { - return (this._state & CONNECTED) !== 0 - } - - connect (port, ip, cb) { - if ((this._state & (CONNECTED | CONNECTING)) !== 0) return cb && cb(new Error('Already connected')) - if ((this._state & INITIALIZED) === 0) this._init() - - if (cb) this.once('connect', cb) - - this._state |= CONNECTING - - binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) - } - - write (data, cb) { - if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) - - const drained = binding.utp_napi_connection_write(this._handle, data) === 1 - - if (drained) cb(null) - else this._writing = [cb, data] - - return drained - } - - writev (batch, cb) { - if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) - - const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 - - if (drained) cb(null) - else this._writing = [cb, batch] - - return drained - } - - shutdown () { - if ((this._state & SHUTDOWN) !== 0) return - - this._state |= SHUTDOWN - - binding.utp_napi_connection_shutdown(this._handle) - } - - close (cb) { - if ((this._state & INITIALIZED) === 0) return cb && cb(null) - - if (cb) this.once('close', cb) - - if ((this._state & (CLOSED | CLOSING)) === 0) { - if ((this._state & CONNECTED) === 0) { - binding.utp_napi_connection_on_close(this._handle) - } else { - binding.utp_napi_connection_close(this._handle) - } - - this._state |= CLOSING - } - } - - _init () { - if ((this._state & INITIALIZED) !== 0) return - - this._state |= INITIALIZED - - binding.utp_napi_connection_init(this._handle, this, this._buffer, - this._onread, - this._ondrain, - this._onend, - this._onerror, - this._onclose, - this._onconnect, - this._realloc - ) - } - - _onread (size) { - const buffer = this._buffer.subarray(this._offset, this._offset += size) - - this.emit('data', buffer) - - if (this._buffer.length - this._offset <= 69632) return this._realloc() - - return EMPTY - } - - _ondrain () { - const cb = this._writing[0] - this._writing = null - cb(null) - } - - _onend () { - this.emit('end') - } - - _onerror (code) { - this.emit('error', createUTPError(code)) - } - - _onclose () { - this._state &= ~CLOSING - this._state |= CLOSED - this.emit('close') - } - - _onconnect () { - this._state &= ~CONNECTING - this._state |= CONNECTED - this.emit('connect') - } - - _realloc () { - this._buffer = b4a.allocUnsafe(this._buffer.length) - this._offset = 0 - return this._buffer - } -} - -const UTP_ERRORS = [ - 'UTP_ECONNREFUSED', - 'UTP_ECONNRESET', - 'UTP_ETIMEDOUT', - 'UTP_UNKNOWN' -] - -function createUTPError (code) { - const str = UTP_ERRORS[code < 0 ? 3 : code] - const err = new Error(str) - err.code = str - err.errno = code - return err -} diff --git a/lib/utp-connection.js b/lib/utp-connection.js new file mode 100644 index 0000000..0a67dbf --- /dev/null +++ b/lib/utp-connection.js @@ -0,0 +1,191 @@ +const EventEmitter = require('events') +const set = require('unordered-set') +const b4a = require('b4a') +const binding = require('./binding') + +const EMPTY = b4a.alloc(0) + +const INITIALIZED = 1 +const CONNECTING = 1 << 1 +const CONNECTED = 1 << 2 +const CLOSING = 1 << 3 +const CLOSED = 1 << 4 + +class UTPConnection extends EventEmitter { + constructor (socket, handle) { + super() + + this._socket = socket + this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) + + this._index = null + this._state = 0 + this._buffer = b4a.allocUnsafe(65536 * 2) + this._offset = 0 + this._writing = null + this._id = Math.random().toString(36).substring(2, 4) + + set.add(this._socket._connections, this) + + if (handle) this._init() + } + + get connected () { + return (this._state & CONNECTED) !== 0 + } + + connect (port, ip, cb) { + if ((this._state & (CONNECTED | CONNECTING)) !== 0) return cb(new Error('Already connected')) + if ((this._state & INITIALIZED) === 0) this._init() + + this._socket._ensureBound((err) => { + if (err) return cb(err) + + this.once('connect', cb) + + this._state |= CONNECTING + + binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) + }) + } + + write (data, cb) { + if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) + + const drained = binding.utp_napi_connection_write(this._handle, data) === 1 + + if (drained) cb(null) + else this._writing = [cb, data] + + return drained + } + + writev (batch, cb) { + if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) + + const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 + + if (drained) cb(null) + else this._writing = [cb, batch] + + return drained + } + + shutdown (cb) { + if ((this._state & INITIALIZED) === 0) return cb(null) + + this._state &= ~CONNECTED + + binding.utp_napi_connection_shutdown(this._handle) + + cb(null) + } + + close (cb) { + if ((this._state & INITIALIZED) === 0) return cb(null) + + this._ensureShutdown((err) => { + if (err) return cb(err) + + this.once('close', cb) + + if ((this._state & (CLOSED | CLOSING)) === 0) { + const state = this._state + + this._state |= CLOSING + + if ((state & CONNECTED) === 0) { + binding.utp_napi_connection_on_close(this._handle) + } else { + binding.utp_napi_connection_close(this._handle) + } + } + }) + } + + _init () { + if ((this._state & INITIALIZED) !== 0) return + + this._state |= INITIALIZED + + binding.utp_napi_connection_init(this._handle, this, this._buffer, + this._onread, + this._ondrain, + this._onend, + this._onerror, + this._onclose, + this._onconnect, + this._realloc + ) + } + + _onread (size) { + if ((this._state & CONNECTED) === 0) this._onconnect() + + const buffer = this._buffer.subarray(this._offset, this._offset += size) + + this.emit('data', buffer) + + if (this._buffer.length - this._offset <= 69632) return this._realloc() + + return EMPTY + } + + _ondrain () { + const cb = this._writing[0] + this._writing = null + cb(null) + } + + _onend () { + this.emit('end') + } + + _onerror (code) { + this.emit('error', createUTPError(code)) + } + + _onclose () { + set.remove(this._socket._connections, this) + + this._state &= ~CLOSING + this._state |= CLOSED + + this.emit('close') + } + + _onconnect () { + this._state &= ~CONNECTING + this._state |= CONNECTED + + this.emit('connect') + } + + _realloc () { + this._buffer = b4a.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer + } + + _ensureShutdown (cb) { + if ((this._state & CONNECTED) !== 0) this.shutdown(cb) + else cb(null) + } +} + +const UTP_ERRORS = [ + 'UTP_ECONNREFUSED', + 'UTP_ECONNRESET', + 'UTP_ETIMEDOUT', + 'UTP_UNKNOWN' +] + +function createUTPError (code) { + const str = UTP_ERRORS[code < 0 ? 3 : code] + const err = new Error(str) + err.code = str + err.errno = code + return err +} + +module.exports = UTPConnection diff --git a/lib/utp-socket.js b/lib/utp-socket.js new file mode 100644 index 0000000..30cc3d6 --- /dev/null +++ b/lib/utp-socket.js @@ -0,0 +1,253 @@ +const EventEmitter = require('events') +const set = require('unordered-set') +const b4a = require('b4a') +const binding = require('./binding') +const UTPConnection = require('./utp-connection') + +const EMPTY = b4a.alloc(0) + +const INITIALIZED = 1 +const BOUND = 1 << 1 +const CLOSING = 1 << 2 +const CLOSED = 1 << 3 +const UNREFED = 1 << 4 + +class UTPSocket extends EventEmitter { + constructor () { + super() + + this._connections = [] + this._sending = [] + this._sent = [] + this._handle = b4a.alloc(binding.sizeof_utp_napi_t) + this._state = 0 + this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) + this._buffer = b4a.allocUnsafe(65536 * 2) + this._offset = 0 + this._accept = new Uint32Array( + this._handle.buffer, + this._handle.byteOffset + binding.offsetof_utp_napi_t_accept_connections, + 1 + ) + } + + get bound () { + return (this._state & BOUND) !== 0 + } + + get closing () { + return (this._state & CLOSING) !== 0 + } + + get closed () { + return (this._state & CLOSED) !== 0 + } + + get unrefed () { + return (this._state & UNREFED) !== 0 + } + + get drained () { + return this._sending.length === 0 + } + + firewall (enable) { + this._accept[0] = enable ? 0 : 1 + } + + ref () { + if ((this._state & INITIALIZED) !== 0) binding.utp_napi_ref(this._handle) + this._refed &= ~UNREFED + } + + unref () { + if ((this._state & INITIALIZED) !== 0) binding.utp_napi_unref(this._handle) + this._state |= UNREFED + } + + bind (port, ip, cb) { + if ((this._state & BOUND) !== 0) return cb(new Error('Already bound')) + if ((this._state & INITIALIZED) === 0) this._init() + + this._state |= BOUND + + try { + binding.utp_napi_bind(this._handle, port, ip) + + cb(null) + } catch (err) { + cb(err) + } + } + + listen (port, ip, cb) { + return this.bind(port, ip, (err) => { + if (err) return cb(err) + this.firewall(false) + cb(null) + }) + } + + send (buffer, offset, len, port, ip, cb) { + if ((this._state & (CLOSED | CLOSING)) === 0) return cb(new Error('Socket is closed')) + + this._ensureBound((err) => { + if (err) return cb(err) + + const request = this._sent.pop() || new UTPSendRequest() + + request.send(buffer, offset, len, port, ip, cb) + }) + } + + connect (port, ip, cb) { + const connection = new UTPConnection(this) + + this._ensureBound((err) => { + if (err) return cb(err) + + connection.connect(port, ip, (err) => { + if (err) return cb(err) + cb(null, connection) + }) + }) + + return connection + } + + close (cb) { + if ((this._state & INITIALIZED) === 0) return cb(null) + if ((this._state & CLOSED) !== 0) return cb(new Error('Already closed')) + + this.once('close', cb) + + if ((this._state & CLOSING) === 0) { + this._state |= CLOSING + + binding.utp_napi_close(this._handle) + } + } + + static connect (port, ip, cb) { + const socket = new UTPSocket() + return socket + .on('close', () => socket.close()) + .connect(port, ip, cb) + } + + _init () { + if ((this._state & INITIALIZED) !== 0) return + + this._state |= INITIALIZED + + binding.utp_napi_init(this._handle, this, this._nextConnection, this._buffer, + this._onmessage, + this._onsend, + this._onconnection, + this._onclose, + this._realloc + ) + + if ((this._state & UNREFED) === 0) this.ref() + else this.unref() + } + + _onmessage (size, port, ip) { + if (size < 0) { + this.emit('error', new Error('Read failed (status: ' + size + ')')) + return EMPTY + } + + const message = this._buffer.subarray(this._offset, this._offset += size) + + this.emit('message', message, { address: ip, family: 'IPv4', port }) + + if (this._buffer.length - this._offset <= 65536) return this._realloc() + + return EMPTY + } + + _onsend (request, status) { + request.finish(status) + } + + _onconnection (port, ip) { + const connection = new UTPConnection(this, this._nextConnection) + + this.emit('connection', port, ip, connection) + + this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) + return this._nextConnection + } + + _onclose () { + this._state &= ~CLOSING + this._state |= CLOSED + + binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) + + this.emit('close') + } + + _realloc () { + this._buffer = b4a.allocUnsafe(this._buffer.length) + this._offset = 0 + return this._buffer + } + + _ensureBound (cb) { + if ((this._state & BOUND) === 0) this.bind(0, '127.0.0.1', cb) + else cb(null) + } +} + +class UTPSendRequest { + constructor (socket) { + this._socket = socket + + this._index = null + this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) + this._buffer = null + this._callback = null + + this._init() + } + + send (buffer, offset, length, port, ip, cb) { + this._index = this._socket._sending.push(this) - 1 + this._buffer = buffer + this._callback = cb + + binding.utp_napi_send(this._socket._handle, this._handle, + buffer, + offset, + length, + port, + ip + ) + } + + finish (status) { + set.remove(this._socket._sending, this) + + this._socket._sent.push(this) + + const cb = this._callback + + this._index = null + this._buffer = null + this._callback = null + + if (cb) cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + } + + _init () { + binding.utp_napi_send_request_init(this._handle, this) + } +} + +function toHandle (obj) { + return obj._handle +} + +module.exports = UTPSocket From e53e4be8e3306837106b2f1d21510333d8570e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 13:49:00 +0100 Subject: [PATCH 22/81] Remove ID --- lib/utp-connection.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 0a67dbf..27d0e9d 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -23,7 +23,6 @@ class UTPConnection extends EventEmitter { this._buffer = b4a.allocUnsafe(65536 * 2) this._offset = 0 this._writing = null - this._id = Math.random().toString(36).substring(2, 4) set.add(this._socket._connections, this) From 081d69679b18578818c872821beb5f5143ac3906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 13:54:30 +0100 Subject: [PATCH 23/81] Fix one-off closing of socket --- lib/utp-socket.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 30cc3d6..b8038bf 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -131,8 +131,10 @@ class UTPSocket extends EventEmitter { static connect (port, ip, cb) { const socket = new UTPSocket() return socket - .on('close', () => socket.close()) .connect(port, ip, cb) + .on('close', () => socket.close((err) => { + if (err) socket.emit('error', err) + })) } _init () { From 44d02037ce9d00813c6e3c9b7cebc83cbb0bd8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 14:36:57 +0100 Subject: [PATCH 24/81] Fix `Connection#_final()` --- index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 9a07aed..06c84c8 100644 --- a/index.js +++ b/index.js @@ -248,9 +248,11 @@ class Connection extends Duplex { } _final (cb) { - this._connection.shutdown() - this._destroyMaybe() - cb(null) + this._connection.shutdown((err) => { + if (err) return cb(err) + this._destroyMaybe() + cb(null) + }) } _writev (datas, cb) { From 578bddfd98221434324086b9a57f014ff64e1c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 14:58:41 +0100 Subject: [PATCH 25/81] Fix connected check --- lib/utp-connection.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 27d0e9d..28072a9 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -83,20 +83,20 @@ class UTPConnection extends EventEmitter { close (cb) { if ((this._state & INITIALIZED) === 0) return cb(null) + const isConnected = (this._state & CONNECTED) !== 0 + this._ensureShutdown((err) => { if (err) return cb(err) this.once('close', cb) if ((this._state & (CLOSED | CLOSING)) === 0) { - const state = this._state - this._state |= CLOSING - if ((state & CONNECTED) === 0) { - binding.utp_napi_connection_on_close(this._handle) - } else { + if (isConnected) { binding.utp_napi_connection_close(this._handle) + } else { + binding.utp_napi_connection_on_close(this._handle) } } }) From fc05047ef67f4be2d8caaa14c5d8b4d8021ab20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:06:32 +0100 Subject: [PATCH 26/81] Remove `destroyed` guard --- binding.cc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/binding.cc b/binding.cc index f92617e..cf5c57d 100644 --- a/binding.cc +++ b/binding.cc @@ -71,7 +71,6 @@ typedef struct { napi_ref on_close; napi_ref on_connect; napi_ref realloc; - bool destroyed; } utp_napi_connection_t; typedef struct { @@ -228,8 +227,6 @@ on_utp_firewall (utp_callback_arguments *a) { inline static void utp_napi_connection_destroy (utp_napi_connection_t *self) { - if (self->destroyed) return; - UTP_NAPI_CALLBACK(self->on_close, { NAPI_MAKE_CALLBACK(env, NULL, ctx, callback, 0, NULL, NULL) }) @@ -237,7 +234,6 @@ utp_napi_connection_destroy (utp_napi_connection_t *self) { self->env = env; self->buf.base = NULL; self->buf.len = 0; - self->destroyed = true; napi_delete_reference(self->env, self->ctx); napi_delete_reference(self->env, self->on_read); @@ -270,7 +266,6 @@ on_utp_state_change (utp_callback_arguments *a) { } case UTP_STATE_EOF: { - if (self->destroyed) return 0; if (self->recv_packet_size) { UTP_NAPI_CALLBACK(self->on_read, { napi_value ret; From c971ee06a350dfd45a17f1867aae48b440019bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:09:31 +0100 Subject: [PATCH 27/81] Handle multiple `close()` calls --- lib/utp-connection.js | 3 ++- lib/utp-socket.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 28072a9..e81f15f 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -82,6 +82,7 @@ class UTPConnection extends EventEmitter { close (cb) { if ((this._state & INITIALIZED) === 0) return cb(null) + if ((this._state & CLOSED) !== 0) return cb(null) const isConnected = (this._state & CONNECTED) !== 0 @@ -90,7 +91,7 @@ class UTPConnection extends EventEmitter { this.once('close', cb) - if ((this._state & (CLOSED | CLOSING)) === 0) { + if ((this._state & CLOSING) === 0) { this._state |= CLOSING if (isConnected) { diff --git a/lib/utp-socket.js b/lib/utp-socket.js index b8038bf..d24ad59 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -117,7 +117,7 @@ class UTPSocket extends EventEmitter { close (cb) { if ((this._state & INITIALIZED) === 0) return cb(null) - if ((this._state & CLOSED) !== 0) return cb(new Error('Already closed')) + if ((this._state & CLOSED) !== 0) return cb(null) this.once('close', cb) From 111e588e80010f259b7d6f1935d58873f963e96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:09:48 +0100 Subject: [PATCH 28/81] Simplify --- lib/utp-socket.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utp-socket.js b/lib/utp-socket.js index d24ad59..ca99ec4 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -216,7 +216,8 @@ class UTPSendRequest { } send (buffer, offset, length, port, ip, cb) { - this._index = this._socket._sending.push(this) - 1 + set.add(this._socket._sending, this) + this._buffer = buffer this._callback = cb @@ -236,11 +237,10 @@ class UTPSendRequest { const cb = this._callback - this._index = null this._buffer = null this._callback = null - if (cb) cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) } _init () { From d4af007be4eca170c9c6cfbfad6a9b39c95778d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:10:05 +0100 Subject: [PATCH 29/81] Deal with multiple writes --- lib/utp-connection.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index e81f15f..7ff2e59 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -22,7 +22,7 @@ class UTPConnection extends EventEmitter { this._state = 0 this._buffer = b4a.allocUnsafe(65536 * 2) this._offset = 0 - this._writing = null + this._writing = [] set.add(this._socket._connections, this) @@ -54,7 +54,7 @@ class UTPConnection extends EventEmitter { const drained = binding.utp_napi_connection_write(this._handle, data) === 1 if (drained) cb(null) - else this._writing = [cb, data] + else this._writing.push([cb, data]) return drained } @@ -65,7 +65,7 @@ class UTPConnection extends EventEmitter { const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 if (drained) cb(null) - else this._writing = [cb, batch] + else this._writing.push([cb, batch]) return drained } @@ -132,8 +132,7 @@ class UTPConnection extends EventEmitter { } _ondrain () { - const cb = this._writing[0] - this._writing = null + const cb = this._writing.shift() cb(null) } From d9f7ab0a0818a5a596dca4535981d4b7b92db143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:23:42 +0100 Subject: [PATCH 30/81] Add `CORKED` state --- lib/utp-connection.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 7ff2e59..6694196 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -10,6 +10,7 @@ const CONNECTING = 1 << 1 const CONNECTED = 1 << 2 const CLOSING = 1 << 3 const CLOSED = 1 << 4 +const CORKED = 1 << 5 class UTPConnection extends EventEmitter { constructor (socket, handle) { @@ -26,7 +27,10 @@ class UTPConnection extends EventEmitter { set.add(this._socket._connections, this) - if (handle) this._init() + if (handle) { + this._init() + this._state |= CORKED + } } get connected () { @@ -84,8 +88,6 @@ class UTPConnection extends EventEmitter { if ((this._state & INITIALIZED) === 0) return cb(null) if ((this._state & CLOSED) !== 0) return cb(null) - const isConnected = (this._state & CONNECTED) !== 0 - this._ensureShutdown((err) => { if (err) return cb(err) @@ -94,7 +96,7 @@ class UTPConnection extends EventEmitter { if ((this._state & CLOSING) === 0) { this._state |= CLOSING - if (isConnected) { + if ((this._state & CORKED) === 0) { binding.utp_napi_connection_close(this._handle) } else { binding.utp_napi_connection_on_close(this._handle) From 33924e82b34c47b1426a95d93f0d057aec5ffc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:28:21 +0100 Subject: [PATCH 31/81] Re-use `CONNECTING` state --- lib/utp-connection.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 6694196..57803f0 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -10,7 +10,6 @@ const CONNECTING = 1 << 1 const CONNECTED = 1 << 2 const CLOSING = 1 << 3 const CLOSED = 1 << 4 -const CORKED = 1 << 5 class UTPConnection extends EventEmitter { constructor (socket, handle) { @@ -29,7 +28,7 @@ class UTPConnection extends EventEmitter { if (handle) { this._init() - this._state |= CORKED + this._state |= CONNECTING } } @@ -96,7 +95,7 @@ class UTPConnection extends EventEmitter { if ((this._state & CLOSING) === 0) { this._state |= CLOSING - if ((this._state & CORKED) === 0) { + if ((this._state & CONNECTING) === 0) { binding.utp_napi_connection_close(this._handle) } else { binding.utp_napi_connection_on_close(this._handle) From fcf56676b0c865fb65f090b9ca41d83c19bd5b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:39:13 +0100 Subject: [PATCH 32/81] Fix typo --- lib/utp-connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 57803f0..e98c47d 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -133,7 +133,7 @@ class UTPConnection extends EventEmitter { } _ondrain () { - const cb = this._writing.shift() + const [cb] = this._writing.shift() cb(null) } From 62cfbab9c92ceeefb0882d878db3b865b8eadaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:39:34 +0100 Subject: [PATCH 33/81] Clear `CONNECTING` on shutdown --- lib/utp-connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index e98c47d..e7ac6ad 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -76,7 +76,7 @@ class UTPConnection extends EventEmitter { shutdown (cb) { if ((this._state & INITIALIZED) === 0) return cb(null) - this._state &= ~CONNECTED + this._state &= ~(CONNECTED | CONNECTING) binding.utp_napi_connection_shutdown(this._handle) From 723ed15901c3072a0277b97737176931cb57dbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 15:39:48 +0100 Subject: [PATCH 34/81] Invoke `cb` on close --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 06c84c8..73c491f 100644 --- a/index.js +++ b/index.js @@ -130,6 +130,7 @@ const Socket = module.exports = class Socket extends EventEmitter { this._socket.close((err) => { if (err) this.emit('error', err) + if (cb) cb(null) }) } From 496388ed8bc2fb51d83ccd6d659a7bbb8fdd8d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 16:17:28 +0100 Subject: [PATCH 35/81] Only close socket when idle --- index.js | 15 --------------- lib/utp-connection.js | 2 ++ lib/utp-socket.js | 9 ++++++--- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 73c491f..0957f2c 100644 --- a/index.js +++ b/index.js @@ -124,23 +124,12 @@ const Socket = module.exports = class Socket extends EventEmitter { } close (cb) { - if (this._socket.closed) return cb && cb(null) - if (cb) this.once('close', cb) - if (this._socket.closing) return - this._socket.close((err) => { if (err) this.emit('error', err) if (cb) cb(null) }) } - _closeMaybe () { - // if (this._closing && !this.connections.length && !this._sending.length && this._inited && !this._closed) { - // this._closed = true - // binding.utp_napi_close(this._handle) - // } - } - _resolveAndSend (buf, offset, len, port, host, cb) { dns.lookup(host, { family: 4 }, (err, ip) => { if (err) cb(err) @@ -155,10 +144,6 @@ const Socket = module.exports = class Socket extends EventEmitter { }) } - _onsend () { - if (this._socket.closing) this._closeMaybe() - } - _onconnection (port, ip, handle) { this.emit('connection', new Connection(this, handle, port, ip, this._allowHalfOpen)) } diff --git a/lib/utp-connection.js b/lib/utp-connection.js index e7ac6ad..d5f1d9d 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -152,6 +152,8 @@ class UTPConnection extends EventEmitter { this._state |= CLOSED this.emit('close') + + if (this._socket.idle) this._socket.emit('idle') } _onconnect () { diff --git a/lib/utp-socket.js b/lib/utp-socket.js index ca99ec4..6e5799d 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -47,8 +47,8 @@ class UTPSocket extends EventEmitter { return (this._state & UNREFED) !== 0 } - get drained () { - return this._sending.length === 0 + get idle () { + return this._connections.length === 0 && this._sending.length === 0 } firewall (enable) { @@ -124,7 +124,8 @@ class UTPSocket extends EventEmitter { if ((this._state & CLOSING) === 0) { this._state |= CLOSING - binding.utp_napi_close(this._handle) + if (this.idle) binding.utp_napi_close(this._handle) + else this.once('idle', () => binding.utp_napi_close(this._handle)) } } @@ -171,6 +172,8 @@ class UTPSocket extends EventEmitter { _onsend (request, status) { request.finish(status) + + if (this.idle) this.emit('idle') } _onconnection (port, ip) { From c7bb20066d7b2f818fcb59abab9cd082478d437e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 15 Nov 2021 16:17:34 +0100 Subject: [PATCH 36/81] Adjust tests --- test/net.js | 54 +++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/test/net.js b/test/net.js index fdf89b0..408ee3c 100644 --- a/test/net.js +++ b/test/net.js @@ -20,6 +20,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) + .write('hello') // why required? }) await close @@ -44,6 +45,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) + .write('foo') // why required? }) await close @@ -132,15 +134,16 @@ test('echo server back and fourth', (t) => withServer(t, async (server) => { t.is(echoed, 10) })) -test('echo big message', (t) => { - t.plan(2) +test.skip('echo big message', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(2) let packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') - const server = utp.createServer((socket) => { + server.on('connection', (socket) => { socket.on('data', () => packets++) socket.pipe(socket) }) @@ -159,23 +162,25 @@ test('echo big message', (t) => { ptr += data.length if (big.length === ptr) { socket.end() - server.close() t.alike(buffer, big) t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') } }) }) -}) -test('echo big message with setContentSize', (t) => { - t.plan(2) + await writes +})) + +test.skip('echo big message with setContentSize', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(2) let packets = 0 const big = Buffer.alloc(8 * 1024 * 1024) big.fill('yolo') - const server = utp.createServer((socket) => { + server.on('connection', (socket) => { socket.setContentSize(big.length) socket.on('data', () => packets++) socket.pipe(socket) @@ -196,13 +201,14 @@ test('echo big message with setContentSize', (t) => { ptr += data.length if (big.length === ptr) { socket.end() - server.close() t.alike(buffer, big) t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') } }) }) -}) + + await writes +})) test.skip('two connections', async (t) => { const writes = t.test('write and close sockets') @@ -335,23 +341,23 @@ test('timeout', (t) => withServer(t, async (server) => { await close })) -test.skip('exception in connection listener', async (t) => { - t.plan(1) +// test.skip('exception in connection listener', async (t) => { +// t.plan(1) - const server = utp.createServer((socket) => { - socket.destroy() - throw new Error('disconnect') - }) +// const server = utp.createServer((socket) => { +// socket.destroy() +// throw new Error('disconnect') +// }) - process.once('uncaughtException', () => { - server.close() - t.pass() - }) +// process.once('uncaughtException', () => { +// server.close() +// t.pass() +// }) - server.listen(() => { - utp.connect(server.address().port).destroy() - }) -}) +// server.listen(() => { +// utp.connect(server.address().port).destroy() +// }) +// }) async function withServer (t, cb) { const server = utp.createServer() From f908b9568ba7a399bfa3abdea9bd02eca9690434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 08:28:11 +0100 Subject: [PATCH 37/81] Adjust close flow --- lib/utp-connection.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index d5f1d9d..0da049e 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -87,21 +87,23 @@ class UTPConnection extends EventEmitter { if ((this._state & INITIALIZED) === 0) return cb(null) if ((this._state & CLOSED) !== 0) return cb(null) - this._ensureShutdown((err) => { - if (err) return cb(err) + this.once('close', cb) + + if ((this._state & CLOSING) === 0) { + this._state |= CLOSING - this.once('close', cb) + const canClose = (this._state & CONNECTING) === 0 - if ((this._state & CLOSING) === 0) { - this._state |= CLOSING + this._ensureShutdown((err) => { + if (err) return cb(err) - if ((this._state & CONNECTING) === 0) { + if (canClose) { binding.utp_napi_connection_close(this._handle) } else { binding.utp_napi_connection_on_close(this._handle) } - } - }) + }) + } } _init () { From 77bfd3d6ace6d18a3ed68f9bae6259d0444d87e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 08:29:16 +0100 Subject: [PATCH 38/81] Adjust tests --- test/net.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/net.js b/test/net.js index 408ee3c..5d38d49 100644 --- a/test/net.js +++ b/test/net.js @@ -9,7 +9,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes ECONNRESET? + .end() // .destroy() causes SIGSEGV? }) server.listen(() => { @@ -34,7 +34,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes ECONNRESET? + .end() // .destroy() causes SIGSEGV? }) server.listen(() => { @@ -51,7 +51,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { await close })) -test('bad resolve', (t) => { +test.skip('bad resolve', (t) => { t.plan(2) const socket = utp.connect(10000, 'domain.does-not-exist') @@ -61,7 +61,7 @@ test('bad resolve', (t) => { .on('close', () => t.pass('closed')) }) -test('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { +test.skip('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { const error = t.test('error on listen') error.plan(1) @@ -341,23 +341,23 @@ test('timeout', (t) => withServer(t, async (server) => { await close })) -// test.skip('exception in connection listener', async (t) => { -// t.plan(1) +test.skip('exception in connection listener', async (t) => { + t.plan(1) -// const server = utp.createServer((socket) => { -// socket.destroy() -// throw new Error('disconnect') -// }) + const server = utp.createServer((socket) => { + socket.destroy() + throw new Error('disconnect') + }) -// process.once('uncaughtException', () => { -// server.close() -// t.pass() -// }) + process.once('uncaughtException', () => { + server.close() + t.pass() + }) -// server.listen(() => { -// utp.connect(server.address().port).destroy() -// }) -// }) + server.listen(() => { + utp.connect(server.address().port).destroy() + }) +}) async function withServer (t, cb) { const server = utp.createServer() From a9548abf98a9d0369ae41689ce439a4eb5be1332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 09:08:47 +0100 Subject: [PATCH 39/81] Don't use `EventEmitter` for low level primitives --- index.js | 60 +++++++++++++------------- lib/utp-connection.js | 72 +++++++++++++++----------------- lib/utp-socket.js | 97 +++++++++++++++---------------------------- 3 files changed, 96 insertions(+), 133 deletions(-) diff --git a/index.js b/index.js index 0957f2c..6a76f15 100644 --- a/index.js +++ b/index.js @@ -19,8 +19,8 @@ const Socket = module.exports = class Socket extends EventEmitter { this._socket = new UTPSocket() this._allowHalfOpen = !opts || opts.allowHalfOpen !== false - this._socket - .on('connection', this._onconnection.bind(this)) + this._socket.onconnection = this._onconnection.bind(this) + this._socket.onclose = this._onclose.bind(this) } firewall (enable) { @@ -90,7 +90,7 @@ const Socket = module.exports = class Socket extends EventEmitter { } send (buf, offset, len, port, host, cb) { - if (!this._socket.bound) this._socket.bind() + if (!this._socket.bound) this.bind() if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) if (this._closing) return cb(new Error('Socket is closed')) } @@ -113,21 +113,21 @@ const Socket = module.exports = class Socket extends EventEmitter { this._address = ip - this._socket.bind(port, ip, (err) => { - if (err) { - this._address = null - this.emit('error', err) - } else { - this.emit('listening') - } - }) + try { + this._socket.bind(port, ip) + this.emit('listening') + } catch (err) { + this._address = null + this.emit('error', err) + } } close (cb) { - this._socket.close((err) => { - if (err) this.emit('error', err) - if (cb) cb(null) - }) + if (this._socket.closed) return cb() + + if (cb) this.once('close', cb) + + this._socket.close() } _resolveAndSend (buf, offset, len, port, host, cb) { @@ -148,6 +148,10 @@ const Socket = module.exports = class Socket extends EventEmitter { this.emit('connection', new Connection(this, handle, port, ip, this._allowHalfOpen)) } + _onclose () { + this.emit('close') + } + static createServer (opts, onconnection) { if (typeof opts === 'function') { onconnection = opts @@ -184,10 +188,9 @@ class Connection extends Duplex { this._contentSize = 0 this._allowOpen = halfOpen ? 2 : 1 - this._connection - .on('data', (buffer) => this._onread(buffer)) - .on('end', () => this._onend()) - .on('connect', () => this._onconnect()) + this._connection.ondata = this._ondata.bind(this) + this._connection.onend = this._onend.bind(this) + this._connection.onconnect = this._onconnect.bind(this) } setTimeout (ms, ontimeout) { @@ -218,7 +221,7 @@ class Connection extends Duplex { _open (cb) { if (this._connection.connected) cb(null) else { - this._connection.once('connect', () => { + this.once('connect', () => { if (this._timeout) this._timeout.refresh() cb(null) }) @@ -226,7 +229,8 @@ class Connection extends Duplex { } _destroy (cb) { - this._connection.close(cb) + this._connection.onclose = cb + this._connection.close() } _destroyMaybe () { @@ -234,11 +238,9 @@ class Connection extends Duplex { } _final (cb) { - this._connection.shutdown((err) => { - if (err) return cb(err) - this._destroyMaybe() - cb(null) - }) + this._connection.shutdown() + this._destroyMaybe() + cb(null) } _writev (datas, cb) { @@ -257,9 +259,7 @@ class Connection extends Duplex { this.remotePort = port this.remoteAddress = ip - this._connection.connect(port, ip, (err) => { - if (err) this.emit('error', err) - }) + this._connection.connect(port, ip) } _resolveAndConnect (port, host) { @@ -273,7 +273,7 @@ class Connection extends Duplex { this.emit('timeout') } - _onread (buffer) { + _ondata (buffer) { if (this._timeout) this._timeout.refresh() let size = buffer.byteLength diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 0da049e..4269842 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -1,4 +1,3 @@ -const EventEmitter = require('events') const set = require('unordered-set') const b4a = require('b4a') const binding = require('./binding') @@ -11,10 +10,8 @@ const CONNECTED = 1 << 2 const CLOSING = 1 << 3 const CLOSED = 1 << 4 -class UTPConnection extends EventEmitter { +class UTPConnection { constructor (socket, handle) { - super() - this._socket = socket this._handle = handle || b4a.alloc(binding.sizeof_utp_napi_connection_t) @@ -24,6 +21,12 @@ class UTPConnection extends EventEmitter { this._offset = 0 this._writing = [] + this.onerror = noop + this.onclose = noop + this.onconnect = noop + this.ondata = noop + this.onend = noop + set.add(this._socket._connections, this) if (handle) { @@ -36,19 +39,15 @@ class UTPConnection extends EventEmitter { return (this._state & CONNECTED) !== 0 } - connect (port, ip, cb) { - if ((this._state & (CONNECTED | CONNECTING)) !== 0) return cb(new Error('Already connected')) + connect (port, ip) { + if ((this._state & (CONNECTED | CONNECTING)) !== 0) throw new Error('Already connected') if ((this._state & INITIALIZED) === 0) this._init() - this._socket._ensureBound((err) => { - if (err) return cb(err) + this._socket._ensureBound() - this.once('connect', cb) - - this._state |= CONNECTING + this._state |= CONNECTING - binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) - }) + binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) } write (data, cb) { @@ -73,36 +72,30 @@ class UTPConnection extends EventEmitter { return drained } - shutdown (cb) { - if ((this._state & INITIALIZED) === 0) return cb(null) + shutdown () { + if ((this._state & INITIALIZED) === 0) return this._state &= ~(CONNECTED | CONNECTING) binding.utp_napi_connection_shutdown(this._handle) - - cb(null) } - close (cb) { - if ((this._state & INITIALIZED) === 0) return cb(null) - if ((this._state & CLOSED) !== 0) return cb(null) - - this.once('close', cb) + close () { + if ((this._state & INITIALIZED) === 0) return + if ((this._state & CLOSED) !== 0) return if ((this._state & CLOSING) === 0) { this._state |= CLOSING const canClose = (this._state & CONNECTING) === 0 - this._ensureShutdown((err) => { - if (err) return cb(err) + this._ensureShutdown() - if (canClose) { - binding.utp_napi_connection_close(this._handle) - } else { - binding.utp_napi_connection_on_close(this._handle) - } - }) + if (canClose) { + binding.utp_napi_connection_close(this._handle) + } else { + binding.utp_napi_connection_on_close(this._handle) + } } } @@ -127,7 +120,7 @@ class UTPConnection extends EventEmitter { const buffer = this._buffer.subarray(this._offset, this._offset += size) - this.emit('data', buffer) + this.ondata(buffer) if (this._buffer.length - this._offset <= 69632) return this._realloc() @@ -140,11 +133,11 @@ class UTPConnection extends EventEmitter { } _onend () { - this.emit('end') + this.onend() } _onerror (code) { - this.emit('error', createUTPError(code)) + this.onerror(createUTPError(code)) } _onclose () { @@ -153,16 +146,16 @@ class UTPConnection extends EventEmitter { this._state &= ~CLOSING this._state |= CLOSED - this.emit('close') + if (this._socket.idle) this._socket._onidle() - if (this._socket.idle) this._socket.emit('idle') + this.onclose() } _onconnect () { this._state &= ~CONNECTING this._state |= CONNECTED - this.emit('connect') + this.onconnect() } _realloc () { @@ -171,9 +164,8 @@ class UTPConnection extends EventEmitter { return this._buffer } - _ensureShutdown (cb) { - if ((this._state & CONNECTED) !== 0) this.shutdown(cb) - else cb(null) + _ensureShutdown () { + if ((this._state & CONNECTED) !== 0) this.shutdown() } } @@ -192,4 +184,6 @@ function createUTPError (code) { return err } +function noop () {} + module.exports = UTPConnection diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 6e5799d..418ab67 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -1,4 +1,3 @@ -const EventEmitter = require('events') const set = require('unordered-set') const b4a = require('b4a') const binding = require('./binding') @@ -12,10 +11,8 @@ const CLOSING = 1 << 2 const CLOSED = 1 << 3 const UNREFED = 1 << 4 -class UTPSocket extends EventEmitter { +class UTPSocket { constructor () { - super() - this._connections = [] this._sending = [] this._sent = [] @@ -29,6 +26,11 @@ class UTPSocket extends EventEmitter { this._handle.byteOffset + binding.offsetof_utp_napi_t_accept_connections, 1 ) + + this.onmessage = noop + this.onerror = noop + this.onclose = noop + this.onconnection = noop } get bound () { @@ -65,79 +67,41 @@ class UTPSocket extends EventEmitter { this._state |= UNREFED } - bind (port, ip, cb) { - if ((this._state & BOUND) !== 0) return cb(new Error('Already bound')) + bind (port, ip) { + if ((this._state & BOUND) !== 0) throw new Error('Already bound') if ((this._state & INITIALIZED) === 0) this._init() this._state |= BOUND - try { - binding.utp_napi_bind(this._handle, port, ip) - - cb(null) - } catch (err) { - cb(err) - } + binding.utp_napi_bind(this._handle, port, ip) } - listen (port, ip, cb) { - return this.bind(port, ip, (err) => { - if (err) return cb(err) - this.firewall(false) - cb(null) - }) + listen (port, ip) { + this.bind(port, ip) + this.firewall(false) } send (buffer, offset, len, port, ip, cb) { - if ((this._state & (CLOSED | CLOSING)) === 0) return cb(new Error('Socket is closed')) + if ((this._state & (CLOSED | CLOSING)) === 0) throw new Error('Socket is closed') - this._ensureBound((err) => { - if (err) return cb(err) + this._ensureBound() - const request = this._sent.pop() || new UTPSendRequest() + const request = this._sent.pop() || new UTPSendRequest() - request.send(buffer, offset, len, port, ip, cb) - }) + request.send(buffer, offset, len, port, ip, cb) } - connect (port, ip, cb) { - const connection = new UTPConnection(this) - - this._ensureBound((err) => { - if (err) return cb(err) - - connection.connect(port, ip, (err) => { - if (err) return cb(err) - cb(null, connection) - }) - }) - - return connection - } - - close (cb) { - if ((this._state & INITIALIZED) === 0) return cb(null) - if ((this._state & CLOSED) !== 0) return cb(null) - - this.once('close', cb) + close () { + if ((this._state & INITIALIZED) === 0) return + if ((this._state & CLOSED) !== 0) return if ((this._state & CLOSING) === 0) { this._state |= CLOSING - if (this.idle) binding.utp_napi_close(this._handle) - else this.once('idle', () => binding.utp_napi_close(this._handle)) + if (this.idle) this._onidle() } } - static connect (port, ip, cb) { - const socket = new UTPSocket() - return socket - .connect(port, ip, cb) - .on('close', () => socket.close((err) => { - if (err) socket.emit('error', err) - })) - } - _init () { if ((this._state & INITIALIZED) !== 0) return @@ -155,15 +119,19 @@ class UTPSocket extends EventEmitter { else this.unref() } + _onidle () { + if ((this._state & CLOSING) !== 0) binding.utp_napi_close(this._handle) + } + _onmessage (size, port, ip) { if (size < 0) { - this.emit('error', new Error('Read failed (status: ' + size + ')')) + this.onerror(new Error('Read failed (status: ' + size + ')')) return EMPTY } const message = this._buffer.subarray(this._offset, this._offset += size) - this.emit('message', message, { address: ip, family: 'IPv4', port }) + this.onmessage(message, { address: ip, family: 'IPv4', port }) if (this._buffer.length - this._offset <= 65536) return this._realloc() @@ -173,13 +141,13 @@ class UTPSocket extends EventEmitter { _onsend (request, status) { request.finish(status) - if (this.idle) this.emit('idle') + if (this.idle) this._onidle() } _onconnection (port, ip) { const connection = new UTPConnection(this, this._nextConnection) - this.emit('connection', port, ip, connection) + this.onconnection(port, ip, connection) this._nextConnection = b4a.alloc(binding.sizeof_utp_napi_connection_t) return this._nextConnection @@ -191,7 +159,7 @@ class UTPSocket extends EventEmitter { binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) - this.emit('close') + this.onclose() } _realloc () { @@ -200,9 +168,8 @@ class UTPSocket extends EventEmitter { return this._buffer } - _ensureBound (cb) { - if ((this._state & BOUND) === 0) this.bind(0, '127.0.0.1', cb) - else cb(null) + _ensureBound () { + if ((this._state & BOUND) === 0) this.bind(0, '127.0.0.1') } } @@ -255,4 +222,6 @@ function toHandle (obj) { return obj._handle } +function noop () {} + module.exports = UTPSocket From eacc8039567ccc8226a26b53fe83ca2a440f9ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 09:20:28 +0100 Subject: [PATCH 40/81] Fix closing uninitialized connection and socket --- lib/utp-connection.js | 2 +- lib/utp-socket.js | 2 +- test/net.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 4269842..3fceac5 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -81,8 +81,8 @@ class UTPConnection { } close () { - if ((this._state & INITIALIZED) === 0) return if ((this._state & CLOSED) !== 0) return + if ((this._state & INITIALIZED) === 0) return this._onclose() if ((this._state & CLOSING) === 0) { this._state |= CLOSING diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 418ab67..03e3e34 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -92,8 +92,8 @@ class UTPSocket { } close () { - if ((this._state & INITIALIZED) === 0) return if ((this._state & CLOSED) !== 0) return + if ((this._state & INITIALIZED) === 0) return this._onclose() if ((this._state & CLOSING) === 0) { this._state |= CLOSING diff --git a/test/net.js b/test/net.js index 5d38d49..29bd295 100644 --- a/test/net.js +++ b/test/net.js @@ -51,7 +51,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { await close })) -test.skip('bad resolve', (t) => { +test('bad resolve', (t) => { t.plan(2) const socket = utp.connect(10000, 'domain.does-not-exist') From b29c69e5b4e3755293b26c7f3007e322a129b57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 09:45:11 +0100 Subject: [PATCH 41/81] Immediately connect server sockets --- lib/utp-connection.js | 2 +- test/net.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 3fceac5..2f0241c 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -31,7 +31,7 @@ class UTPConnection { if (handle) { this._init() - this._state |= CONNECTING + this._state |= CONNECTED } } diff --git a/test/net.js b/test/net.js index 29bd295..bfd5b58 100644 --- a/test/net.js +++ b/test/net.js @@ -9,7 +9,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes SIGSEGV? + .end() // .destroy() hangs? }) server.listen(() => { @@ -34,7 +34,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes SIGSEGV? + .end() // .destroy() hangs? }) server.listen(() => { From 0e6b99c2d57311e1704873f6bbee22247d98718f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 09:55:15 +0100 Subject: [PATCH 42/81] Remove `utp_napi_connection_write` --- binding.cc | 15 --------------- lib/utp-connection.js | 11 ----------- 2 files changed, 26 deletions(-) diff --git a/binding.cc b/binding.cc index cf5c57d..bd61d06 100644 --- a/binding.cc +++ b/binding.cc @@ -650,20 +650,6 @@ NAPI_METHOD(utp_napi_connection_on_close) { return NULL; } -NAPI_METHOD(utp_napi_connection_write) { - NAPI_ARGV(2) - NAPI_ARGV_BUFFER_CAST(utp_napi_connection_t *, self, 0) - NAPI_ARGV_BUFFER(buf, 1) - - self->send_buffer_next = self->send_buffer; - self->send_buffer_next->iov_base = buf; - self->send_buffer_next->iov_len = buf_len; - self->send_buffer_missing = 1; - - int drained = utp_napi_connection_drain(self); - NAPI_RETURN_UINT32(drained) -} - NAPI_METHOD(utp_napi_connection_writev) { NAPI_ARGV(2) NAPI_ARGV_BUFFER_CAST(utp_napi_connection_t *, self, 0) @@ -745,7 +731,6 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(utp_napi_send_buffer) NAPI_EXPORT_FUNCTION(utp_napi_recv_buffer) NAPI_EXPORT_FUNCTION(utp_napi_connection_init) - NAPI_EXPORT_FUNCTION(utp_napi_connection_write) NAPI_EXPORT_FUNCTION(utp_napi_connection_writev) NAPI_EXPORT_FUNCTION(utp_napi_connection_close) NAPI_EXPORT_FUNCTION(utp_napi_connection_shutdown) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 2f0241c..3670b78 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -50,17 +50,6 @@ class UTPConnection { binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) } - write (data, cb) { - if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) - - const drained = binding.utp_napi_connection_write(this._handle, data) === 1 - - if (drained) cb(null) - else this._writing.push([cb, data]) - - return drained - } - writev (batch, cb) { if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) From 688078ac12c455fabc56b75755cd2307e794be83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 09:55:45 +0100 Subject: [PATCH 43/81] Move `UTPSendRequest` --- lib/utp-send-request.js | 49 +++++++++++++++++++++++++++++++++++++ lib/utp-socket.js | 53 ++++------------------------------------- 2 files changed, 54 insertions(+), 48 deletions(-) create mode 100644 lib/utp-send-request.js diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js new file mode 100644 index 0000000..806cc61 --- /dev/null +++ b/lib/utp-send-request.js @@ -0,0 +1,49 @@ +const set = require('unordered-set') +const b4a = require('b4a') +const binding = require('./binding') + +class UTPSendRequest { + constructor (socket) { + this._socket = socket + + this._index = null + this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) + this._buffer = null + this._callback = null + + this._init() + } + + send (buffer, offset, length, port, ip, cb) { + set.add(this._socket._sending, this) + + this._buffer = buffer + this._callback = cb + + binding.utp_napi_send(this._socket._handle, this._handle, + buffer, + offset, + length, + port, + ip + ) + } + + finish (status) { + set.remove(this._socket._sending, this) + + this._socket._sent.push(this) + + const cb = this._callback + + this._buffer = null + this._callback = null + + cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + } + + _init () { + binding.utp_napi_send_request_init(this._handle, this) + } +} +module.exports = UTPSendRequest diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 03e3e34..eeaefe7 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -1,7 +1,7 @@ -const set = require('unordered-set') const b4a = require('b4a') const binding = require('./binding') const UTPConnection = require('./utp-connection') +const UTPSendRequest = require('./utp-send-request') const EMPTY = b4a.alloc(0) @@ -120,7 +120,9 @@ class UTPSocket { } _onidle () { - if ((this._state & CLOSING) !== 0) binding.utp_napi_close(this._handle) + if ((this._state & CLOSED) === 0 && (this._state & CLOSING) !== 0) { + binding.utp_napi_close(this._handle) + } } _onmessage (size, port, ip) { @@ -173,55 +175,10 @@ class UTPSocket { } } -class UTPSendRequest { - constructor (socket) { - this._socket = socket - - this._index = null - this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) - this._buffer = null - this._callback = null - - this._init() - } - - send (buffer, offset, length, port, ip, cb) { - set.add(this._socket._sending, this) - - this._buffer = buffer - this._callback = cb - - binding.utp_napi_send(this._socket._handle, this._handle, - buffer, - offset, - length, - port, - ip - ) - } - - finish (status) { - set.remove(this._socket._sending, this) - - this._socket._sent.push(this) - - const cb = this._callback - - this._buffer = null - this._callback = null - - cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) - } - - _init () { - binding.utp_napi_send_request_init(this._handle, this) - } -} +module.exports = UTPSocket function toHandle (obj) { return obj._handle } function noop () {} - -module.exports = UTPSocket From b2290d64bf6cd54fa68e150b1a09f12ee155583b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:09:47 +0100 Subject: [PATCH 44/81] Feedback from @mafintosh --- index.js | 61 ++++++++++++++++++++++++++++++------------- lib/utp-connection.js | 7 ++--- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 6a76f15..e07810d 100644 --- a/index.js +++ b/index.js @@ -174,7 +174,7 @@ Socket.Socket = Socket class Connection extends Duplex { constructor (socket, connection, port, address, halfOpen) { - super() + super({ mapWritable: toBuffer }) this.remoteAddress = address this.remoteFamily = 'IPv4' @@ -188,9 +188,13 @@ class Connection extends Duplex { this._contentSize = 0 this._allowOpen = halfOpen ? 2 : 1 + this._opening = null + this._destroying = null + this._connection.ondata = this._ondata.bind(this) this._connection.onend = this._onend.bind(this) this._connection.onconnect = this._onconnect.bind(this) + this._connection.onclose = this._onclose.bind(this) } setTimeout (ms, ontimeout) { @@ -220,17 +224,24 @@ class Connection extends Duplex { _open (cb) { if (this._connection.connected) cb(null) - else { - this.once('connect', () => { - if (this._timeout) this._timeout.refresh() - cb(null) - }) + else this._opening = cb + } + + _predestroy () { + const cb = this._opening + + if (cb) { + this._opening = null + cb(new Error('Socket was destroyed')) } } _destroy (cb) { - this._connection.onclose = cb - this._connection.close() + if (this._connection.closed) cb(null) + else { + this._destroying = cb + this._connection.close() + } } _destroyMaybe () { @@ -243,16 +254,8 @@ class Connection extends Duplex { cb(null) } - _writev (datas, cb) { - let bufs = new Array(datas.length) - for (var i = 0; i < datas.length; i++) { - const data = datas[i] - bufs[i] = typeof data === 'string' ? b4a.from(data) : data - } - - if (bufs.length > 256) bufs = [b4a.concat(bufs)] - - this._connection.writev(bufs, cb) + _writev (batch, cb) { + this._connection.writev(batch.length > 256 ? [b4a.concat(batch)] : batch, cb) } _connect (port, ip) { @@ -294,6 +297,28 @@ class Connection extends Duplex { } _onconnect () { + const cb = this._opening + + if (cb) { + this._openning = null + cb(null) + } + this.emit('connect') } + + _onclose () { + const cb = this._destroying + + if (cb) { + this._destroying = null + cb(null) + } else { + this.destroy() + } + } +} + +function toBuffer (data) { + return typeof data === 'string' ? b4a.from(data) : data } diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 3670b78..18dcb77 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -19,7 +19,7 @@ class UTPConnection { this._state = 0 this._buffer = b4a.allocUnsafe(65536 * 2) this._offset = 0 - this._writing = [] + this._writing = null this.onerror = noop this.onclose = noop @@ -56,7 +56,7 @@ class UTPConnection { const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 if (drained) cb(null) - else this._writing.push([cb, batch]) + else this._writing = [cb, batch] return drained } @@ -117,7 +117,8 @@ class UTPConnection { } _ondrain () { - const [cb] = this._writing.shift() + const cb = this._writing[0] + this._wiriting = null cb(null) } From 481ca293d1985e666aebb9845c58162fba8e2155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:14:25 +0100 Subject: [PATCH 45/81] Fix bad reference --- lib/utp-socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utp-socket.js b/lib/utp-socket.js index eeaefe7..5e1d52a 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -59,7 +59,7 @@ class UTPSocket { ref () { if ((this._state & INITIALIZED) !== 0) binding.utp_napi_ref(this._handle) - this._refed &= ~UNREFED + this._state &= ~UNREFED } unref () { From 22adfd91ff2bf8503aba1532f72f75b207adaaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:14:50 +0100 Subject: [PATCH 46/81] That one passes now --- test/net.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/net.js b/test/net.js index bfd5b58..4035f5f 100644 --- a/test/net.js +++ b/test/net.js @@ -61,7 +61,7 @@ test('bad resolve', (t) => { .on('close', () => t.pass('closed')) }) -test.skip('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { +test('server listens on a port in use', (t) => withServer(t, (a) => withServer(t, async (b) => { const error = t.test('error on listen') error.plan(1) From 0309f92b527881e3ea792e1cac2ef64edee320e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:15:52 +0100 Subject: [PATCH 47/81] Add missing reference --- lib/utp-socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 5e1d52a..096cfdd 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -86,7 +86,7 @@ class UTPSocket { this._ensureBound() - const request = this._sent.pop() || new UTPSendRequest() + const request = this._sent.pop() || new UTPSendRequest(this) request.send(buffer, offset, len, port, ip, cb) } From 74ec569d308b181be3ec234866d4e9a30c257b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:30:09 +0100 Subject: [PATCH 48/81] Typo --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index e07810d..3175997 100644 --- a/index.js +++ b/index.js @@ -300,7 +300,7 @@ class Connection extends Duplex { const cb = this._opening if (cb) { - this._openning = null + this._opening = null cb(null) } From e4eee086955c2d51fee6aab48df1e732431953ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:32:17 +0100 Subject: [PATCH 49/81] Feedback from @mafintosh --- index.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 3175997..d136bf2 100644 --- a/index.js +++ b/index.js @@ -227,15 +227,19 @@ class Connection extends Duplex { else this._opening = cb } - _predestroy () { + _continueOpen (err) { const cb = this._opening if (cb) { this._opening = null - cb(new Error('Socket was destroyed')) + cb(err) } } + _predestroy () { + this._continueOpen(new Error('Socket was destroyed')) + } + _destroy (cb) { if (this._connection.closed) cb(null) else { @@ -297,13 +301,7 @@ class Connection extends Duplex { } _onconnect () { - const cb = this._opening - - if (cb) { - this._opening = null - cb(null) - } - + this._continueOpen() this.emit('connect') } From 6c7455f335c284ba08f7497837d8f51f0180c5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:36:34 +0100 Subject: [PATCH 50/81] Implement direct message sending --- index.js | 11 +++++++++-- lib/utp-socket.js | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index d136bf2..ca3803a 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ const Socket = module.exports = class Socket extends EventEmitter { this._allowHalfOpen = !opts || opts.allowHalfOpen !== false this._socket.onconnection = this._onconnection.bind(this) + this._socket.onmessage = this._onmessage.bind(this) this._socket.onclose = this._onclose.bind(this) } @@ -92,7 +93,9 @@ const Socket = module.exports = class Socket extends EventEmitter { send (buf, offset, len, port, host, cb) { if (!this._socket.bound) this.bind() if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) - if (this._closing) return cb(new Error('Socket is closed')) + if (this._socket.closed || this._socket.closing) return cb(new Error('Socket is closed')) + + this._socket.send(buf, offset, len, port, host, cb) } bind (port, ip, onlistening) { @@ -101,7 +104,7 @@ const Socket = module.exports = class Socket extends EventEmitter { if (!port) port = 0 if (!ip) ip = '0.0.0.0' - if (this._socket.closing) return + if (this._socket.closed || this._socket.closing) return if (this._address) { this.emit('error', new Error('Socket already bound')) @@ -148,6 +151,10 @@ const Socket = module.exports = class Socket extends EventEmitter { this.emit('connection', new Connection(this, handle, port, ip, this._allowHalfOpen)) } + _onmessage (buffer, address) { + this.emit('message', buffer, address) + } + _onclose () { this.emit('close') } diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 096cfdd..4c514f3 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -82,13 +82,13 @@ class UTPSocket { } send (buffer, offset, len, port, ip, cb) { - if ((this._state & (CLOSED | CLOSING)) === 0) throw new Error('Socket is closed') + if ((this._state & (CLOSED | CLOSING)) !== 0) throw new Error('Socket is closed') this._ensureBound() const request = this._sent.pop() || new UTPSendRequest(this) - request.send(buffer, offset, len, port, ip, cb) + request.send(buffer, offset, len, port, ip, cb || noop) } close () { From eb6110436a10079b7ba7b5b58434e92d74ee9cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:36:55 +0100 Subject: [PATCH 51/81] Formatting --- lib/utp-connection.js | 4 ++-- lib/utp-send-request.js | 1 + lib/utp-socket.js | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 18dcb77..7c94cca 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -159,6 +159,8 @@ class UTPConnection { } } +module.exports = UTPConnection + const UTP_ERRORS = [ 'UTP_ECONNREFUSED', 'UTP_ECONNRESET', @@ -175,5 +177,3 @@ function createUTPError (code) { } function noop () {} - -module.exports = UTPConnection diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index 806cc61..c25447b 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -46,4 +46,5 @@ class UTPSendRequest { binding.utp_napi_send_request_init(this._handle, this) } } + module.exports = UTPSendRequest diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 4c514f3..bd4c869 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -58,13 +58,15 @@ class UTPSocket { } ref () { - if ((this._state & INITIALIZED) !== 0) binding.utp_napi_ref(this._handle) this._state &= ~UNREFED + + if ((this._state & INITIALIZED) !== 0) binding.utp_napi_ref(this._handle) } unref () { - if ((this._state & INITIALIZED) !== 0) binding.utp_napi_unref(this._handle) this._state |= UNREFED + + if ((this._state & INITIALIZED) !== 0) binding.utp_napi_unref(this._handle) } bind (port, ip) { From 1a078f2868c628d9f2e130b00dc8c434c99ab657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 10:39:06 +0100 Subject: [PATCH 52/81] Make `cb` optional --- lib/utp-send-request.js | 4 ++-- lib/utp-socket.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index c25447b..449d873 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -18,7 +18,7 @@ class UTPSendRequest { set.add(this._socket._sending, this) this._buffer = buffer - this._callback = cb + this._callback = cb || null binding.utp_napi_send(this._socket._handle, this._handle, buffer, @@ -39,7 +39,7 @@ class UTPSendRequest { this._buffer = null this._callback = null - cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + if (cb) cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) } _init () { diff --git a/lib/utp-socket.js b/lib/utp-socket.js index bd4c869..b5140e6 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -90,7 +90,7 @@ class UTPSocket { const request = this._sent.pop() || new UTPSendRequest(this) - request.send(buffer, offset, len, port, ip, cb || noop) + request.send(buffer, offset, len, port, ip, cb) } close () { From edfcd5e5766f29e3fb9ff01e8b4917388f0627b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 11:13:30 +0100 Subject: [PATCH 53/81] Adjust `UTPSendRequest` --- index.js | 16 +++++++++------- lib/utp-send-request.js | 31 ++++++++++++++++++++++++------- lib/utp-socket.js | 13 ++++--------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index ca3803a..b55d0be 100644 --- a/index.js +++ b/index.js @@ -90,12 +90,14 @@ const Socket = module.exports = class Socket extends EventEmitter { this.firewall(false) } - send (buf, offset, len, port, host, cb) { + send (buf, offset, len, port, host, onsent) { if (!this._socket.bound) this.bind() - if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, cb) - if (this._socket.closed || this._socket.closing) return cb(new Error('Socket is closed')) + if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, onsent) + if (this._socket.closed || this._socket.closing) throw new Error('Socket is closed') - this._socket.send(buf, offset, len, port, host, cb) + const request = this._socket.send(buf, offset, len, port, host) + + if (onsent) request.onsent = onsent } bind (port, ip, onlistening) { @@ -133,10 +135,10 @@ const Socket = module.exports = class Socket extends EventEmitter { this._socket.close() } - _resolveAndSend (buf, offset, len, port, host, cb) { + _resolveAndSend (buf, offset, len, port, host, onsent) { dns.lookup(host, { family: 4 }, (err, ip) => { - if (err) cb(err) - else this.send(buf, offset, len, port, ip, cb) + if (err) this.emit('error', err) + else this.send(buf, offset, len, port, ip, onsent) }) } diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index 449d873..063c94f 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -2,23 +2,31 @@ const set = require('unordered-set') const b4a = require('b4a') const binding = require('./binding') +const INITIALIZED = 1 +const SENDING = 1 << 1 + class UTPSendRequest { constructor (socket) { this._socket = socket this._index = null this._handle = b4a.alloc(binding.sizeof_utp_napi_send_request_t) + this._state = 0 this._buffer = null - this._callback = null + + this.onsent = noop this._init() } - send (buffer, offset, length, port, ip, cb) { + send (buffer, offset, length, port, ip) { + if ((this._state & SENDING) !== 0) throw new Error('Already sending') + + this._state |= SENDING + set.add(this._socket._sending, this) this._buffer = buffer - this._callback = cb || null binding.utp_napi_send(this._socket._handle, this._handle, buffer, @@ -30,21 +38,30 @@ class UTPSendRequest { } finish (status) { + if ((this._state & SENDING) === 0) throw new Error('Not sending') + + this._state &= ~SENDING + set.remove(this._socket._sending, this) this._socket._sent.push(this) - const cb = this._callback - this._buffer = null - this._callback = null - if (cb) cb(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) + if (this._socket.idle) this._socket._onidle() + + this.onsent(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) } _init () { + if ((this._state & INITIALIZED) !== 0) return + + this._state |= INITIALIZED + binding.utp_napi_send_request_init(this._handle, this) } } module.exports = UTPSendRequest + +function noop () {} diff --git a/lib/utp-socket.js b/lib/utp-socket.js index b5140e6..f20af1c 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -78,19 +78,16 @@ class UTPSocket { binding.utp_napi_bind(this._handle, port, ip) } - listen (port, ip) { - this.bind(port, ip) - this.firewall(false) - } - - send (buffer, offset, len, port, ip, cb) { + send (buffer, offset, len, port, ip) { if ((this._state & (CLOSED | CLOSING)) !== 0) throw new Error('Socket is closed') this._ensureBound() const request = this._sent.pop() || new UTPSendRequest(this) - request.send(buffer, offset, len, port, ip, cb) + request.send(buffer, offset, len, port, ip) + + return request } close () { @@ -144,8 +141,6 @@ class UTPSocket { _onsend (request, status) { request.finish(status) - - if (this.idle) this._onidle() } _onconnection (port, ip) { From 13b2c1a9dbbebb5b1e9fc8592eba4290a629fcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 11:21:17 +0100 Subject: [PATCH 54/81] Implement connection firewalling --- index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/index.js b/index.js index b55d0be..5d6744f 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const net = require('net') const dns = require('dns') const { Duplex } = require('streamx') const timeout = require('timeout-refresh') +const set = require('unordered-set') const b4a = require('b4a') const binding = require('./lib/binding') const UTPConnection = require('./lib/utp-connection') @@ -204,6 +205,15 @@ class Connection extends Duplex { this._connection.onend = this._onend.bind(this) this._connection.onconnect = this._onconnect.bind(this) this._connection.onclose = this._onclose.bind(this) + + set.add(this._socket.connections, this) + + if ( + this._socket.maxConnections > 0 && + this._socket.connections.length >= this._socket.maxConnections + ) { + this._socket.firewall(true) + } } setTimeout (ms, ontimeout) { @@ -250,6 +260,15 @@ class Connection extends Duplex { } _destroy (cb) { + set.remove(this._socket.connections, this) + + if ( + this._socket.maxConnections <= 0 || + this._socket.connections.length < this._socket.maxConnections + ) { + this._socket.firewall(false) + } + if (this._connection.closed) cb(null) else { this._destroying = cb From 1d7df8573cb34d389c7d0d425913b6168e6f2631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 11:27:30 +0100 Subject: [PATCH 55/81] Add `UTPSendRequest.toHandle()` --- lib/utp-send-request.js | 4 ++++ lib/utp-socket.js | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index 063c94f..f9ed80d 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -53,6 +53,10 @@ class UTPSendRequest { this.onsent(status < 0 ? new Error('Send failed (status: ' + status + ')') : null) } + static toHandle (request) { + return request._handle + } + _init () { if ((this._state & INITIALIZED) !== 0) return diff --git a/lib/utp-socket.js b/lib/utp-socket.js index f20af1c..162573d 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -156,7 +156,7 @@ class UTPSocket { this._state &= ~CLOSING this._state |= CLOSED - binding.utp_napi_destroy(this._handle, this._sent.map(toHandle)) + binding.utp_napi_destroy(this._handle, this._sent.map(UTPSendRequest.toHandle)) this.onclose() } @@ -174,8 +174,4 @@ class UTPSocket { module.exports = UTPSocket -function toHandle (obj) { - return obj._handle -} - function noop () {} From 1cf58c794d1ac95cb6684d26a598cf4f5f5775ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 11:45:08 +0100 Subject: [PATCH 56/81] Bug fixes --- index.js | 14 +++++++++----- lib/utp-socket.js | 5 +++-- test/sockets.js | 34 ++++++++++++++++---------------- test/timeouts.js | 34 ++++++++++++++++---------------- test/udp.js | 49 ++++++++++++++++++++++++----------------------- 5 files changed, 71 insertions(+), 65 deletions(-) diff --git a/index.js b/index.js index 5d6744f..554a15e 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const binding = require('./lib/binding') const UTPConnection = require('./lib/utp-connection') const UTPSocket = require('./lib/utp-socket') -const Socket = module.exports = class Socket extends EventEmitter { +class Socket extends EventEmitter { constructor (opts) { super() @@ -94,11 +94,14 @@ const Socket = module.exports = class Socket extends EventEmitter { send (buf, offset, len, port, host, onsent) { if (!this._socket.bound) this.bind() if (!net.isIPv4(host)) return this._resolveAndSend(buf, offset, len, port, host, onsent) - if (this._socket.closed || this._socket.closing) throw new Error('Socket is closed') - const request = this._socket.send(buf, offset, len, port, host) + try { + const request = this._socket.send(buf, offset, len, port, host) - if (onsent) request.onsent = onsent + if (onsent) request.onsent = onsent + } catch (err) { + if (onsent) onsent(err) + } } bind (port, ip, onlistening) { @@ -115,6 +118,7 @@ const Socket = module.exports = class Socket extends EventEmitter { } if (onlistening) this.once('listening', onlistening) + if (!net.isIPv4(ip)) return this._resolveAndBind(port, ip) this._address = ip @@ -180,7 +184,7 @@ const Socket = module.exports = class Socket extends EventEmitter { } } -Socket.Socket = Socket +module.exports = Socket.Socket = Socket class Connection extends Duplex { constructor (socket, connection, port, address, halfOpen) { diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 162573d..63375ad 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -73,9 +73,10 @@ class UTPSocket { if ((this._state & BOUND) !== 0) throw new Error('Already bound') if ((this._state & INITIALIZED) === 0) this._init() - this._state |= BOUND - + // This will throw if not successfully bound binding.utp_napi_bind(this._handle, port, ip) + + this._state |= BOUND } send (buffer, offset, len, port, ip) { diff --git a/test/sockets.js b/test/sockets.js index 174dcb1..4745b3a 100644 --- a/test/sockets.js +++ b/test/sockets.js @@ -2,7 +2,7 @@ const test = require('brittle') const dgram = require('dgram') const utp = require('../') -test('dgram-like socket', function (t) { +test('dgram-like socket', (t) => { t.plan(3) const socket = new utp.Socket() @@ -14,28 +14,28 @@ test('dgram-like socket', function (t) { socket.close() }) - socket.bind(function () { + socket.bind(() => { socket.send(Buffer.from('hello'), 0, 5, socket.address().port, '127.0.0.1') }) }) -test('double close', function (t) { +test('double close', (t) => { t.plan(1) const socket = new utp.Socket() - socket.on('close', function () { - socket.close(function () { + socket.on('close', () => { + socket.close(() => { t.pass('closed twice') }) }) - socket.bind(0, function () { + socket.bind(0, () => { socket.close() }) }) -test('echo socket', function (t) { +test('echo socket', (t) => { t.plan(3) const socket = new utp.Socket() @@ -44,8 +44,8 @@ test('echo socket', function (t) { socket.send(buf, 0, buf.length, rinfo.port, rinfo.address) }) - socket.bind(function () { - var other = dgram.createSocket('udp4') + socket.bind(() => { + const other = dgram.createSocket('udp4') other.on('message', function (buf, rinfo) { t.is(rinfo.port, socket.address().port) t.is(rinfo.address, '127.0.0.1') @@ -57,7 +57,7 @@ test('echo socket', function (t) { }) }) -test('echo socket with resolve', function (t) { +test('echo socket with resolve', (t) => { t.plan(3) const socket = new utp.Socket() @@ -66,7 +66,7 @@ test('echo socket with resolve', function (t) { socket.send(buf, 0, buf.length, rinfo.port, 'localhost') }) - socket.bind(function () { + socket.bind(() => { const other = dgram.createSocket('udp4') other.on('message', function (buf, rinfo) { t.is(rinfo.port, socket.address().port) @@ -79,7 +79,7 @@ test('echo socket with resolve', function (t) { }) }) -test('combine server and connection', function (t) { +test('combine server and connection', (t) => { t.plan(3) const socket = new utp.Socket() @@ -90,8 +90,8 @@ test('combine server and connection', function (t) { client.pipe(client) }) - socket.listen(function () { - var client = socket.connect(socket.address().port) + socket.listen(() => { + const client = socket.connect(socket.address().port) client.write('hi') client.on('data', function (data) { client.end() @@ -101,7 +101,7 @@ test('combine server and connection', function (t) { }) }) -test('both ends write first', async function (t) { +test.skip('both ends write first', async (t) => { const close = t.test('close') close.plan(2) @@ -115,8 +115,8 @@ test('both ends write first', async function (t) { }) }) - socket.listen(0, function () { - var connection = socket.connect(socket.address().port) + socket.listen(0, () => { + const connection = socket.connect(socket.address().port) connection.write('b') connection.on('data', function (data) { close.alike(data, Buffer.from('a')) diff --git a/test/timeouts.js b/test/timeouts.js index bc541fe..1c8b1c7 100644 --- a/test/timeouts.js +++ b/test/timeouts.js @@ -2,11 +2,11 @@ const test = require('brittle') const dgram = require('dgram') const utp = require('../') -test('connection timeout. this may take >20s', function (t) { +test('connection timeout. this may take >20s', (t) => { t.plan(1) const socket = dgram.createSocket('udp4') - socket.bind(0, function () { + socket.bind(0, () => { const connection = utp.connect(socket.address().port) connection.on('error', function (err) { socket.close() @@ -15,11 +15,11 @@ test('connection timeout. this may take >20s', function (t) { }) }) -test('write timeout. this may take >20s', function (t) { +test('write timeout. this may take >20s', (t) => { t.plan(3) const server = utp.createServer() - var connection + let connection server.on('connection', function (socket) { t.pass('server received connection') @@ -27,13 +27,13 @@ test('write timeout. this may take >20s', function (t) { socket.destroy() }) - server.on('close', function () { + server.on('close', () => { connection.write('hello?') }) - server.listen(function () { + server.listen(() => { connection = utp.connect(server.address().port) - connection.on('connect', function () { + connection.on('connect', () => { t.pass('connected to server') }) connection.on('error', function (err) { @@ -42,10 +42,10 @@ test('write timeout. this may take >20s', function (t) { }) }) -test('server max connections. this may take >20s', function (t) { +test('server max connections. this may take >20s', (t) => { t.plan(4) - var inc = 0 + let inc = 0 const server = utp.createServer({ allowHalfOpen: false }, function (socket) { inc++ t.ok(inc < 3) @@ -53,19 +53,19 @@ test('server max connections. this may take >20s', function (t) { }) server.maxConnections = 2 - server.listen(0, function () { - var a = utp.connect(server.address().port) + server.listen(0, () => { + const a = utp.connect(server.address().port) a.write('hi') - a.on('connect', function () { - var b = utp.connect(server.address().port) + a.on('connect', () => { + const b = utp.connect(server.address().port) b.write('hi') - b.on('connect', function () { - var c = utp.connect(server.address().port) + b.on('connect', () => { + const c = utp.connect(server.address().port) c.write('hi') - c.on('connect', function () { + c.on('connect', () => { t.fail('only 2 connections') }) - c.on('error', function () { + c.on('error', () => { a.destroy() b.destroy() c.destroy() diff --git a/test/udp.js b/test/udp.js index 0c2c4cc..02480a7 100644 --- a/test/udp.js +++ b/test/udp.js @@ -1,12 +1,12 @@ const test = require('brittle') const utp = require('../') -test('bind', function (t) { +test('bind', (t) => { t.plan(4) const sock = new utp.Socket() - sock.bind(function () { + sock.bind(() => { const { port, address } = sock.address() t.is(address, '0.0.0.0') t.is(typeof port, 'number') @@ -15,20 +15,20 @@ test('bind', function (t) { }) }) -test('bind, close, bind', function (t) { +test('bind, close, bind', (t) => { t.plan(6) const sock = new utp.Socket() - sock.bind(function () { + sock.bind(() => { const { port, address } = sock.address() t.is(address, '0.0.0.0') t.is(typeof port, 'number') t.ok(port > 0 && port < 65536) - sock.close(function () { + sock.close(() => { const otherSock = new utp.Socket() - otherSock.bind(port, function () { + otherSock.bind(port, () => { const addr = otherSock.address() t.is(addr.port, port) t.is(addr.address, address) @@ -38,55 +38,56 @@ test('bind, close, bind', function (t) { }) }) -test('bind after error', function (t) { +test('bind after error', (t) => { t.plan(3) const a = new utp.Socket() const b = new utp.Socket() - a.listen(function () { - b.once('error', function (err) { - t.ok(err, 'should error') - b.listen(function () { - t.pass('should still bind') - a.close(() => b.close(() => t.pass())) + a.listen(() => { + b + .once('error', (err) => { + t.ok(err, 'should error') + b.listen(() => { + t.pass('should still bind') + a.close(() => b.close(() => t.pass())) + }) }) - }) - b.listen(a.address().port) + .listen(a.address().port) }) }) -test('send message', function (t) { +test('send message', (t) => { t.plan(4) const sock = new utp.Socket() - sock.bind(0, '127.0.0.1', function () { + sock.bind(0, '127.0.0.1', () => { const addr = sock.address() - sock.on('message', function (message, rinfo) { + sock.on('message', (message, rinfo) => { t.alike(rinfo, addr) t.alike(message, Buffer.from('hello')) sock.close(() => t.pass()) }) - sock.send(Buffer.from('hello'), 0, 5, addr.port, addr.address, function (err) { + sock.send(Buffer.from('hello'), 0, 5, addr.port, addr.address, (err) => { t.absent(err, 'no error') }) }) }) -test('send after close', function (t) { +test('send after close', (t) => { t.plan(2) const sock = new utp.Socket() - sock.bind(0, '127.0.0.1', function () { + sock.bind(0, '127.0.0.1', () => { const { port, address } = sock.address() - sock.send(Buffer.from('hello'), 0, 5, port, address, function (err) { + sock.send(Buffer.from('hello'), 0, 5, port, address, (err) => { t.absent(err, 'no error') - sock.close(function () { - sock.send(Buffer.from('world'), 0, 5, port, address, function (err) { + sock.close(() => { + sock.send(Buffer.from('world'), 0, 5, port, address, (err) => { t.ok(err, 'should error') }) }) From 4d033fd11e040a16e878589512fe903bde1edc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:15:13 +0100 Subject: [PATCH 57/81] More fixes --- binding.cc | 8 ++++---- index.js | 22 +++++++++++----------- lib/utp-connection.js | 34 ++++++++++++++++++++++++++++++---- lib/utp-send-request.js | 8 ++++++++ lib/utp-socket.js | 6 +++++- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/binding.cc b/binding.cc index bd61d06..eb09354 100644 --- a/binding.cc +++ b/binding.cc @@ -641,12 +641,12 @@ NAPI_METHOD(utp_napi_connection_init) { return NULL; } -NAPI_METHOD(utp_napi_connection_on_close) { - // To trigger a manual teardown if connect was never called - // on a client connection +NAPI_METHOD(utp_napi_connection_destroy) { NAPI_ARGV(1) NAPI_ARGV_BUFFER_CAST(utp_napi_connection_t *, self, 0) + utp_napi_connection_destroy(self); + return NULL; } @@ -734,6 +734,6 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(utp_napi_connection_writev) NAPI_EXPORT_FUNCTION(utp_napi_connection_close) NAPI_EXPORT_FUNCTION(utp_napi_connection_shutdown) - NAPI_EXPORT_FUNCTION(utp_napi_connection_on_close) + NAPI_EXPORT_FUNCTION(utp_napi_connection_destroy) NAPI_EXPORT_FUNCTION(utp_napi_connect) } diff --git a/index.js b/index.js index 554a15e..ad8710c 100644 --- a/index.js +++ b/index.js @@ -30,7 +30,7 @@ class Socket extends EventEmitter { } address () { - if (!this._address || this._closing) throw new Error('Socket not bound') + if (!this._socket.bound || this._socket.closing) throw new Error('Socket not bound') return { address: this._address, family: 'IPv4', @@ -39,32 +39,32 @@ class Socket extends EventEmitter { } getRecvBufferSize () { - if (!this._inited) throw new Error('getRecvBufferSize EBADF') - if (this._closing) return 0 + if (!this._socket.inited) throw new Error('getRecvBufferSize EBADF') + if (this._socket.closing) return 0 return binding.utp_napi_recv_buffer(this._socket._handle, 0) } setRecvBufferSize (n) { - if (!this._inited) throw new Error('setRecvBufferSize EBADF') - if (this._closing) return 0 + if (!this._socket.inited) throw new Error('setRecvBufferSize EBADF') + if (this._socket.closing) return 0 return binding.utp_napi_recv_buffer(this._socket._handle, n) } getSendBufferSize () { - if (!this._inited) throw new Error('getSendBufferSize EBADF') - if (this._closing) return 0 + if (!this._socket.inited) throw new Error('getSendBufferSize EBADF') + if (this._socket.closing) return 0 return binding.utp_napi_send_buffer(this._socket._handle, 0) } setSendBufferSize (n) { - if (!this._inited) throw new Error('setSendBufferSize EBADF') - if (this._closing) return 0 + if (!this._socket.inited) throw new Error('setSendBufferSize EBADF') + if (this._socket.closing) return 0 return binding.utp_napi_send_buffer(this._socket._handle, n) } setTTL (ttl) { - if (!this._inited) throw new Error('setTTL EBADF') - if (this._closing) return + if (!this._socket.inited) throw new Error('setTTL EBADF') + if (this._socket.closing) return binding.utp_napi_set_ttl(this._socket._handle, ttl) } diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 7c94cca..c67e6d3 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -9,6 +9,7 @@ const CONNECTING = 1 << 1 const CONNECTED = 1 << 2 const CLOSING = 1 << 3 const CLOSED = 1 << 4 +const WRITABLE = 1 << 5 class UTPConnection { constructor (socket, handle) { @@ -29,16 +30,38 @@ class UTPConnection { set.add(this._socket._connections, this) + // Server connections start out connected, but are only writable after the + // first read if (handle) { this._init() this._state |= CONNECTED } } + get inited () { + return (this._state & INITIALIZED) !== 0 + } + + get connecting () { + return (this._state & CONNECTING) !== 0 + } + get connected () { return (this._state & CONNECTED) !== 0 } + get closing () { + return (this._state & CLOSING) !== 0 + } + + get closed () { + return (this._state & CLOSED) !== 0 + } + + get writable () { + return (this._state & WRITABLE) !== 0 + } + connect (port, ip) { if ((this._state & (CONNECTED | CONNECTING)) !== 0) throw new Error('Already connected') if ((this._state & INITIALIZED) === 0) this._init() @@ -51,7 +74,7 @@ class UTPConnection { } writev (batch, cb) { - if ((this._state & CONNECTED) === 0) return cb(new Error('Not connected')) + if ((this._state & WRITABLE) === 0) return cb(new Error('Not writable')) const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 @@ -64,7 +87,7 @@ class UTPConnection { shutdown () { if ((this._state & INITIALIZED) === 0) return - this._state &= ~(CONNECTED | CONNECTING) + this._state &= ~(CONNECTED | CONNECTING | WRITABLE) binding.utp_napi_connection_shutdown(this._handle) } @@ -76,6 +99,8 @@ class UTPConnection { if ((this._state & CLOSING) === 0) { this._state |= CLOSING + // The connection can only be closed if not in the process of connecting, + // otherwise destroy the connection immediately const canClose = (this._state & CONNECTING) === 0 this._ensureShutdown() @@ -83,7 +108,7 @@ class UTPConnection { if (canClose) { binding.utp_napi_connection_close(this._handle) } else { - binding.utp_napi_connection_on_close(this._handle) + binding.utp_napi_connection_destroy(this._handle) } } } @@ -105,7 +130,7 @@ class UTPConnection { } _onread (size) { - if ((this._state & CONNECTED) === 0) this._onconnect() + if ((this._state & WRITABLE) === 0) this._onconnect() const buffer = this._buffer.subarray(this._offset, this._offset += size) @@ -144,6 +169,7 @@ class UTPConnection { _onconnect () { this._state &= ~CONNECTING this._state |= CONNECTED + this._state |= WRITABLE this.onconnect() } diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index f9ed80d..5559053 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -19,6 +19,14 @@ class UTPSendRequest { this._init() } + get inited () { + return (this._state & INITIALIZED) !== 0 + } + + get sending () { + return (this._state & SENDING) !== 0 + } + send (buffer, offset, length, port, ip) { if ((this._state & SENDING) !== 0) throw new Error('Already sending') diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 63375ad..915b11f 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -33,6 +33,10 @@ class UTPSocket { this.onconnection = noop } + get inited () { + return (this._state & INITIALIZED) !== 0 + } + get bound () { return (this._state & BOUND) !== 0 } @@ -154,7 +158,7 @@ class UTPSocket { } _onclose () { - this._state &= ~CLOSING + this._state &= ~(BOUND | CLOSING) this._state |= CLOSED binding.utp_napi_destroy(this._handle, this._sent.map(UTPSendRequest.toHandle)) From 27aa2d3d329faf32e304355ec0c21b969b17e06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:17:04 +0100 Subject: [PATCH 58/81] Shorten things --- lib/utp-connection.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index c67e6d3..3e85f3f 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -168,8 +168,7 @@ class UTPConnection { _onconnect () { this._state &= ~CONNECTING - this._state |= CONNECTED - this._state |= WRITABLE + this._state |= (CONNECTED | WRITABLE) this.onconnect() } From cd437c37de0b345c7182ccfe2c9fec61a6e6b00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:19:42 +0100 Subject: [PATCH 59/81] Handle errors --- index.js | 10 ++++++++++ test/net.js | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index ad8710c..383a0f0 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ class Socket extends EventEmitter { this._socket = new UTPSocket() this._allowHalfOpen = !opts || opts.allowHalfOpen !== false + this._socket.onerror = this._onerror.bind(this) this._socket.onconnection = this._onconnection.bind(this) this._socket.onmessage = this._onmessage.bind(this) this._socket.onclose = this._onclose.bind(this) @@ -154,6 +155,10 @@ class Socket extends EventEmitter { }) } + _onerror (err) { + this.emit('error', err) + } + _onconnection (port, ip, handle) { this.emit('connection', new Connection(this, handle, port, ip, this._allowHalfOpen)) } @@ -205,6 +210,7 @@ class Connection extends Duplex { this._opening = null this._destroying = null + this._connection.onerror = this._onerror.bind(this) this._connection.ondata = this._ondata.bind(this) this._connection.onend = this._onend.bind(this) this._connection.onconnect = this._onconnect.bind(this) @@ -312,6 +318,10 @@ class Connection extends Duplex { this.emit('timeout') } + _onerror (err) { + this.emit('error', err) + } + _ondata (buffer) { if (this._timeout) this._timeout.refresh() diff --git a/test/net.js b/test/net.js index 4035f5f..0483afd 100644 --- a/test/net.js +++ b/test/net.js @@ -9,7 +9,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() hangs? + .end() // .destroy() causes UTP_ECONNRESET? }) server.listen(() => { @@ -34,7 +34,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() hangs? + .end() // .destroy() causes UTP_ECONNRESET? }) server.listen(() => { From ec82b6c5233cd6c701ec78f4a6e6c34003edf48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:26:57 +0100 Subject: [PATCH 60/81] No longer required --- test/net.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/net.js b/test/net.js index 0483afd..14072e0 100644 --- a/test/net.js +++ b/test/net.js @@ -20,7 +20,6 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) - .write('hello') // why required? }) await close @@ -45,7 +44,6 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) - .write('foo') // why required? }) await close From aeafa7a6fabbb27ff38311fa58e366b0c85dd917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:44:50 +0100 Subject: [PATCH 61/81] Fix remaining test cases --- test/net.js | 112 ++++++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/test/net.js b/test/net.js index 14072e0..3273f85 100644 --- a/test/net.js +++ b/test/net.js @@ -103,13 +103,13 @@ test('echo server back and fourth', (t) => withServer(t, async (server) => { let echoed = 0 server.on('connection', (socket) => { - socket.pipe(socket) socket .on('data', (data) => { echoed++ writes.alike(data, Buffer.from('hello')) }) .on('close', () => writes.pass('server socket closed')) + .pipe(socket) }) server.listen(() => { @@ -132,88 +132,100 @@ test('echo server back and fourth', (t) => withServer(t, async (server) => { t.is(echoed, 10) })) -test.skip('echo big message', (t) => withServer(t, async (server) => { +test('echo big message', (t) => withServer(t, async (server) => { const writes = t.test('write and close sockets') - writes.plan(2) + writes.plan(3) let packets = 0 - const big = Buffer.alloc(8 * 1024 * 1024) - big.fill('yolo') + const sending = Buffer.alloc(8 * 1024 * 1024) + sending.fill('yolo') server.on('connection', (socket) => { - socket.on('data', () => packets++) - socket.pipe(socket) + socket + .on('data', () => packets++) + .on('close', () => writes.pass('server socket closed')) + .pipe(socket) }) server.listen(() => { const then = Date.now() const socket = utp.connect(server.address().port) - const buffer = Buffer.alloc(big.length) + const buffer = Buffer.alloc(sending.length) - let ptr = 0 + let total = 0 - socket.write(big) - socket.on('data', (data) => { - packets++ - data.copy(buffer, ptr) - ptr += data.length - if (big.length === ptr) { - socket.end() - t.alike(buffer, big) - t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') - } - }) + socket + .on('data', (data) => { + packets++ + data.copy(buffer, total) + total += data.length + + if (sending.length === total) { + writes.alike(buffer, sending) + writes.comment('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') + socket.end() + } + }) + .on('close', () => writes.pass('client socket closed')) + .write(sending) }) await writes })) -test.skip('echo big message with setContentSize', (t) => withServer(t, async (server) => { +test('echo big message with setContentSize', (t) => withServer(t, async (server) => { const writes = t.test('write and close sockets') - writes.plan(2) + writes.plan(3) let packets = 0 - const big = Buffer.alloc(8 * 1024 * 1024) - big.fill('yolo') + const sending = Buffer.alloc(8 * 1024 * 1024) + sending.fill('yolo') server.on('connection', (socket) => { - socket.setContentSize(big.length) - socket.on('data', () => packets++) - socket.pipe(socket) + socket.setContentSize(sending.length) + socket + .on('data', () => packets++) + .on('close', () => writes.pass('server socket closed')) + .pipe(socket) }) server.listen(() => { const then = Date.now() const socket = utp.connect(server.address().port) - const buffer = Buffer.alloc(big.length) + const buffer = Buffer.alloc(sending.length) - let ptr = 0 + let total = 0 - socket.setContentSize(big.length) - socket.write(big) - socket.on('data', (data) => { - packets++ - data.copy(buffer, ptr) - ptr += data.length - if (big.length === ptr) { - socket.end() - t.alike(buffer, big) - t.pass('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') - } - }) + socket.setContentSize(sending.length) + socket + .on('data', (data) => { + packets++ + data.copy(buffer, total) + total += data.length + + if (sending.length === total) { + writes.alike(buffer, sending) + writes.comment('echo took ' + (Date.now() - then) + 'ms (' + packets + ' packets)') + socket.end() + } + }) + .on('close', () => writes.pass('client socket closed')) + .write(sending) }) await writes })) -test.skip('two connections', async (t) => { +test('two connections', (t) => withServer(t, async (server) => { const writes = t.test('write and close sockets') - writes.plan(4) + writes.plan(6) - const server = utp.createServer((socket) => { - socket.pipe(socket) + server.on('connection', (socket) => { + socket + .on('close', () => writes.pass('server socket closed')) + .pipe(socket) }) server.listen(() => { @@ -232,7 +244,7 @@ test.skip('two connections', async (t) => { }) await writes -}) +})) test('flushes', (t) => withServer(t, async (server) => { const writes = t.test('writes') @@ -262,7 +274,7 @@ test('flushes', (t) => withServer(t, async (server) => { await writes })) -test.skip('close waits for connections to close', (t) => withServer(t, async (server) => { +test('close waits for connections to close', (t) => withServer(t, async (server) => { const close = t.test('close') close.plan(2) @@ -270,12 +282,10 @@ test.skip('close waits for connections to close', (t) => withServer(t, async (se server.on('connection', (socket) => { const recv = [] socket - .on('data', (data) => { - recv.push(data) - }) + .on('data', (data) => recv.push(data)) .on('end', () => { socket.end() - t.alike(Buffer.concat(recv), Buffer.concat(sent)) + close.alike(Buffer.concat(recv), Buffer.concat(sent)) }) server.close(() => close.pass('server closed')) }) From 4757c277a32aedb6a6d6d1a50fe89ab6786e84d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 12:58:57 +0100 Subject: [PATCH 62/81] Destroy connection on error --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 383a0f0..40752d1 100644 --- a/index.js +++ b/index.js @@ -319,7 +319,7 @@ class Connection extends Duplex { } _onerror (err) { - this.emit('error', err) + this.destroy(err) } _ondata (buffer) { From a797b35e3eec332f4d7960860c6246f448cea82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 13:28:49 +0100 Subject: [PATCH 63/81] Error is fine --- test/net.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/net.js b/test/net.js index 3273f85..b6eb676 100644 --- a/test/net.js +++ b/test/net.js @@ -9,7 +9,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes UTP_ECONNRESET? + .end() }) server.listen(() => { @@ -33,7 +33,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) - .end() // .destroy() causes UTP_ECONNRESET? + .end() }) server.listen(() => { From 5279e0412e3b87221ff7bbddb0d0d3f23fa5bcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 13:28:56 +0100 Subject: [PATCH 64/81] Adjust error messages --- lib/utp-connection.js | 4 ++-- lib/utp-send-request.js | 4 ++-- lib/utp-socket.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 3e85f3f..6d72da0 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -63,7 +63,7 @@ class UTPConnection { } connect (port, ip) { - if ((this._state & (CONNECTED | CONNECTING)) !== 0) throw new Error('Already connected') + if ((this._state & (CONNECTED | CONNECTING)) !== 0) throw new Error('Connection is already connected') if ((this._state & INITIALIZED) === 0) this._init() this._socket._ensureBound() @@ -74,7 +74,7 @@ class UTPConnection { } writev (batch, cb) { - if ((this._state & WRITABLE) === 0) return cb(new Error('Not writable')) + if ((this._state & WRITABLE) === 0) return cb(new Error('Connection is not writable')) const drained = binding.utp_napi_connection_writev(this._handle, batch) === 1 diff --git a/lib/utp-send-request.js b/lib/utp-send-request.js index 5559053..ebce89a 100644 --- a/lib/utp-send-request.js +++ b/lib/utp-send-request.js @@ -28,7 +28,7 @@ class UTPSendRequest { } send (buffer, offset, length, port, ip) { - if ((this._state & SENDING) !== 0) throw new Error('Already sending') + if ((this._state & SENDING) !== 0) throw new Error('Request is already sending') this._state |= SENDING @@ -46,7 +46,7 @@ class UTPSendRequest { } finish (status) { - if ((this._state & SENDING) === 0) throw new Error('Not sending') + if ((this._state & SENDING) === 0) throw new Error('Request is not sending') this._state &= ~SENDING diff --git a/lib/utp-socket.js b/lib/utp-socket.js index 915b11f..8c718cd 100644 --- a/lib/utp-socket.js +++ b/lib/utp-socket.js @@ -74,7 +74,7 @@ class UTPSocket { } bind (port, ip) { - if ((this._state & BOUND) !== 0) throw new Error('Already bound') + if ((this._state & BOUND) !== 0) throw new Error('Socket is already bound') if ((this._state & INITIALIZED) === 0) this._init() // This will throw if not successfully bound From d18eaaadc58a3ff3430340b84c24bb5852863902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 13:49:40 +0100 Subject: [PATCH 65/81] Fix half-open logic --- index.js | 9 ++------- test/net.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 40752d1..3c70580 100644 --- a/index.js +++ b/index.js @@ -205,7 +205,6 @@ class Connection extends Duplex { this._view = new Uint32Array(this._connection._handle.buffer, this._connection._handle.byteOffset, 2) this._timeout = null this._contentSize = 0 - this._allowOpen = halfOpen ? 2 : 1 this._opening = null this._destroying = null @@ -224,6 +223,8 @@ class Connection extends Duplex { ) { this._socket.firewall(true) } + + if (!halfOpen) this.on('end', () => this.end()) } setTimeout (ms, ontimeout) { @@ -286,13 +287,8 @@ class Connection extends Duplex { } } - _destroyMaybe () { - if (this._allowOpen && !--this._allowOpen) this.destroy() - } - _final (cb) { this._connection.shutdown() - this._destroyMaybe() cb(null) } @@ -339,7 +335,6 @@ class Connection extends Duplex { _onend () { if (this._timeout) this._timeout.destroy() this.push(null) - this._destroyMaybe() } _onconnect () { diff --git a/test/net.js b/test/net.js index b6eb676..b44c290 100644 --- a/test/net.js +++ b/test/net.js @@ -49,6 +49,34 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { await close })) +test('emits end and close', (t) => withServer(t, async (server) => { + const end = t.test('end') + end.plan(4) + + server.on('connection', (socket) => { + socket + .on('end', () => { + end.pass('server socket ended') + socket.end() + }) + .on('close', () => { + end.pass('server socket closed') + }) + .resume() + }) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('end', () => end.pass('client socket ended')) + .on('connect', () => socket.end()) + .on('close', () => end.pass('client socket closed')) + .resume() + }) + + await end +})) + test('bad resolve', (t) => { t.plan(2) @@ -334,7 +362,7 @@ test('timeout', (t) => withServer(t, async (server) => { socket .on('close', () => close.pass('server closed')) .setTimeout(100, () => - socket.end() // .destroy() causes ECONNRESET + socket.destroy() // .end hangs? ) }) From d5e0305bff5592e484d9d2ace3455f20db2c5089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 13:57:02 +0100 Subject: [PATCH 66/81] Adjust tests --- test/net.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/net.js b/test/net.js index b44c290..15c27f3 100644 --- a/test/net.js +++ b/test/net.js @@ -62,7 +62,6 @@ test('emits end and close', (t) => withServer(t, async (server) => { .on('close', () => { end.pass('server socket closed') }) - .resume() }) server.listen(() => { @@ -71,7 +70,6 @@ test('emits end and close', (t) => withServer(t, async (server) => { .on('end', () => end.pass('client socket ended')) .on('connect', () => socket.end()) .on('close', () => end.pass('client socket closed')) - .resume() }) await end @@ -369,7 +367,7 @@ test('timeout', (t) => withServer(t, async (server) => { server.listen(() => { const socket = utp.connect(server.address().port) socket - .on('end', () => socket.end()) // no .end() hangs? + .on('end', () => socket.end()) .on('close', () => close.pass('client closed')) .write('hello') // why required? }) From a488bba5c574955450fed3fc2a643c1f69a2bcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 20:14:36 +0100 Subject: [PATCH 67/81] Fix another segfault --- lib/utp-connection.js | 14 +++++-------- test/net.js | 48 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 6d72da0..eee101a 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -9,7 +9,8 @@ const CONNECTING = 1 << 1 const CONNECTED = 1 << 2 const CLOSING = 1 << 3 const CLOSED = 1 << 4 -const WRITABLE = 1 << 5 +const CLOSABLE = 1 << 5 +const WRITABLE = 1 << 6 class UTPConnection { constructor (socket, handle) { @@ -34,7 +35,7 @@ class UTPConnection { // first read if (handle) { this._init() - this._state |= CONNECTED + this._state |= (CONNECTED | CLOSABLE) } } @@ -68,7 +69,7 @@ class UTPConnection { this._socket._ensureBound() - this._state |= CONNECTING + this._state |= (CONNECTING | CLOSABLE) binding.utp_napi_connect(this._socket._handle, this._handle, port, ip) } @@ -98,14 +99,9 @@ class UTPConnection { if ((this._state & CLOSING) === 0) { this._state |= CLOSING - - // The connection can only be closed if not in the process of connecting, - // otherwise destroy the connection immediately - const canClose = (this._state & CONNECTING) === 0 - this._ensureShutdown() - if (canClose) { + if ((this._state & CLOSABLE) !== 0) { binding.utp_napi_connection_close(this._handle) } else { binding.utp_napi_connection_destroy(this._handle) diff --git a/test/net.js b/test/net.js index 15c27f3..814d042 100644 --- a/test/net.js +++ b/test/net.js @@ -16,7 +16,7 @@ test('server + connect', (t) => withServer(t, async (server) => { const socket = utp.connect(server.address().port) socket .on('connect', () => { - socket.end() // .destroy hangs? + socket.end() // .destroy() hangs? close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) @@ -67,14 +67,54 @@ test('emits end and close', (t) => withServer(t, async (server) => { server.listen(() => { const socket = utp.connect(server.address().port) socket - .on('end', () => end.pass('client socket ended')) .on('connect', () => socket.end()) + .on('end', () => end.pass('client socket ended')) .on('close', () => end.pass('client socket closed')) }) await end })) +test.skip('client immediately destroys', (t) => withServer(t, async (server) => { + const close = t.test('close sockets') + close.plan(2) + + server.on('connection', (socket) => { + socket.on('close', () => close.pass('server socket closed')) + }) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('close', () => close.pass('client socket closed')) + .write('foo') + socket.destroy() + }) + + await close +})) + +test('server immediately destroys', (t) => withServer(t, async (server) => { + const close = t.test('close sockets') + close.plan(2) + + server.on('connection', (socket) => { + socket + .on('close', () => close.pass('server socket closed')) + .destroy() + }) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('close', () => close.pass('client socket closed')) + .on('error', () => { /* UTP_ECONNRESET */ }) + .write('foo') + }) + + await close +})) + test('bad resolve', (t) => { t.plan(2) @@ -359,9 +399,7 @@ test('timeout', (t) => withServer(t, async (server) => { server.on('connection', (socket) => { socket .on('close', () => close.pass('server closed')) - .setTimeout(100, () => - socket.destroy() // .end hangs? - ) + .setTimeout(100, () => socket.destroy()) }) server.listen(() => { From 8a8e7e21020d1cc2cec4a0bb956dfed05f4457ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 16 Nov 2021 20:43:44 +0100 Subject: [PATCH 68/81] Add more test cases --- test/net.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/net.js b/test/net.js index 814d042..ef4d287 100644 --- a/test/net.js +++ b/test/net.js @@ -115,6 +115,53 @@ test('server immediately destroys', (t) => withServer(t, async (server) => { await close })) +test('client sends first', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(3) + + server.on('connection', (socket) => { + socket + .on('data', (data) => { + writes.alike(data, Buffer.from('foo')) + socket.end() + }) + .on('close', () => writes.pass('server socket closed')) + }) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('connect', () => { + socket.end('foo') + }) + .on('close', () => writes.pass('client socket closed')) + }) + + await writes +})) + +test('server sends first', (t) => withServer(t, async (server) => { + const writes = t.test('write and close sockets') + writes.plan(3) + + server.on('connection', (socket) => { + socket + .on('close', () => writes.pass('server socket closed')) + .on('error', (err) => writes.ok(err)) + .end('foo') + }) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('close', () => writes.pass('client socket closed')) + .on('error', () => { /* UTP_ECONNRESET */ }) + .end() + }) + + await writes +})) + test('bad resolve', (t) => { t.plan(2) From abae7b6095dd872a8932b96a2f5fee37d1c791af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 09:12:12 +0100 Subject: [PATCH 69/81] Only ever shutdown once --- lib/utp-connection.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index eee101a..897813b 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -11,6 +11,7 @@ const CLOSING = 1 << 3 const CLOSED = 1 << 4 const CLOSABLE = 1 << 5 const WRITABLE = 1 << 6 +const SHUTDOWN = 1 << 7 class UTPConnection { constructor (socket, handle) { @@ -87,8 +88,10 @@ class UTPConnection { shutdown () { if ((this._state & INITIALIZED) === 0) return + if ((this._state & SHUTDOWN) !== 0) return this._state &= ~(CONNECTED | CONNECTING | WRITABLE) + this._state |= SHUTDOWN binding.utp_napi_connection_shutdown(this._handle) } @@ -126,7 +129,7 @@ class UTPConnection { } _onread (size) { - if ((this._state & WRITABLE) === 0) this._onconnect() + if ((this._state & (WRITABLE | SHUTDOWN)) === 0) this._onconnect() const buffer = this._buffer.subarray(this._offset, this._offset += size) From 99bcd1f51bcdce9714887d24148aba9fed799ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 09:12:52 +0100 Subject: [PATCH 70/81] Don't open connection until writable --- index.js | 2 +- test/net.js | 28 ++++++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 3c70580..8620fc7 100644 --- a/index.js +++ b/index.js @@ -253,7 +253,7 @@ class Connection extends Duplex { } _open (cb) { - if (this._connection.connected) cb(null) + if (this._connection.writable) cb(null) else this._opening = cb } diff --git a/test/net.js b/test/net.js index ef4d287..7ab3081 100644 --- a/test/net.js +++ b/test/net.js @@ -9,6 +9,7 @@ test('server + connect', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) + .resume() .end() }) @@ -16,10 +17,11 @@ test('server + connect', (t) => withServer(t, async (server) => { const socket = utp.connect(server.address().port) socket .on('connect', () => { - socket.end() // .destroy() hangs? + socket.end() close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) + .write('foo') }) await close @@ -33,6 +35,7 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { close.pass('server socket connected') socket .on('close', () => close.pass('server socket closed')) + .resume() .end() }) @@ -40,10 +43,11 @@ test('server + connect with resolve', (t) => withServer(t, async (server) => { const socket = utp.connect(server.address().port, 'localhost') socket .on('connect', () => { - socket.end() // .destroy() hangs? + socket.end() close.pass('client socket connected') }) .on('close', () => close.pass('client socket closed')) + .write('foo') }) await close @@ -62,6 +66,7 @@ test('emits end and close', (t) => withServer(t, async (server) => { .on('close', () => { end.pass('server socket closed') }) + .resume() }) server.listen(() => { @@ -70,6 +75,7 @@ test('emits end and close', (t) => withServer(t, async (server) => { .on('connect', () => socket.end()) .on('end', () => end.pass('client socket ended')) .on('close', () => end.pass('client socket closed')) + .write('foo') }) await end @@ -142,21 +148,31 @@ test('client sends first', (t) => withServer(t, async (server) => { test('server sends first', (t) => withServer(t, async (server) => { const writes = t.test('write and close sockets') - writes.plan(3) + writes.plan(4) server.on('connection', (socket) => { socket .on('close', () => writes.pass('server socket closed')) - .on('error', (err) => writes.ok(err)) + .resume() .end('foo') }) server.listen(() => { const socket = utp.connect(server.address().port) + + // Due to https://github.com/bittorrent/libutp/issues/118, the server socket + // won't open before the client writes. + socket.setTimeout(100, () => { + writes.pass('client socket timed out') + socket.write('foo') + }) + socket + .on('data', (data) => { + writes.alike(data, Buffer.from('foo')) + socket.end() + }) .on('close', () => writes.pass('client socket closed')) - .on('error', () => { /* UTP_ECONNRESET */ }) - .end() }) await writes From 56d80774496c48406e9713f8cc92074b6cf5e9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 09:49:09 +0100 Subject: [PATCH 71/81] Ensure shutdown if `CLOSABLE` --- lib/utp-connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 897813b..1b3bd41 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -179,7 +179,7 @@ class UTPConnection { } _ensureShutdown () { - if ((this._state & CONNECTED) !== 0) this.shutdown() + if ((this._state & CLOSABLE) !== 0) this.shutdown() } } From d846831811eedc47a8400457bc9b5c2620e59cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 09:59:17 +0100 Subject: [PATCH 72/81] Additional test cases --- test/net.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/net.js b/test/net.js index 7ab3081..7158b12 100644 --- a/test/net.js +++ b/test/net.js @@ -83,18 +83,37 @@ test('emits end and close', (t) => withServer(t, async (server) => { test.skip('client immediately destroys', (t) => withServer(t, async (server) => { const close = t.test('close sockets') - close.plan(2) + close.plan(1) + + server.on('connection', () => close.fail('server socket connected')) + + server.listen(() => { + const socket = utp.connect(server.address().port) + socket + .on('close', () => close.pass('client socket closed')) + .destroy() + }) + + await close +})) + +test.skip('client destroys on connect', (t) => withServer(t, async (server) => { + const close = t.test('close sockets') + close.plan(3) server.on('connection', (socket) => { - socket.on('close', () => close.pass('server socket closed')) + close.pass('server socket connected') + socket + .on('close', () => close.pass('server socket closed')) + .resume() }) server.listen(() => { const socket = utp.connect(server.address().port) socket + .on('connect', () => socket.destroy()) .on('close', () => close.pass('client socket closed')) .write('foo') - socket.destroy() }) await close From 28e6ed0ab8929f8e538726df3951d66e1a785db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 10:36:34 +0100 Subject: [PATCH 73/81] Don't connect before `_open` --- index.js | 36 ++++++++++++++++++++---------------- test/net.js | 2 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 8620fc7..1785b05 100644 --- a/index.js +++ b/index.js @@ -73,18 +73,13 @@ class Socket extends EventEmitter { if (!this._socket.bound) this.bind() if (!ip) ip = '127.0.0.1' - const connection = new Connection( + return new Connection( this, null, port, ip, this._allowHalfOpen ) - - if (!net.isIPv4(ip)) connection._resolveAndConnect(port, ip) - else connection._connect(port, ip) - - return connection } listen (port, ip, onlistening) { @@ -225,6 +220,9 @@ class Connection extends Duplex { } if (!halfOpen) this.on('end', () => this.end()) + + // https://github.com/streamxorg/streamx/pull/49 + this.resume().pause() } setTimeout (ms, ontimeout) { @@ -253,8 +251,14 @@ class Connection extends Duplex { } _open (cb) { - if (this._connection.writable) cb(null) - else this._opening = cb + if (this._connection.connected) { + if (this._connection.writable) return cb(null) + } else { + if (net.isIPv4(this.remoteAddress)) this._connect() + else this._resolveAndConnect() + } + + this._opening = cb } _continueOpen (err) { @@ -296,17 +300,17 @@ class Connection extends Duplex { this._connection.writev(batch.length > 256 ? [b4a.concat(batch)] : batch, cb) } - _connect (port, ip) { - this.remotePort = port - this.remoteAddress = ip - - this._connection.connect(port, ip) + _connect () { + this._connection.connect(this.remotePort, this.remoteAddress) } - _resolveAndConnect (port, host) { - dns.lookup(host, { family: 4 }, (err, ip) => { + _resolveAndConnect () { + dns.lookup(this.remoteAddress, { family: 4 }, (err, ip) => { if (err) this.destroy(err) - else this._connect(port, ip) + else { + this.remoteAddress = ip + this._connect() + } }) } diff --git a/test/net.js b/test/net.js index 7158b12..73826b5 100644 --- a/test/net.js +++ b/test/net.js @@ -81,7 +81,7 @@ test('emits end and close', (t) => withServer(t, async (server) => { await end })) -test.skip('client immediately destroys', (t) => withServer(t, async (server) => { +test('client immediately destroys', (t) => withServer(t, async (server) => { const close = t.test('close sockets') close.plan(1) From 2ac1e2f4f9933878a5cc4ec793c9bd3b1bec543d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 10:57:48 +0100 Subject: [PATCH 74/81] Update `streamx` --- index.js | 5 +---- package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 1785b05..7650771 100644 --- a/index.js +++ b/index.js @@ -188,7 +188,7 @@ module.exports = Socket.Socket = Socket class Connection extends Duplex { constructor (socket, connection, port, address, halfOpen) { - super({ mapWritable: toBuffer }) + super({ mapWritable: toBuffer, eagerOpen: true }) this.remoteAddress = address this.remoteFamily = 'IPv4' @@ -220,9 +220,6 @@ class Connection extends Duplex { } if (!halfOpen) this.on('end', () => this.end()) - - // https://github.com/streamxorg/streamx/pull/49 - this.resume().pause() } setTimeout (ms, ontimeout) { diff --git a/package.json b/package.json index 1dbfde5..9cae620 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "napi-macros": "^2.0.0", "node-gyp-build": "^4.2.0", "queue-tick": "^1.0.0", - "streamx": "^2.11.3", + "streamx": "^2.12.0", "timeout-refresh": "^1.0.0", "unordered-set": "^2.0.1" }, From 6a019f11bf09f671d7abbee313de5288c2158fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 11:57:14 +0100 Subject: [PATCH 75/81] Shutdown always happens before close --- lib/utp-connection.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/utp-connection.js b/lib/utp-connection.js index 1b3bd41..e3d9d48 100644 --- a/lib/utp-connection.js +++ b/lib/utp-connection.js @@ -102,7 +102,6 @@ class UTPConnection { if ((this._state & CLOSING) === 0) { this._state |= CLOSING - this._ensureShutdown() if ((this._state & CLOSABLE) !== 0) { binding.utp_napi_connection_close(this._handle) @@ -177,10 +176,6 @@ class UTPConnection { this._offset = 0 return this._buffer } - - _ensureShutdown () { - if ((this._state & CLOSABLE) !== 0) this.shutdown() - } } module.exports = UTPConnection From 8c1acc7523ba92349477ba06b316a3b58eb773bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 13:54:20 +0100 Subject: [PATCH 76/81] Feedback from @davidmarkclements --- test/net.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/net.js b/test/net.js index 73826b5..c0abb5c 100644 --- a/test/net.js +++ b/test/net.js @@ -97,13 +97,19 @@ test('client immediately destroys', (t) => withServer(t, async (server) => { await close })) +// The socket socket for some reason never emits the `close` event. The test is +// therefore skipped for now. test.skip('client destroys on connect', (t) => withServer(t, async (server) => { const close = t.test('close sockets') - close.plan(3) + close.plan(4) server.on('connection', (socket) => { close.pass('server socket connected') socket + .on('end', () => { + close.pass('server socket ended') + socket.destroy() + }) .on('close', () => close.pass('server socket closed')) .resume() }) From e31e53f3a5af52462b869ae9a40f018a4d4afd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 13:58:33 +0100 Subject: [PATCH 77/81] Don't skip that one --- test/sockets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sockets.js b/test/sockets.js index 4745b3a..81783a8 100644 --- a/test/sockets.js +++ b/test/sockets.js @@ -101,7 +101,7 @@ test('combine server and connection', (t) => { }) }) -test.skip('both ends write first', async (t) => { +test('both ends write first', async (t) => { const close = t.test('close') close.plan(2) From dd24315f56263fbe29e551c087c7362c7b08beb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 17 Nov 2021 15:33:56 +0100 Subject: [PATCH 78/81] Simplify connection callback handling --- binding.cc | 9 +++------ test/net.js | 29 ++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/binding.cc b/binding.cc index eb09354..6c80182 100644 --- a/binding.cc +++ b/binding.cc @@ -314,12 +314,9 @@ on_utp_accept (utp_callback_arguments *a) { napi_value argv[2]; napi_create_uint32(env, port, &(argv[0])); napi_create_string_utf8(env, ip, NAPI_AUTO_LENGTH, &(argv[1])); - napi_value next; - if (napi_make_callback(env, NULL, ctx, callback, 2, argv, &next) == napi_pending_exception) { - napi_value fatal_exception; - napi_get_and_clear_last_exception(env, &fatal_exception); - napi_fatal_exception(env, fatal_exception); - } else { + napi_value next = NULL; + NAPI_MAKE_CALLBACK(env, NULL, ctx, callback, 2, argv, &next) + if (next != NULL) { utp_napi_connection_t *connection; size_t connection_size; napi_get_buffer_info(env, next, (void **) &connection, &connection_size); diff --git a/test/net.js b/test/net.js index c0abb5c..42e319f 100644 --- a/test/net.js +++ b/test/net.js @@ -501,23 +501,34 @@ test('timeout', (t) => withServer(t, async (server) => { await close })) -test.skip('exception in connection listener', async (t) => { - t.plan(1) +test.skip('exception in connection listener', (t) => withServer(t, async (server) => { + const close = t.test('close sockets') + close.plan(3) + + const err = new Error() - const server = utp.createServer((socket) => { - socket.destroy() - throw new Error('disconnect') + server.on('connection', (socket) => { + socket + .on('close', () => close.pass('server socket closed')) + .resume() + .end() + throw err }) - process.once('uncaughtException', () => { + process.once('uncaughtException', (thrown) => { + close.is(thrown, err) server.close() - t.pass() }) server.listen(() => { - utp.connect(server.address().port).destroy() + const socket = utp.connect(server.address().port) + socket + .on('close', () => close.pass('client socket closed')) + .end('foo') }) -}) + + await close +})) async function withServer (t, cb) { const server = utp.createServer() From 927e7052019d576e07892b265a64100ba57137fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 19 Nov 2021 11:50:52 +0100 Subject: [PATCH 79/81] Update Brittle --- package.json | 2 +- test/net.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9cae620..24fadc7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "unordered-set": "^2.0.1" }, "devDependencies": { - "brittle": "^1.6.0", + "brittle": "^1.6.1", "prebuildify": "^4.1.2", "standard": "^14.3.1" }, diff --git a/test/net.js b/test/net.js index 42e319f..3d25e96 100644 --- a/test/net.js +++ b/test/net.js @@ -501,7 +501,7 @@ test('timeout', (t) => withServer(t, async (server) => { await close })) -test.skip('exception in connection listener', (t) => withServer(t, async (server) => { +test('exception in connection listener', (t) => withServer(t, async (server) => { const close = t.test('close sockets') close.plan(3) From 16798dd2f11f9c6268da838b0b8e2bb3fb62fbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 9 May 2022 11:51:54 +0200 Subject: [PATCH 80/81] Update dependencies --- package.json | 4 ++-- ucat.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 24fadc7..4933efd 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "unordered-set": "^2.0.1" }, "devDependencies": { - "brittle": "^1.6.1", + "brittle": "^2.3.1", "prebuildify": "^4.1.2", - "standard": "^14.3.1" + "standard": "^17.0.0" }, "repository": { "type": "git", diff --git a/ucat.js b/ucat.js index 5e343ad..c3403d3 100755 --- a/ucat.js +++ b/ucat.js @@ -1,10 +1,10 @@ #!/usr/bin/env node const socket = require('./')({ allowHalfOpen: false }) -var host = null -var port = 0 +let host = null +let port = 0 -for (var i = 2; i < process.argv.length; i++) { +for (let i = 2; i < process.argv.length; i++) { if (process.argv[i][0] !== '-') { const parts = process.argv[i].split(':') port = Number(parts.pop()) || 0 From e55ab7b02dc04829328864631967c65c092dd466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 9 May 2022 11:53:22 +0200 Subject: [PATCH 81/81] Add test workflow --- .github/workflows/test-node.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/test-node.yml diff --git a/.github/workflows/test-node.yml b/.github/workflows/test-node.yml new file mode 100644 index 0000000..a93c431 --- /dev/null +++ b/.github/workflows/test-node.yml @@ -0,0 +1,23 @@ +name: Build Status +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + build: + strategy: + matrix: + node-version: [lts/*] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test