Skip to content

Commit 51462f7

Browse files
mildsunrisehertzg
authored andcommitted
feat: add support for the TCP_USER_TIMEOUT option
1 parent 4766b02 commit 51462f7

File tree

7 files changed

+224
-0
lines changed

7 files changed

+224
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ The Missing (`TCP_KEEPINTVL` and `TCP_KEEPCNT`) `SO_KEEPALIVE` socket option set
3131

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

34+
There's also linux support for setting the `TCP_USER_TIMEOUT` option, which is closely related to keep-alive.
35+
3436
## Install
3537

3638
```bash

index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,12 @@ export function setKeepAliveProbes(
2828
export function getKeepAliveProbes(
2929
socket: NodeJSSocketWithFileDescriptor
3030
): number
31+
32+
export function setUserTimeout(
33+
socket: NodeJSSocketWithFileDescriptor,
34+
timeout: number
35+
): boolean
36+
37+
export function getUserTimeout(
38+
socket: NodeJSSocketWithFileDescriptor
39+
): number

index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { expectType } from 'tsd'
55
Net.createServer((incomingSocket) => {
66
expectType<boolean>(NetKeepAlive.setKeepAliveInterval(incomingSocket, 1000))
77
expectType<boolean>(NetKeepAlive.setKeepAliveProbes(incomingSocket, 1))
8+
expectType<boolean>(NetKeepAlive.setUserTimeout(incomingSocket, 5000))
89
})
910

1011
const clientSocket = Net.createConnection({port: -1})
1112
expectType<boolean>(NetKeepAlive.setKeepAliveInterval(clientSocket, 1000))
1213
expectType<boolean>(NetKeepAlive.setKeepAliveProbes(clientSocket, 1))
14+
expectType<boolean>(NetKeepAlive.setUserTimeout(clientSocket, 5000))

lib/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Constants = {
66
SOL_TCP: 6,
77
TCP_KEEPINTVL: undefined,
88
TCP_KEEPCNT: undefined,
9+
TCP_USER_TIMEOUT: undefined,
910
}
1011

1112
switch (OS.platform()) {
@@ -23,6 +24,7 @@ switch (OS.platform()) {
2324
default:
2425
Constants.TCP_KEEPINTVL = 5
2526
Constants.TCP_KEEPCNT = 6
27+
Constants.TCP_USER_TIMEOUT = 18
2628
break
2729
}
2830

lib/index.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,96 @@ module.exports.getKeepAliveProbes = function getKeepAliveProbes(socket) {
213213

214214
return cntVal.deref()
215215
}
216+
217+
/**
218+
* Sets the TCP_USER_TIMEOUT value for specified socket.
219+
*
220+
* Note: The msec will be rounded towards the closest integer
221+
*
222+
* @since v1.4.0
223+
* @param {Net.Socket} socket to set the value for
224+
* @param {number} msecs to wait for unacknowledged data before closing the connection
225+
*
226+
* @returns {boolean} <code>true</code> on success
227+
*
228+
* @example <caption>Set user timeout to 30 seconds (<code>1000</code> milliseconds) for socket <code>s</code></caption>
229+
* NetKeepAlive.setUserTimeout(s, 30000)
230+
*
231+
* @throws {AssertionError} setUserTimeout requires two arguments
232+
* @throws {AssertionError} setUserTimeout expects an instance of socket as its first argument
233+
* @throws {AssertionError} setUserTimeout requires msec to be a Number
234+
* @throws {ErrnoException|Error} Unexpected error
235+
*/
236+
module.exports.setUserTimeout = function setUserTimeout(
237+
socket,
238+
msecs
239+
) {
240+
Assert.strictEqual(
241+
arguments.length,
242+
2,
243+
'setUserTimeout requires two arguments'
244+
)
245+
Assert(
246+
_isSocket(socket),
247+
'setUserTimeout expects an instance of socket as its first argument'
248+
)
249+
Assert.strictEqual(
250+
msecs != null ? msecs.constructor : void 0,
251+
Number,
252+
'setUserTimeout requires msec to be a Number'
253+
)
254+
255+
const fd = _getSocketFD(socket),
256+
seconds = ~~msecs,
257+
intvlVal = Ref.alloc('int', seconds),
258+
intvlValLn = intvlVal.type.size
259+
260+
return FFIBindings.setsockopt(
261+
fd,
262+
Constants.SOL_TCP,
263+
Constants.TCP_USER_TIMEOUT,
264+
intvlVal,
265+
intvlValLn
266+
)
267+
}
268+
269+
/**
270+
* Returns the TCP_USER_TIMEOUT value for specified socket.
271+
*
272+
* @since v1.4.0
273+
* @param {Net.Socket} socket to check the value for
274+
*
275+
* @returns {number} msecs to wait for unacknowledged data before closing the connection
276+
*
277+
* @example <caption>Get the current user timeout for socket <code>s</code></caption>
278+
* NetKeepAlive.getUserTimeout(s) // returns 30000 based on setter example
279+
*
280+
* @throws {AssertionError} getUserTimeout requires one arguments
281+
* @throws {AssertionError} getUserTimeout expects an instance of socket as its first argument
282+
* @throws {ErrnoException|Error} Unexpected error
283+
*/
284+
module.exports.getUserTimeout = function getUserTimeout(socket) {
285+
Assert.strictEqual(
286+
arguments.length,
287+
1,
288+
'getUserTimeout requires one arguments'
289+
)
290+
Assert(
291+
_isSocket(socket),
292+
'getUserTimeout expects an instance of socket as its first argument'
293+
)
294+
295+
const fd = _getSocketFD(socket),
296+
intvlVal = Ref.alloc(Ref.types.uint32),
297+
intvlValLn = Ref.alloc(Ref.types.uint32, intvlVal.type.size)
298+
299+
FFIBindings.getsockopt(
300+
fd,
301+
Constants.SOL_TCP,
302+
Constants.TCP_USER_TIMEOUT,
303+
intvlVal,
304+
intvlValLn
305+
)
306+
307+
return intvlVal.deref()
308+
}

test/unit/test-constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('constants', () => {
5757
SOL_TCP: 6,
5858
TCP_KEEPINTVL: 5,
5959
TCP_KEEPCNT: 6,
60+
TCP_USER_TIMEOUT: 18,
6061
})
6162
})
6263

@@ -67,6 +68,7 @@ describe('constants', () => {
6768
SOL_TCP: 6,
6869
TCP_KEEPINTVL: 5,
6970
TCP_KEEPCNT: 6,
71+
TCP_USER_TIMEOUT: 18,
7072
})
7173
})
7274
})

test/unit/test-timeout.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const Stream = require('stream')
2+
const Should = require('should')
3+
const OS = require('os')
4+
const Net = require('net')
5+
const Lib = require('../../lib')
6+
7+
describe('TCP User Timeout', () => {
8+
const itSkipOS = (skipOs, ...args) =>
9+
(skipOs.includes(OS.platform()) ? it.skip : it)(...args)
10+
11+
it('should be a function', function () {
12+
Lib.setUserTimeout.should.be.type('function')
13+
})
14+
15+
it('should validate passed arguments', function () {
16+
;(() => Lib.setUserTimeout()).should.throw(
17+
'setUserTimeout requires two arguments'
18+
)
19+
;(() => Lib.setUserTimeout('')).should.throw(
20+
'setUserTimeout requires two arguments'
21+
)
22+
;(() => Lib.setUserTimeout('', '', '')).should.throw(
23+
'setUserTimeout requires two arguments'
24+
)
25+
;(() => Lib.setUserTimeout(null, 1)).should.throw(
26+
'setUserTimeout expects an instance of socket as its first argument'
27+
)
28+
;(() => Lib.setUserTimeout({}, 1)).should.throw(
29+
'setUserTimeout expects an instance of socket as its first argument'
30+
)
31+
;(() => Lib.setUserTimeout(new (class {})(), 1)).should.throw(
32+
'setUserTimeout expects an instance of socket as its first argument'
33+
)
34+
;(() => Lib.setUserTimeout(new Stream.PassThrough(), 1)).should.throw(
35+
'setUserTimeout expects an instance of socket as its first argument'
36+
)
37+
38+
const socket = new Net.Socket()
39+
;(() => Lib.setUserTimeout(socket, null)).should.throw(
40+
'setUserTimeout requires msec to be a Number'
41+
)
42+
;(() => Lib.setUserTimeout(socket, '')).should.throw(
43+
'setUserTimeout requires msec to be a Number'
44+
)
45+
;(() => Lib.setUserTimeout(socket, true)).should.throw(
46+
'setUserTimeout requires msec to be a Number'
47+
)
48+
;(() => Lib.setUserTimeout(socket, {})).should.throw(
49+
'setUserTimeout requires msec to be a Number'
50+
)
51+
})
52+
53+
itSkipOS(
54+
['darwin', 'freebsd'],
55+
'should throw when setsockopt returns -1',
56+
(done) => {
57+
const srv = Net.createServer()
58+
srv.listen(0, () => {
59+
const socket = Net.createConnection(srv.address(), () => {
60+
;(() => Lib.setUserTimeout(socket, -1)).should.throw(
61+
/^setsockopt /i
62+
)
63+
socket.destroy()
64+
srv.close(done)
65+
})
66+
})
67+
}
68+
)
69+
70+
itSkipOS(['darwin', 'freebsd'], 'should be able to set and get 4 second value', (done) => {
71+
const srv = Net.createServer()
72+
srv.listen(0, () => {
73+
const expected = 4000
74+
75+
const socket = Net.createConnection(srv.address(), () => {
76+
;(() => {
77+
Lib.setUserTimeout(socket, expected)
78+
}).should.not.throw()
79+
80+
let actual
81+
;(() => {
82+
actual = Lib.getUserTimeout(socket)
83+
}).should.not.throw()
84+
85+
expected.should.eql(actual)
86+
87+
socket.destroy()
88+
srv.close(done)
89+
})
90+
})
91+
})
92+
93+
itSkipOS(['darwin', 'freebsd'], 'should throw when trying to get using invalid fd', (done) => {
94+
;(() => Lib.setUserTimeout(new Net.Socket(), 1)).should.throw(
95+
'Unable to get socket fd'
96+
)
97+
98+
const srv = Net.createServer()
99+
srv.listen(0, function () {
100+
const socket = Net.createConnection(this.address(), () => {
101+
const oldHandle = socket._handle
102+
103+
socket._handle = { fd: -99999 }
104+
;(() => Lib.getUserTimeout(socket)).should.throw(
105+
'getsockopt EBADF'
106+
)
107+
108+
socket._handle = oldHandle
109+
socket.destroy()
110+
srv.close(done)
111+
})
112+
})
113+
})
114+
})

0 commit comments

Comments
 (0)