GeezerVison is showing github.com — net: support TCP handle transfer on Windows · nodejs/node@fffd8a7
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md -8Lines changed: 0 additions & 8 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3568,14 +3568,6 @@ An attempt was made to transfer a `net.Socket` or `net.Server` to another thread | 3568 | 3568 | via a `worker_threads` `postMessage()` call while it was not in a transferable | 3569 | 3569 | state, for example because it had already started reading or had buffered data. | 3570 | 3570 |
| 3571 | | -<a id="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a> | 3572 | | - | 3573 | | -### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED` | 3574 | | - | 3575 | | -An attempt was made to transfer a `net.Socket` or `net.Server` to another thread | 3576 | | -on a platform where moving the underlying handle between event loops is not | 3577 | | -supported (currently Windows). | 3578 | | - | 3579 | 3571 | <a id="ERR_WORKER_INIT_FAILED"></a> | 3580 | 3572 |
| 3581 | 3573 | ### `ERR_WORKER_INIT_FAILED` | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/net.md +1-3Lines changed: 1 addition & 3 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -770,9 +770,7 @@ threads. | 770 | 770 | The socket must be a freshly accepted or created TCP connection: it must still | 771 | 771 | be attached to a live handle, must not be connecting or destroyed, and must not | 772 | 772 | 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. | 776 | 774 |
| 777 | 775 | ```cjs | 778 | 776 | const net = require('node:net'); | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/worker_threads.md +1-2Lines changed: 1 addition & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1249,8 +1249,7 @@ freshly accepted or created TCP connection that has not yet started reading and | 1249 | 1249 | has no buffered data, otherwise `postMessage()` throws | 1250 | 1250 | `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. This makes it possible to accept | 1251 | 1251 | 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. | 1254 | 1253 |
| 1255 | 1254 | If `value` contains {SharedArrayBuffer} instances, those are accessible | 1256 | 1255 | from either thread. They cannot be listed in `transferList`. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js -2Lines changed: 0 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1975,8 +1975,6 @@ E('ERR_WORKER_HANDLE_NOT_TRANSFERABLE', | 1975 | 1975 | '%s cannot be transferred in its current state; it must be a freshly ' + | 1976 | 1976 | 'created or accepted handle that has not started reading and has no ' + | 1977 | 1977 | 'pending writes', Error); | 1978 | | -E('ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED', | 1979 | | - 'Transferring a %s to another thread is not supported on this platform', Error); | 1980 | 1978 | E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); | 1981 | 1979 | E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') => | 1982 | 1980 | `Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`, | |
Collapse file Copy file name to clipboardExpand all lines: lib/net.js -7Lines changed: 0 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -120,7 +120,6 @@ const { | 120 | 120 | ERR_SOCKET_CONNECTION_TIMEOUT, | 121 | 121 | ERR_SOCKET_HANDLE_ADOPTED, | 122 | 122 | ERR_WORKER_HANDLE_NOT_TRANSFERABLE, | 123 | | - ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED, | 124 | 123 | }, | 125 | 124 | genericNodeError, | 126 | 125 | } = require('internal/errors'); | @@ -1600,9 +1599,6 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { | 1600 | 1599 | // connecting or destroyed, and with no data already buffered in either | 1601 | 1600 | // direction (which would otherwise be lost on the sending side). | 1602 | 1601 | function assertTransferableSocket(socket) { | 1603 | | - if (isWindows) { | 1604 | | - throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket'); | 1605 | | - } | 1606 | 1602 | const handle = socket._handle; | 1607 | 1603 | if (handle == null || !(handle instanceof TCP) || | 1608 | 1604 | socket.destroyed || socket.connecting || | @@ -2364,9 +2360,6 @@ Server.prototype._listen2 = setupListenHandle; // legacy alias | 2364 | 2360 | // underlying listening socket (and its pending accept queue) to that thread's | 2365 | 2361 | // event loop. Only a server bound to a live TCP handle can be transferred. | 2366 | 2362 | function assertTransferableServer(server) { | 2367 | | - if (isWindows) { | 2368 | | - throw new ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server'); | 2369 | | - } | 2370 | 2363 | if (server._handle == null || !(server._handle instanceof TCP)) { | 2371 | 2364 | throw new ERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server'); | 2372 | 2365 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/tcp_wrap.cc +41-28Lines changed: 41 additions & 28 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -381,57 +381,66 @@ void TCPWrap::Open(const FunctionCallbackInfo<Value>& args) { | 381 | 381 | } | 382 | 382 |
| 383 | 383 | 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 | 392 | 384 | // Only a live handle that is not already being torn down can be transferred. | 393 | 385 | // Higher-level guards (no buffered reads, no pending writes) are enforced by | 394 | 386 | // the JS net.Socket/net.Server layer before a handle reaches here. | 395 | 387 | if (!HandleWrap::IsAlive(this) || IsHandleClosing()) | 396 | 388 | return TransferMode::kDisallowCloneAndTransfer; | 397 | 389 | return TransferMode::kTransferable; | 398 | | -#endif | 399 | 390 | } | 400 | 391 |
| 401 | 392 | std::unique_ptr<worker::TransferData> TCPWrap::TransferForMessaging() { | 402 | | -#ifdef _WIN32 | 403 | | - return {}; | 404 | | -#else | 405 | 393 | CHECK_NE(GetTransferMode(), TransferMode::kDisallowCloneAndTransfer); | 406 | 394 |
| 407 | 395 | uv_os_fd_t fd; | 408 | 396 | if (uv_fileno(reinterpret_cast<uv_handle_t*>(&handle_), &fd) != 0) return {}; | 409 | 397 |
| 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 | 416 | 421 |
| 417 | 422 | SocketType type = | 418 | 423 | provider_type() == ProviderType::PROVIDER_TCPSERVERWRAP ? SERVER : SOCKET; | 419 | 424 |
| 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. | 421 | 428 | Close(); | 422 | 429 |
| 423 | | - return std::make_unique<TransferData>(dup_fd, type); | 424 | | -#endif | | 430 | + return std::make_unique<TransferData>(duplicate, type); | 425 | 431 | } | 426 | 432 |
| 427 | 433 | 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) { | 431 | 439 | 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)); | 433 | 441 | uv_fs_req_cleanup(&req); | 434 | 442 | } | | 443 | +#endif | 435 | 444 | } | 436 | 445 |
| 437 | 446 | BaseObjectPtr<BaseObject> TCPWrap::TransferData::Deserialize( | @@ -454,10 +463,14 @@ BaseObjectPtr<BaseObject> TCPWrap::TransferData::Deserialize( | 454 | 463 | TCPWrap* wrap = BaseObject::Unwrap<TCPWrap>(obj); | 455 | 464 | if (wrap == nullptr) return {}; | 456 | 465 |
| 457 | | - if (uv_tcp_open(&wrap->handle_, fd_) != 0) return {}; | | 466 | + if (uv_tcp_open(&wrap->handle_, socket_) != 0) return {}; | 458 | 467 |
| 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 | 461 | 474 | return BaseObjectPtr<BaseObject>(wrap); | 462 | 475 | } | 463 | 476 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/tcp_wrap.h +7-7Lines changed: 7 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -62,11 +62,10 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> { | 62 | 62 | } | 63 | 63 | } | 64 | 64 |
| 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. | 70 | 69 | BaseObject::TransferMode GetTransferMode() const override; | 71 | 70 | std::unique_ptr<worker::TransferData> TransferForMessaging() override; | 72 | 71 |
| @@ -75,7 +74,8 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> { | 75 | 74 |
| 76 | 75 | class TransferData : public worker::TransferData { | 77 | 76 | 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) {} | 79 | 79 | ~TransferData() override; | 80 | 80 |
| 81 | 81 | BaseObjectPtr<BaseObject> Deserialize( | @@ -88,7 +88,7 @@ class TCPWrap : public ConnectionWrap<TCPWrap, uv_tcp_t> { | 88 | 88 | SET_SELF_SIZE(TransferData) | 89 | 89 |
| 90 | 90 | private: | 91 | | - int fd_; | | 91 | + uv_os_sock_t socket_; | 92 | 92 | SocketType type_; | 93 | 93 | }; | 94 | 94 |
| |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-net-server-transfer-worker.js -5Lines changed: 0 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 |
| 8 | 8 | const common = require('../common'); | 9 | 9 |
| 10 | | -if (common.isWindows) { | 11 | | - common.skip('transferring TCP handles between threads is not supported on ' + | 12 | | - 'Windows yet'); | 13 | | -} | 14 | | - | 15 | 10 | const assert = require('assert'); | 16 | 11 | const net = require('net'); | 17 | 12 | const { | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-net-socket-transfer-worker-http.js -5Lines changed: 0 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 |
| 8 | 8 | const common = require('../common'); | 9 | 9 |
| 10 | | -if (common.isWindows) { | 11 | | - common.skip('transferring TCP handles between threads is not supported on ' + | 12 | | - 'Windows yet'); | 13 | | -} | 14 | | - | 15 | 10 | const assert = require('assert'); | 16 | 11 | const net = require('net'); | 17 | 12 | const http = require('http'); | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-net-socket-transfer-worker.js -5Lines changed: 0 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 |
| 8 | 8 | const common = require('../common'); | 9 | 9 |
| 10 | | -if (common.isWindows) { | 11 | | - common.skip('transferring TCP handles between threads is not supported on ' + | 12 | | - 'Windows yet'); | 13 | | -} | 14 | | - | 15 | 10 | const assert = require('assert'); | 16 | 11 | const net = require('net'); | 17 | 12 | const { | |
|