From 060c604656dea1c3c5ddcfb2adb76a4c9fd01e3c Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 12 Jul 2026 21:20:45 +0200 Subject: [PATCH] net: support TCP handle transfer on Windows Signed-off-by: Matteo Collina --- doc/api/errors.md | 8 --- doc/api/net.md | 4 +- doc/api/worker_threads.md | 3 +- lib/internal/errors.js | 2 - lib/net.js | 7 -- src/tcp_wrap.cc | 69 +++++++++++-------- src/tcp_wrap.h | 14 ++-- .../test-net-server-transfer-worker.js | 5 -- .../test-net-socket-transfer-worker-http.js | 5 -- .../test-net-socket-transfer-worker.js | 5 -- test/parallel/test-net-transfer-guards.js | 5 -- 11 files changed, 50 insertions(+), 77 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 29cd9e8a4937d1..5b31cf729d3f40 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3590,14 +3590,6 @@ An attempt was made to transfer a `net.Socket` or `net.Server` to another thread via a `worker_threads` `postMessage()` call while it was not in a transferable state, for example because it had already started reading or had buffered data. - - -### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED` - -An attempt was made to transfer a `net.Socket` or `net.Server` to another thread -on a platform where moving the underlying handle between event loops is not -supported (currently Windows). - ### `ERR_WORKER_INIT_FAILED` diff --git a/doc/api/net.md b/doc/api/net.md index 81863d9b0a13c5..ec14ae5165182b 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -770,9 +770,7 @@ threads. The socket must be a freshly accepted or created TCP connection: it must still be attached to a live handle, must not be connecting or destroyed, and must not have started reading or have buffered data. Otherwise `postMessage()` throws -`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported, and only -on Unix-like platforms; on Windows `postMessage()` throws -`ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`. +`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported. ```cjs const net = require('node:net'); diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index d0b4f0a830b03e..e137722674b39b 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -1249,8 +1249,7 @@ freshly accepted or created TCP connection that has not yet started reading and has no buffered data, otherwise `postMessage()` throws `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. This makes it possible to accept connections on one thread and distribute them across a pool of worker threads. -Only TCP handles are supported, and only on Unix-like platforms; on Windows -`postMessage()` throws `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`. +Only TCP handles are supported. If `value` contains {SharedArrayBuffer} instances, those are accessible from either thread. They cannot be listed in `transferList`. diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 5f4cba63a64499..c39c3a413bd722 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1980,8 +1980,6 @@ E('ERR_WORKER_HANDLE_NOT_TRANSFERABLE', '%s cannot be transferred in its current state; it must be a freshly ' + 'created or accepted handle that has not started reading and has no ' + 'pending writes', Error); -E('ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED', - 'Transferring a %s to another thread is not supported on this platform', Error); E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') => `Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`, diff --git a/lib/net.js b/lib/net.js index d0620449c31e7b..cfaa4242bdba5e 100644 --- a/lib/net.js +++ b/lib/net.js @@ -120,7 +120,6 @@ const { ERR_SOCKET_CONNECTION_TIMEOUT, ERR_SOCKET_HANDLE_ADOPTED, ERR_WORKER_HANDLE_NOT_TRANSFERABLE, - ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED, }, genericNodeError, } = require('internal/errors'); @@ -1517,9 +1516,6 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { // connecting or destroyed, and with no data already buffered in either // direction (which would otherwise be lost on the sending side). function assertTransferableSocket(socket) { - if (isWindows) { - throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket'); - } const handle = socket._handle; if (handle == null || !(handle instanceof TCP) || socket.destroyed || socket.connecting || @@ -2270,9 +2266,6 @@ Server.prototype._listen2 = setupListenHandle; // legacy alias // underlying listening socket (and its pending accept queue) to that thread's // event loop. Only a server bound to a live TCP handle can be transferred. function assertTransferableServer(server) { - if (isWindows) { - throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server'); - } if (server._handle == null || !(server._handle instanceof TCP)) { throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server'); } diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index dc3cd6553d4f9b..9bb742fb3edcbc 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -381,57 +381,66 @@ void TCPWrap::Open(const FunctionCallbackInfo& args) { } BaseObject::TransferMode TCPWrap::GetTransferMode() const { -#ifdef _WIN32 - // Re-adopting a socket into another thread's event loop requires - // re-associating it with that loop's IOCP, which needs same-process - // WSADuplicateSocket support that is not wired up yet. The JS net layer - // throws a clearer error before reaching here; this is the backstop for the - // low-level `socket._handle` transfer path. - return TransferMode::kDisallowCloneAndTransfer; -#else // Only a live handle that is not already being torn down can be transferred. // Higher-level guards (no buffered reads, no pending writes) are enforced by // the JS net.Socket/net.Server layer before a handle reaches here. if (!HandleWrap::IsAlive(this) || IsHandleClosing()) return TransferMode::kDisallowCloneAndTransfer; return TransferMode::kTransferable; -#endif } std::unique_ptr TCPWrap::TransferForMessaging() { -#ifdef _WIN32 - return {}; -#else CHECK_NE(GetTransferMode(), TransferMode::kDisallowCloneAndTransfer); uv_os_fd_t fd; if (uv_fileno(reinterpret_cast(&handle_), &fd) != 0) return {}; - // dup() the descriptor so the receiving event loop owns an independent - // reference to the same socket. We then close the source handle, which - // renders it unusable on this side (true transfer semantics) while the dup - // keeps the underlying socket alive for the destination thread. - int dup_fd = dup(fd); - if (dup_fd < 0) return {}; +#ifdef _WIN32 + // A socket that is already associated with an IOCP cannot be associated with + // another one. Create a same-process duplicate that is not associated with + // any IOCP yet; uv_tcp_open() will associate it with the receiving loop. + WSAPROTOCOL_INFOW protocol_info; + uv_os_sock_t source_socket = reinterpret_cast(fd); + if (WSADuplicateSocketW( + source_socket, GetCurrentProcessId(), &protocol_info) != 0) { + return {}; + } + uv_os_sock_t duplicate = WSASocketW(FROM_PROTOCOL_INFO, + FROM_PROTOCOL_INFO, + FROM_PROTOCOL_INFO, + &protocol_info, + 0, + WSA_FLAG_OVERLAPPED); + if (duplicate == static_cast(-1)) return {}; +#else + // Unix threads share the descriptor table, so dup() creates an independent + // reference to the same socket for the receiving event loop. + uv_os_sock_t duplicate = dup(fd); + if (duplicate < 0) return {}; +#endif SocketType type = provider_type() == ProviderType::PROVIDER_TCPSERVERWRAP ? SERVER : SOCKET; - // Stop watching the fd and tear down the source handle. + // Stop watching the original socket and tear down the source handle. The + // duplicate keeps the underlying socket alive until the destination adopts + // it, or until TransferData is destroyed if the message is not delivered. Close(); - return std::make_unique(dup_fd, type); -#endif + return std::make_unique(duplicate, type); } TCPWrap::TransferData::~TransferData() { - // Only reached if the message was never delivered (e.g. the destination port - // closed in flight); close the dup'd fd so it is not leaked. - if (fd_ >= 0) { +#ifdef _WIN32 + if (socket_ != static_cast(-1)) + CHECK_EQ(0, closesocket(socket_)); +#else + if (socket_ >= 0) { uv_fs_t req; - CHECK_EQ(0, uv_fs_close(nullptr, &req, fd_, nullptr)); + CHECK_EQ(0, uv_fs_close(nullptr, &req, socket_, nullptr)); uv_fs_req_cleanup(&req); } +#endif } BaseObjectPtr TCPWrap::TransferData::Deserialize( @@ -454,10 +463,14 @@ BaseObjectPtr TCPWrap::TransferData::Deserialize( TCPWrap* wrap = BaseObject::Unwrap(obj); if (wrap == nullptr) return {}; - if (uv_tcp_open(&wrap->handle_, fd_) != 0) return {}; + if (uv_tcp_open(&wrap->handle_, socket_) != 0) return {}; - wrap->set_fd(fd_); - fd_ = -1; // Ownership has been handed to the new handle. +#ifdef _WIN32 + socket_ = static_cast(-1); +#else + wrap->set_fd(socket_); + socket_ = -1; +#endif return BaseObjectPtr(wrap); } diff --git a/src/tcp_wrap.h b/src/tcp_wrap.h index d1bcb528157479..4802b62727a264 100644 --- a/src/tcp_wrap.h +++ b/src/tcp_wrap.h @@ -62,11 +62,10 @@ class TCPWrap : public ConnectionWrap { } } - // Transfer the underlying socket to another thread via .postMessage(). Within - // a single process all threads share the same file descriptor table, so the - // transfer dup()s the fd and re-adopts it (uv_tcp_open) in the receiving - // event loop. This is the building block for distributing listening sockets - // and accepted connections across worker_threads. + // Transfer the underlying socket to another thread via .postMessage(). The + // transfer duplicates the socket and re-adopts it (uv_tcp_open) in the + // receiving event loop. This is the building block for distributing + // listening sockets and accepted connections across worker_threads. BaseObject::TransferMode GetTransferMode() const override; std::unique_ptr TransferForMessaging() override; @@ -75,7 +74,8 @@ class TCPWrap : public ConnectionWrap { class TransferData : public worker::TransferData { public: - explicit TransferData(int fd, SocketType type) : fd_(fd), type_(type) {} + explicit TransferData(uv_os_sock_t socket, SocketType type) + : socket_(socket), type_(type) {} ~TransferData() override; BaseObjectPtr Deserialize( @@ -88,7 +88,7 @@ class TCPWrap : public ConnectionWrap { SET_SELF_SIZE(TransferData) private: - int fd_; + uv_os_sock_t socket_; SocketType type_; }; diff --git a/test/parallel/test-net-server-transfer-worker.js b/test/parallel/test-net-server-transfer-worker.js index 6a8fc6a770b5c6..18a52bc1a0c561 100644 --- a/test/parallel/test-net-server-transfer-worker.js +++ b/test/parallel/test-net-server-transfer-worker.js @@ -7,11 +7,6 @@ const common = require('../common'); -if (common.isWindows) { - common.skip('transferring TCP handles between threads is not supported on ' + - 'Windows yet'); -} - const assert = require('assert'); const net = require('net'); const { diff --git a/test/parallel/test-net-socket-transfer-worker-http.js b/test/parallel/test-net-socket-transfer-worker-http.js index 23a3ec1790c808..86245cfeeea19a 100644 --- a/test/parallel/test-net-socket-transfer-worker-http.js +++ b/test/parallel/test-net-socket-transfer-worker-http.js @@ -7,11 +7,6 @@ const common = require('../common'); -if (common.isWindows) { - common.skip('transferring TCP handles between threads is not supported on ' + - 'Windows yet'); -} - const assert = require('assert'); const net = require('net'); const http = require('http'); diff --git a/test/parallel/test-net-socket-transfer-worker.js b/test/parallel/test-net-socket-transfer-worker.js index fe512ee3723453..32a07471fc6162 100644 --- a/test/parallel/test-net-socket-transfer-worker.js +++ b/test/parallel/test-net-socket-transfer-worker.js @@ -7,11 +7,6 @@ const common = require('../common'); -if (common.isWindows) { - common.skip('transferring TCP handles between threads is not supported on ' + - 'Windows yet'); -} - const assert = require('assert'); const net = require('net'); const { diff --git a/test/parallel/test-net-transfer-guards.js b/test/parallel/test-net-transfer-guards.js index 4724913b9f3352..0879ec9da81a8e 100644 --- a/test/parallel/test-net-transfer-guards.js +++ b/test/parallel/test-net-transfer-guards.js @@ -6,11 +6,6 @@ const common = require('../common'); -if (common.isWindows) { - common.skip('transferring TCP handles between threads is not supported on ' + - 'Windows yet'); -} - const assert = require('assert'); const net = require('net'); const { MessageChannel } = require('worker_threads');