Skip to content

Commit fdcb1de

Browse files
mcollinaguybedford
authored andcommitted
net: support TCP handle transfer on Windows
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64460 Fixes: #64456 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent b420cb6 commit fdcb1de

11 files changed

Lines changed: 50 additions & 77 deletions

doc/api/errors.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3577,14 +3577,6 @@ An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
35773577
via a `worker_threads` `postMessage()` call while it was not in a transferable
35783578
state, for example because it had already started reading or had buffered data.
35793579

3580-
<a id="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3581-
3582-
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3583-
3584-
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3585-
on a platform where moving the underlying handle between event loops is not
3586-
supported (currently Windows).
3587-
35883580
<a id="ERR_WORKER_INIT_FAILED"></a>
35893581

35903582
### `ERR_WORKER_INIT_FAILED`

doc/api/net.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -770,9 +770,7 @@ threads.
770770
The socket must be a freshly accepted or created TCP connection: it must still
771771
be attached to a live handle, must not be connecting or destroyed, and must not
772772
have started reading or have buffered data. Otherwise `postMessage()` throws
773-
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported, and only
774-
on Unix-like platforms; on Windows `postMessage()` throws
775-
`ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.
773+
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported.
776774

777775
```cjs
778776
const net = require('node:net');

doc/api/worker_threads.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,8 +1249,7 @@ freshly accepted or created TCP connection that has not yet started reading and
12491249
has no buffered data, otherwise `postMessage()` throws
12501250
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. This makes it possible to accept
12511251
connections on one thread and distribute them across a pool of worker threads.
1252-
Only TCP handles are supported, and only on Unix-like platforms; on Windows
1253-
`postMessage()` throws `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.
1252+
Only TCP handles are supported.
12541253
12551254
If `value` contains {SharedArrayBuffer} instances, those are accessible
12561255
from either thread. They cannot be listed in `transferList`.

lib/internal/errors.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,8 +1978,6 @@ E('ERR_WORKER_HANDLE_NOT_TRANSFERABLE',
19781978
'%s cannot be transferred in its current state; it must be a freshly ' +
19791979
'created or accepted handle that has not started reading and has no ' +
19801980
'pending writes', Error);
1981-
E('ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED',
1982-
'Transferring a %s to another thread is not supported on this platform', Error);
19831981
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
19841982
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
19851983
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,

lib/net.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ const {
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122122
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123-
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
124123
},
125124
genericNodeError,
126125
} = require('internal/errors');
@@ -1600,9 +1599,6 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
16001599
// connecting or destroyed, and with no data already buffered in either
16011600
// direction (which would otherwise be lost on the sending side).
16021601
function assertTransferableSocket(socket) {
1603-
if (isWindows) {
1604-
throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1605-
}
16061602
const handle = socket._handle;
16071603
if (handle == null || !(handle instanceof TCP) ||
16081604
socket.destroyed || socket.connecting ||
@@ -2364,9 +2360,6 @@ Server.prototype._listen2 = setupListenHandle; // legacy alias
23642360
// underlying listening socket (and its pending accept queue) to that thread's
23652361
// event loop. Only a server bound to a live TCP handle can be transferred.
23662362
function assertTransferableServer(server) {
2367-
if (isWindows) {
2368-
throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2369-
}
23702363
if (server._handle == null || !(server._handle instanceof TCP)) {
23712364
throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
23722365
}

src/tcp_wrap.cc

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -381,57 +381,66 @@ void TCPWrap::Open(const FunctionCallbackInfo<Value>& args) {
381381
}
382382

383383
BaseObject::TransferMode TCPWrap::GetTransferMode() const {
384-
#ifdef _WIN32
385-
// Re-adopting a socket into another thread's event loop requires
386-
// re-associating it with that loop's IOCP, which needs same-process
387-
// WSADuplicateSocket support that is not wired up yet. The JS net layer
388-
// throws a clearer error before reaching here; this is the backstop for the
389-
// low-level `socket._handle` transfer path.
390-
return TransferMode::kDisallowCloneAndTransfer;
391-
#else
392384
// Only a live handle that is not already being torn down can be transferred.
393385
// Higher-level guards (no buffered reads, no pending writes) are enforced by
394386
// the JS net.Socket/net.Server layer before a handle reaches here.
395387
if (!HandleWrap::IsAlive(this) || IsHandleClosing())
396388
return TransferMode::kDisallowCloneAndTransfer;
397389
return TransferMode::kTransferable;
398-
#endif
399390
}
400391

401392
std::unique_ptr<worker::TransferData> TCPWrap::TransferForMessaging() {
402-
#ifdef _WIN32
403-
return {};
404-
#else
405393
CHECK_NE(GetTransferMode(), TransferMode::kDisallowCloneAndTransfer);
406394

407395
uv_os_fd_t fd;
408396
if (uv_fileno(reinterpret_cast<uv_handle_t*>(&handle_), &fd) != 0) return {};
409397

410-
// dup() the descriptor so the receiving event loop owns an independent
411-
// reference to the same socket. We then close the source handle, which
412-
// renders it unusable on this side (true transfer semantics) while the dup
413-
// keeps the underlying socket alive for the destination thread.
414-
int dup_fd = dup(fd);
415-
if (dup_fd < 0) return {};
398+
#ifdef _WIN32
399+
// A socket that is already associated with an IOCP cannot be associated with
400+
// another one. Create a same-process duplicate that is not associated with
401+
// any IOCP yet; uv_tcp_open() will associate it with the receiving loop.
402+
WSAPROTOCOL_INFOW protocol_info;
403+
uv_os_sock_t source_socket = reinterpret_cast<uv_os_sock_t>(fd);
404+
if (WSADuplicateSocketW(
405+
source_socket, GetCurrentProcessId(), &protocol_info) != 0) {
406+
return {};
407+
}
408+
uv_os_sock_t duplicate = WSASocketW(FROM_PROTOCOL_INFO,
409+
FROM_PROTOCOL_INFO,
410+
FROM_PROTOCOL_INFO,
411+
&protocol_info,
412+
0,
413+
WSA_FLAG_OVERLAPPED);
414+
if (duplicate == static_cast<uv_os_sock_t>(-1)) return {};
415+
#else
416+
// Unix threads share the descriptor table, so dup() creates an independent
417+
// reference to the same socket for the receiving event loop.
418+
uv_os_sock_t duplicate = dup(fd);
419+
if (duplicate < 0) return {};
420+
#endif
416421

417422
SocketType type =
418423
provider_type() == ProviderType::PROVIDER_TCPSERVERWRAP ? SERVER : SOCKET;
419424

420-
// Stop watching the fd and tear down the source handle.
425+
// Stop watching the original socket and tear down the source handle. The
426+
// duplicate keeps the underlying socket alive until the destination adopts
427+
// it, or until TransferData is destroyed if the message is not delivered.
421428
Close();
422429

423-
return std::make_unique<TransferData>(dup_fd, type);
424-
#endif
430+
return std::make_unique<TransferData>(duplicate, type);
425431
}
426432

427433
TCPWrap::TransferData::~TransferData() {
428-
// Only reached if the message was never delivered (e.g. the destination port
429-
// closed in flight); close the dup'd fd so it is not leaked.
430-
if (fd_ >= 0) {
434+
#ifdef _WIN32
435+
if (socket_ != static_cast<uv_os_sock_t>(-1))
436+
CHECK_EQ(0, closesocket(socket_));
437+
#else
438+
if (socket_ >= 0) {
431439
uv_fs_t req;
432-
CHECK_EQ(0, uv_fs_close(nullptr, &req, fd_, nullptr));
440+
CHECK_EQ(0, uv_fs_close(nullptr, &req, socket_, nullptr));
433441
uv_fs_req_cleanup(&req);
434442
}
443+
#endif
435444
}
436445

437446
BaseObjectPtr<BaseObject> TCPWrap::TransferData::Deserialize(
@@ -454,10 +463,14 @@ BaseObjectPtr<BaseObject> TCPWrap::TransferData::Deserialize(
454463
TCPWrap* wrap = BaseObject::Unwrap<TCPWrap>(obj);
455464
if (wrap == nullptr) return {};
456465

457-
if (uv_tcp_open(&wrap->handle_, fd_) != 0) return {};
466+
if (uv_tcp_open(&wrap->handle_, socket_) != 0) return {};
458467

459-
wrap->set_fd(fd_);
460-
fd_ = -1; // Ownership has been handed to the new handle.
468+
#ifdef _WIN32
469+
socket_ = static_cast<uv_os_sock_t>(-1);
470+
#else
471+
wrap->set_fd(socket_);
472+
socket_ = -1;
473+
#endif
461474
return BaseObjectPtr<BaseObject>(wrap);
462475
}
463476

src/tcp_wrap.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,10 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> {
6262
}
6363
}
6464

65-
// Transfer the underlying socket to another thread via .postMessage(). Within
66-
// a single process all threads share the same file descriptor table, so the
67-
// transfer dup()s the fd and re-adopts it (uv_tcp_open) in the receiving
68-
// event loop. This is the building block for distributing listening sockets
69-
// and accepted connections across worker_threads.
65+
// Transfer the underlying socket to another thread via .postMessage(). The
66+
// transfer duplicates the socket and re-adopts it (uv_tcp_open) in the
67+
// receiving event loop. This is the building block for distributing
68+
// listening sockets and accepted connections across worker_threads.
7069
BaseObject::TransferMode GetTransferMode() const override;
7170
std::unique_ptr<worker::TransferData> TransferForMessaging() override;
7271

@@ -75,7 +74,8 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> {
7574

7675
class TransferData : public worker::TransferData {
7776
public:
78-
explicit TransferData(int fd, SocketType type) : fd_(fd), type_(type) {}
77+
explicit TransferData(uv_os_sock_t socket, SocketType type)
78+
: socket_(socket), type_(type) {}
7979
~TransferData() override;
8080

8181
BaseObjectPtr<BaseObject> Deserialize(
@@ -88,7 +88,7 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> {
8888
SET_SELF_SIZE(TransferData)
8989

9090
private:
91-
int fd_;
91+
uv_os_sock_t socket_;
9292
SocketType type_;
9393
};
9494

test/parallel/test-net-server-transfer-worker.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77

88
const common = require('../common');
99

10-
if (common.isWindows) {
11-
common.skip('transferring TCP handles between threads is not supported on ' +
12-
'Windows yet');
13-
}
14-
1510
const assert = require('assert');
1611
const net = require('net');
1712
const {

test/parallel/test-net-socket-transfer-worker-http.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77

88
const common = require('../common');
99

10-
if (common.isWindows) {
11-
common.skip('transferring TCP handles between threads is not supported on ' +
12-
'Windows yet');
13-
}
14-
1510
const assert = require('assert');
1611
const net = require('net');
1712
const http = require('http');

test/parallel/test-net-socket-transfer-worker.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77

88
const common = require('../common');
99

10-
if (common.isWindows) {
11-
common.skip('transferring TCP handles between threads is not supported on ' +
12-
'Windows yet');
13-
}
14-
1510
const assert = require('assert');
1611
const net = require('net');
1712
const {

0 commit comments

Comments
 (0)