Skip to content

Commit

Permalink
feat: add support for the TCP_USER_TIMEOUT option
Browse files Browse the repository at this point in the history
  • Loading branch information
mildsunrise authored and hertzg committed Nov 7, 2020
1 parent 4766b02 commit 51462f7
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The Missing (`TCP_KEEPINTVL` and `TCP_KEEPCNT`) `SO_KEEPALIVE` socket option set

Tested on 🐧 `linux` & 🍏 `osx` (both `amd64` and `arm64`), should work on 😈 `freebsd` and others. Does not work on 🐄 `win32` (pull requests welcome).

There's also linux support for setting the `TCP_USER_TIMEOUT` option, which is closely related to keep-alive.

## Install

```bash
Expand Down
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ export function setKeepAliveProbes(
export function getKeepAliveProbes(
socket: NodeJSSocketWithFileDescriptor
): number

export function setUserTimeout(
socket: NodeJSSocketWithFileDescriptor,
timeout: number
): boolean

export function getUserTimeout(
socket: NodeJSSocketWithFileDescriptor
): number
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { expectType } from 'tsd'
Net.createServer((incomingSocket) => {
expectType<boolean>(NetKeepAlive.setKeepAliveInterval(incomingSocket, 1000))
expectType<boolean>(NetKeepAlive.setKeepAliveProbes(incomingSocket, 1))
expectType<boolean>(NetKeepAlive.setUserTimeout(incomingSocket, 5000))
})

const clientSocket = Net.createConnection({port: -1})
expectType<boolean>(NetKeepAlive.setKeepAliveInterval(clientSocket, 1000))
expectType<boolean>(NetKeepAlive.setKeepAliveProbes(clientSocket, 1))
expectType<boolean>(NetKeepAlive.setUserTimeout(clientSocket, 5000))
2 changes: 2 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Constants = {
SOL_TCP: 6,
TCP_KEEPINTVL: undefined,
TCP_KEEPCNT: undefined,
TCP_USER_TIMEOUT: undefined,
}

switch (OS.platform()) {
Expand All @@ -23,6 +24,7 @@ switch (OS.platform()) {
default:
Constants.TCP_KEEPINTVL = 5
Constants.TCP_KEEPCNT = 6
Constants.TCP_USER_TIMEOUT = 18
break
}

Expand Down
93 changes: 93 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,96 @@ module.exports.getKeepAliveProbes = function getKeepAliveProbes(socket) {

return cntVal.deref()
}

/**
* Sets the TCP_USER_TIMEOUT value for specified socket.
*
* Note: The msec will be rounded towards the closest integer
*
* @since v1.4.0
* @param {Net.Socket} socket to set the value for
* @param {number} msecs to wait for unacknowledged data before closing the connection
*
* @returns {boolean} <code>true</code> on success
*
* @example <caption>Set user timeout to 30 seconds (<code>1000</code> milliseconds) for socket <code>s</code></caption>
* NetKeepAlive.setUserTimeout(s, 30000)
*
* @throws {AssertionError} setUserTimeout requires two arguments
* @throws {AssertionError} setUserTimeout expects an instance of socket as its first argument
* @throws {AssertionError} setUserTimeout requires msec to be a Number
* @throws {ErrnoException|Error} Unexpected error
*/
module.exports.setUserTimeout = function setUserTimeout(
socket,
msecs
) {
Assert.strictEqual(
arguments.length,
2,
'setUserTimeout requires two arguments'
)
Assert(
_isSocket(socket),
'setUserTimeout expects an instance of socket as its first argument'
)
Assert.strictEqual(
msecs != null ? msecs.constructor : void 0,
Number,
'setUserTimeout requires msec to be a Number'
)

const fd = _getSocketFD(socket),
seconds = ~~msecs,
intvlVal = Ref.alloc('int', seconds),
intvlValLn = intvlVal.type.size

return FFIBindings.setsockopt(
fd,
Constants.SOL_TCP,
Constants.TCP_USER_TIMEOUT,
intvlVal,
intvlValLn
)
}

/**
* Returns the TCP_USER_TIMEOUT value for specified socket.
*
* @since v1.4.0
* @param {Net.Socket} socket to check the value for
*
* @returns {number} msecs to wait for unacknowledged data before closing the connection
*
* @example <caption>Get the current user timeout for socket <code>s</code></caption>
* NetKeepAlive.getUserTimeout(s) // returns 30000 based on setter example
*
* @throws {AssertionError} getUserTimeout requires one arguments
* @throws {AssertionError} getUserTimeout expects an instance of socket as its first argument
* @throws {ErrnoException|Error} Unexpected error
*/
module.exports.getUserTimeout = function getUserTimeout(socket) {
Assert.strictEqual(
arguments.length,
1,
'getUserTimeout requires one arguments'
)
Assert(
_isSocket(socket),
'getUserTimeout expects an instance of socket as its first argument'
)

const fd = _getSocketFD(socket),
intvlVal = Ref.alloc(Ref.types.uint32),
intvlValLn = Ref.alloc(Ref.types.uint32, intvlVal.type.size)

FFIBindings.getsockopt(
fd,
Constants.SOL_TCP,
Constants.TCP_USER_TIMEOUT,
intvlVal,
intvlValLn
)

return intvlVal.deref()
}
2 changes: 2 additions & 0 deletions test/unit/test-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('constants', () => {
SOL_TCP: 6,
TCP_KEEPINTVL: 5,
TCP_KEEPCNT: 6,
TCP_USER_TIMEOUT: 18,
})
})

Expand All @@ -67,6 +68,7 @@ describe('constants', () => {
SOL_TCP: 6,
TCP_KEEPINTVL: 5,
TCP_KEEPCNT: 6,
TCP_USER_TIMEOUT: 18,
})
})
})
114 changes: 114 additions & 0 deletions test/unit/test-timeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const Stream = require('stream')
const Should = require('should')
const OS = require('os')
const Net = require('net')
const Lib = require('../../lib')

describe('TCP User Timeout', () => {
const itSkipOS = (skipOs, ...args) =>
(skipOs.includes(OS.platform()) ? it.skip : it)(...args)

it('should be a function', function () {
Lib.setUserTimeout.should.be.type('function')
})

it('should validate passed arguments', function () {
;(() => Lib.setUserTimeout()).should.throw(
'setUserTimeout requires two arguments'
)
;(() => Lib.setUserTimeout('')).should.throw(
'setUserTimeout requires two arguments'
)
;(() => Lib.setUserTimeout('', '', '')).should.throw(
'setUserTimeout requires two arguments'
)
;(() => Lib.setUserTimeout(null, 1)).should.throw(
'setUserTimeout expects an instance of socket as its first argument'
)
;(() => Lib.setUserTimeout({}, 1)).should.throw(
'setUserTimeout expects an instance of socket as its first argument'
)
;(() => Lib.setUserTimeout(new (class {})(), 1)).should.throw(
'setUserTimeout expects an instance of socket as its first argument'
)
;(() => Lib.setUserTimeout(new Stream.PassThrough(), 1)).should.throw(
'setUserTimeout expects an instance of socket as its first argument'
)

const socket = new Net.Socket()
;(() => Lib.setUserTimeout(socket, null)).should.throw(
'setUserTimeout requires msec to be a Number'
)
;(() => Lib.setUserTimeout(socket, '')).should.throw(
'setUserTimeout requires msec to be a Number'
)
;(() => Lib.setUserTimeout(socket, true)).should.throw(
'setUserTimeout requires msec to be a Number'
)
;(() => Lib.setUserTimeout(socket, {})).should.throw(
'setUserTimeout requires msec to be a Number'
)
})

itSkipOS(
['darwin', 'freebsd'],
'should throw when setsockopt returns -1',
(done) => {
const srv = Net.createServer()
srv.listen(0, () => {
const socket = Net.createConnection(srv.address(), () => {
;(() => Lib.setUserTimeout(socket, -1)).should.throw(
/^setsockopt /i
)
socket.destroy()
srv.close(done)
})
})
}
)

itSkipOS(['darwin', 'freebsd'], 'should be able to set and get 4 second value', (done) => {
const srv = Net.createServer()
srv.listen(0, () => {
const expected = 4000

const socket = Net.createConnection(srv.address(), () => {
;(() => {
Lib.setUserTimeout(socket, expected)
}).should.not.throw()

let actual
;(() => {
actual = Lib.getUserTimeout(socket)
}).should.not.throw()

expected.should.eql(actual)

socket.destroy()
srv.close(done)
})
})
})

itSkipOS(['darwin', 'freebsd'], 'should throw when trying to get using invalid fd', (done) => {
;(() => Lib.setUserTimeout(new Net.Socket(), 1)).should.throw(
'Unable to get socket fd'
)

const srv = Net.createServer()
srv.listen(0, function () {
const socket = Net.createConnection(this.address(), () => {
const oldHandle = socket._handle

socket._handle = { fd: -99999 }
;(() => Lib.getUserTimeout(socket)).should.throw(
'getsockopt EBADF'
)

socket._handle = oldHandle
socket.destroy()
srv.close(done)
})
})
})
})

0 comments on commit 51462f7

Please sign in to comment.