| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent c25b8e3 commit fe9e0db
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1699,9 +1699,20 @@ to `listen()` or `new net.Socket()` later on. For `listen()` this enables | |||
| 1699 | 1699 | synchronous port reservation, while for `new net.Socket()`, it allows control | |
| 1700 | 1700 | over the local egress port/IP, via `bind(2)` semantics. | |
| 1701 | 1701 | ||
| 1702 | + A `BoundSocket` binds either a TCP endpoint (`host` or `port`) or a | ||
| 1703 | + Unix domain/named-pipe endpoint (`path`); the two are mutually exclusive. For a | ||
| 1704 | + `path`, the file system entry is reserved in the constructor, so conflicts such | ||
| 1705 | + as `EADDRINUSE` throw synchronously exactly as a TCP bind does. On Linux a | ||
| 1706 | + leading `'\0'` in `path` selects the abstract namespace (no file system entry); | ||
| 1707 | + an abstract path on any other platform throws [`ERR_INVALID_ARG_VALUE`][]. | ||
| 1708 | + | ||
| 1702 | 1709 | Adoption transfers ownership of the socket; afterwards `address()` and `close()` | |
| 1703 | 1710 | throw [`ERR_SOCKET_HANDLE_ADOPTED`][]. A handle that is never adopted must be | |
| 1704 | - closed to avoid leaking the socket. | ||
| 1711 | + closed to avoid leaking the socket. Closing a pipe `BoundSocket` removes its | ||
| 1712 | + file system entry; abstract and TCP binds have none to remove. | ||
| 1713 | + | ||
| 1714 | + When a pipe `BoundSocket` bound to a source `path` is adopted as a client, that | ||
| 1715 | + path is reported as the socket's `localAddress` once it connects. | ||
| 1705 | 1716 | ||
| 1706 | 1717 | When an adopted `BoundSocket` connects to a numeric IP literal, `connect(2)` is | |
| 1707 | 1718 | issued synchronously, so [`socket.localAddress`][] is resolved once | |
@@ -1723,6 +1734,10 @@ server.listen(bound); // Adopt as a server, or pass to new net.Socket() instead. | |||
| 1723 | 1734 | ||
| 1724 | 1735 | <!-- YAML | |
| 1725 | 1736 | added: v26.4.0 | |
| 1737 | + changes: | ||
| 1738 | + - version: REPLACEME | ||
| 1739 | + pr-url: https://github.com/nodejs/node/pull/64399 | ||
| 1740 | + description: The `path` option is supported. | ||
| 1726 | 1741 | --> | |
| 1727 | 1742 | ||
| 1728 | 1743 | * `options` {Object} | |
@@ -1737,19 +1752,41 @@ added: v26.4.0 | |||
| 1737 | 1752 | * `reusePort` {boolean} Sets `SO_REUSEPORT`, allowing multiple sockets to bind | |
| 1738 | 1753 | the same address and port for kernel-level load balancing. Support is | |
| 1739 | 1754 | platform-dependent. **Default:** `false`. | |
| 1755 | + * `path` {string} Binds a Unix domain socket (or Windows named pipe) at the | ||
| 1756 | + given path instead of a TCP endpoint. A leading `'\0'` selects the Linux | ||
| 1757 | + abstract namespace. Mutually exclusive with `host`, `port`, `ipv6Only`, and | ||
| 1758 | + `reusePort`; combining them throws [`ERR_INVALID_ARG_VALUE`][]. | ||
| 1740 | 1759 | ||
| 1741 | 1760 | ### `boundSocket.address()` | |
| 1742 | 1761 | ||
| 1743 | 1762 | <!-- YAML | |
| 1744 | 1763 | added: v26.4.0 | |
| 1764 | + changes: | ||
| 1765 | + - version: REPLACEME | ||
| 1766 | + pr-url: https://github.com/nodejs/node/pull/64399 | ||
| 1767 | + description: The bound path is returned for a pipe bind. | ||
| 1745 | 1768 | --> | |
| 1746 | 1769 | ||
| 1747 | - * Returns: {Object} An object with `address`, `family`, and `port` properties, | ||
| 1748 | - as [`server.address()`][] returns. | ||
| 1770 | + * Returns: {Object|string} For a TCP bind, an object with `address`, `family`, | ||
| 1771 | + and `port` properties, as [`server.address()`][] returns. For a pipe bind, the | ||
| 1772 | + bound path string, as [`server.address()`][] returns for a pipe server. | ||
| 1749 | 1773 | ||
| 1750 | 1774 | Returns the bound local address. When bound with `port: 0`, `port` is the | |
| 1751 | 1775 | OS-assigned ephemeral port. | |
| 1752 | 1776 | ||
| 1777 | + ### `boundSocket.isPipe` | ||
| 1778 | + | ||
| 1779 | + <!-- YAML | ||
| 1780 | + added: REPLACEME | ||
| 1781 | + --> | ||
| 1782 | + | ||
| 1783 | + * {boolean} | ||
| 1784 | + | ||
| 1785 | + `true` when the socket was bound with a `path` (a Unix domain socket or Windows | ||
| 1786 | + named pipe), `false` for a TCP bind. The getter's presence on | ||
| 1787 | + `net.BoundSocket.prototype` also serves as a capability probe for `path` | ||
| 1788 | + support. | ||
| 1789 | + | ||
| 1753 | 1790 | ### `boundSocket.fd()` | |
| 1754 | 1791 | ||
| 1755 | 1792 | <!-- YAML | |
@@ -2256,6 +2293,7 @@ net.isIPv6('fhqwhgads'); // returns false | |||
| 2256 | 2293 | [`'listening'`]: #event-listening | |
| 2257 | 2294 | [`'timeout'`]: #event-timeout | |
| 2258 | 2295 | [`BoundSocket`]: #class-netboundsocket | |
| 2296 | + [`ERR_INVALID_ARG_VALUE`]: errors.md#err_invalid_arg_value | ||
| 2259 | 2297 | [`ERR_SOCKET_HANDLE_ADOPTED`]: errors.md#err_socket_handle_adopted | |
| 2260 | 2298 | [`EventEmitter`]: events.md#class-eventemitter | |
| 2261 | 2299 | [`child_process.fork()`]: child_process.md#child_processforkmodulepath-args-options | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -380,19 +380,35 @@ const kBoundSource = Symbol('kBoundSource'); | |||
| 380 | 380 | // Server/Socket. | |
| 381 | 381 | const kBoundSocketConsume = Symbol('kBoundSocketConsume'); | |
| 382 | 382 | ||
| 383 | - // A role-neutral wrapper over a synchronously bound libuv TCP handle: bound to | ||
| 384 | - // a local address but neither listening nor connecting until adopted by exactly | ||
| 385 | - // one Server (server.listen) or Socket (new net.Socket({ handle })). Adoption | ||
| 386 | - // transfers ownership; an un-adopted handle must be closed by the caller. | ||
| 387 | - // bind(2) is non-blocking, so binding happens inline and errors throw | ||
| 388 | - // synchronously. host must be a numeric IP literal; no DNS is performed. | ||
| 383 | + // Internal: read a pipe BoundSocket's bound path before adoption (undefined for | ||
| 384 | + // a TCP BoundSocket). | ||
| 385 | + const kBoundSocketPath = Symbol('kBoundSocketPath'); | ||
| 386 | + | ||
| 387 | + // The source path of an adopted, bound client pipe, surfaced as localAddress. | ||
| 388 | + const kBoundPath = Symbol('kBoundPath'); | ||
| 389 | + | ||
| 390 | + const isLinux = process.platform === 'linux'; | ||
| 391 | + | ||
| 392 | + // A role-neutral wrapper over a synchronously bound libuv handle: bound to a | ||
| 393 | + // local address (a numeric IP literal for TCP, or a filesystem/abstract path | ||
| 394 | + // for a unix-domain socket via { path }) but neither listening nor connecting | ||
| 395 | + // until adopted by exactly one Server (server.listen) or Socket | ||
| 396 | + // (new net.Socket({ handle })). Adoption transfers ownership; an un-adopted | ||
| 397 | + // handle must be closed by the caller. bind(2) is non-blocking, so binding | ||
| 398 | + // happens inline and errors throw synchronously. No DNS is performed. | ||
| 389 | 399 | class BoundSocket { | |
| 390 | 400 | #handle; | |
| 391 | 401 | #address = {}; | |
| 402 | + #path; | ||
| 392 | 403 | ||
| 393 | 404 | constructor(options = kEmptyObject) { | |
| 394 | 405 | validateObject(options, 'options'); | |
| 395 | 406 | ||
| 407 | + if (options.path !== undefined) { | ||
| 408 | + this.#bindPipe(options); | ||
| 409 | + return; | ||
| 410 | + } | ||
| 411 | + | ||
| 396 | 412 | const port = validatePort(options.port ?? 0, 'options.port'); | |
| 397 | 413 | ||
| 398 | 414 | const ipv6Only = options.ipv6Only ?? false; | |
@@ -443,13 +459,47 @@ class BoundSocket { | |||
| 443 | 459 | this.#handle = handle; | |
| 444 | 460 | } | |
| 445 | 461 | ||
| 446 | - // The kernel-assigned local address, resolved at construction; reflects the | ||
| 447 | - // OS-assigned ephemeral port when the bind requested port 0. | ||
| 462 | + // Bind a named unix-domain socket (or Windows named pipe). A leading '\0' | ||
| 463 | + // selects the Linux abstract namespace. path is mutually exclusive with the | ||
| 464 | + // TCP options; uv_pipe_bind is synchronous so conflicts throw here. | ||
| 465 | + #bindPipe(options) { | ||
| 466 | + const { path, host, port, ipv6Only, reusePort } = options; | ||
| 467 | + if (host !== undefined || port !== undefined || | ||
| 468 | + ipv6Only !== undefined || reusePort !== undefined) { | ||
| 469 | + throw new ERR_INVALID_ARG_VALUE( | ||
| 470 | + 'options', options, | ||
| 471 | + 'path is mutually exclusive with host, port, ipv6Only, and reusePort'); | ||
| 472 | + } | ||
| 473 | + validateString(path, 'options.path'); | ||
| 474 | + if (path[0] === '\0' && !isLinux) { | ||
| 475 | + throw new ERR_INVALID_ARG_VALUE( | ||
| 476 | + 'options.path', path, | ||
| 477 | + 'abstract socket paths are only supported on Linux'); | ||
| 478 | + } | ||
| 479 | + | ||
| 480 | + const handle = new Pipe(PipeConstants.SOCKET); | ||
| 481 | + const err = handle.bind(path); | ||
| 482 | + if (err) { | ||
| 483 | + handle.close(); | ||
| 484 | + throw new ErrnoException(err, 'bind'); | ||
| 485 | + } | ||
| 486 | + | ||
| 487 | + this.#handle = handle; | ||
| 488 | + this.#path = path; | ||
| 489 | + } | ||
| 490 | + | ||
| 491 | + // The bound local endpoint: an { address, family, port } object for TCP, or | ||
| 492 | + // the path string for a pipe (matching net.Server.address()). Resolved at | ||
| 493 | + // construction; a TCP bind of port 0 reflects the OS-assigned port. | ||
| 448 | 494 | address() { | |
| 449 | 495 | if (this.#handle === null) { | |
| 450 | 496 | throw new ERR_SOCKET_HANDLE_ADOPTED(); | |
| 451 | 497 | } | |
| 452 | - return this.#address; | ||
| 498 | + return this.#path ?? this.#address; | ||
| 499 | + } | ||
| 500 | + | ||
| 501 | + get [kBoundSocketPath]() { | ||
| 502 | + return this.#path; | ||
| 453 | 503 | } | |
| 454 | 504 | ||
| 455 | 505 | // The underlying OS file descriptor, or -1 where sockets have none (Windows). | |
@@ -488,6 +538,13 @@ class BoundSocket { | |||
| 488 | 538 | this.#handle = null; | |
| 489 | 539 | return handle; | |
| 490 | 540 | } | |
| 541 | + | ||
| 542 | + // Reports whether this is a pipe (unix-domain) bind rather than TCP. Its mere | ||
| 543 | + // presence on the prototype ('isPipe' in net.BoundSocket.prototype) is the | ||
| 544 | + // capability signal that this build honors { path } instead of a TCP port. | ||
| 545 | + get isPipe() { | ||
| 546 | + return this.#path !== undefined; | ||
| 547 | + } | ||
| 491 | 548 | } | |
| 492 | 549 | ||
| 493 | 550 | function Socket(options) { | |
@@ -555,9 +612,24 @@ function Socket(options) { | |||
| 555 | 612 | let boundNotConnected = false; | |
| 556 | 613 | if (options.handle) { | |
| 557 | 614 | if (options.handle instanceof BoundSocket) { | |
| 615 | + const boundPath = options.handle[kBoundSocketPath]; | ||
| 558 | 616 | this._handle = options.handle[kBoundSocketConsume](); | |
| 559 | 617 | this[kBoundSource] = true; | |
| 560 | 618 | boundNotConnected = true; | |
| 619 | + // A bound client pipe owns a source path; surface it as localAddress. | ||
| 620 | + if (boundPath !== undefined) { | ||
| 621 | + this[kBoundPath] = boundPath; | ||
| 622 | + // uv_pipe_bind() only assigns the fd; it does not open the stream, so a | ||
| 623 | + // later connect() would leave the handle without its readable/writable | ||
| 624 | + // flags and unusable. Re-open the already-bound fd (idempotent, sets the | ||
| 625 | + // flags) so the adopted client pipe connects to a working stream. | ||
| 626 | + if (this._handle.fd >= 0) { | ||
| 627 | + const err = this._handle.open(this._handle.fd); | ||
| 628 | + if (err) { | ||
| 629 | + throw new ErrnoException(err, 'open'); | ||
| 630 | + } | ||
| 631 | + } | ||
| 632 | + } | ||
| 561 | 633 | } else { | |
| 562 | 634 | this._handle = options.handle; // private | |
| 563 | 635 | } | |
@@ -1120,6 +1192,11 @@ protoGetter('remotePort', function remotePort() { | |||
| 1120 | 1192 | ||
| 1121 | 1193 | ||
| 1122 | 1194 | Socket.prototype._getsockname = function() { | |
| 1195 | + // An adopted bound client pipe has no handle getsockname; its source path was | ||
| 1196 | + // captured at adoption and stays authoritative across the connect reset. | ||
| 1197 | + if (this[kBoundPath] !== undefined) { | ||
| 1198 | + return { address: this[kBoundPath] }; | ||
| 1199 | + } | ||
| 1123 | 1200 | if (!this._handle || !this._handle.getsockname) { | |
| 1124 | 1201 | return {}; | |
| 1125 | 1202 | } else if (!this._sockname) { | |
@@ -1478,7 +1555,13 @@ Socket.prototype.connect = function(...args) { | |||
| 1478 | 1555 | } | |
| 1479 | 1556 | ||
| 1480 | 1557 | const { path } = options; | |
| 1481 | - const pipe = !!path; | ||
| 1558 | + // An adopted BoundSocket handle already fixes the transport; trust its type | ||
| 1559 | + // rather than inferring pipe-ness from a path option on the connect call. | ||
| 1560 | + // Once destroyed the adopted handle is gone (its reservation released), so | ||
| 1561 | + // fall back to the path option, as for other pre-existing handles (e.g. a | ||
| 1562 | + // TLSWrap) that are not transport handles. | ||
| 1563 | + const pipe = this[kBoundSource] && this._handle ? | ||
| 1564 | + this._handle instanceof Pipe : !!path; | ||
| 1482 | 1565 | debug('pipe', pipe, path); | |
| 1483 | 1566 | ||
| 1484 | 1567 | if (!this._handle) { | |
@@ -2425,7 +2508,12 @@ Server.prototype.listen = function(...args) { | |||
| 2425 | 2508 | boundSocket = options.handle; | |
| 2426 | 2509 | } | |
| 2427 | 2510 | if (boundSocket !== null) { | |
| 2511 | + const boundPath = boundSocket[kBoundSocketPath]; | ||
| 2428 | 2512 | this._handle = boundSocket[kBoundSocketConsume](); | |
| 2513 | + // A pipe-backed handle reports its path via Server.address(). | ||
| 2514 | + if (boundPath !== undefined) { | ||
| 2515 | + this._pipeName = boundPath; | ||
| 2516 | + } | ||
| 2429 | 2517 | this[async_id_symbol] = this._handle.getAsyncId(); | |
| 2430 | 2518 | this._listeningId++; | |
| 2431 | 2519 | listenInCluster(this, null, -1, -1, backlogFromArgs, undefined, true); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments