feat(net): support reusePort server listen options

Co-authored-by: Flora90001 <163703040+Flora90001@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-08-31 14:57:38 -05:00
co-authored by Flora90001
parent 1730676951
commit 591bed4e32
14 changed files with 411 additions and 51 deletions
+3 -2
View File
@@ -2036,8 +2036,9 @@ declare module "net" {
* spelling). */
listen(port: number, host: string, callback?: () => void): Server;
/* The explicit-interface bind: host is an IP literal (absent = the
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY. */
listen(options: { port: number; host?: string; ipv6Only?: boolean }, callback?: () => void): Server;
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort
* requests kernel connection distribution where Node supports it. */
listen(options: { port: number; host?: string; ipv6Only?: boolean; reusePort?: boolean }, callback?: () => void): Server;
close(callback?: () => void): void;
/* The bound AddressInfo (Node answers null before listen — this
* surface answers the record with port 0 there, the serverPort
+30
View File
@@ -5335,6 +5335,10 @@ function emitNetworkLibCall(state: LibCallState): Temp {
emitter.usesTimers = true;
emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, NULL);${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "net.listenOptsReusePort":
emitter.usesTimers = true;
emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, NULL);${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
case "net.listenOptsCb": {
emitter.usesTimers = true;
// The callback slot may be the `(() => void) | undefined`
@@ -5362,6 +5366,32 @@ function emitNetworkLibCall(state: LibCallState): Temp {
emitter.line(`scr_net_listen_opts(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${cbExpr});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
}
case "net.listenOptsReusePortCb": {
emitter.usesTimers = true;
// The callback slot follows reusePort in the additive ABI and
// may be the `(() => void) | undefined` optional-binding union.
const cbT = e.args[5]!.type;
let cbExpr: string;
if (cbT.kind === "func") {
const cb = args[5]!;
emitter.moveTemp(cb);
cbExpr = cb.name;
} else {
if (cbT.kind !== "union") throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb callback shape");
const def = emitter.unionsById.get(cbT.unionId);
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
if (funcTag < 0) throw new InternalCompilerError("emitter bug: net.listenOptsReusePortCb union lacks its func arm");
const u = args[5]!;
const t = emitter.newTemp(
def!.arms[funcTag]!,
`${u.name}->tag == ${funcTag} ? scr_closure_retain((ScrClosure *)scr_union_peek(${u.name})) : NULL`,
);
emitter.moveTemp(t);
cbExpr = t.name;
}
emitter.line(`scr_net_listen_opts_reuse_port(${arg(0)}, ${arg(1)}, ${arg(2)}, ${arg(3)}, ${arg(4)}, ${cbExpr});${emitter.srcComment(e.loc)}`);
return { name: "", type: e.type };
}
case "net.serverPort":
return finish(`scr_net_server_port(${arg(0)})`);
case "net.serverAddress": {
@@ -40,28 +40,34 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
B.line(`call void @scr_net_listen(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${cb})`);
return { name: "", type: e.type };
}
if (e.fn === "net.listenOpts" || e.fn === "net.listenOptsCb") {
if (e.fn === "net.listenOpts" || e.fn === "net.listenOptsCb" ||
e.fn === "net.listenOptsReusePort" || e.fn === "net.listenOptsReusePortCb") {
// The callback slot may be the `(() => void) | undefined` optional-
// binding union: unwrap to a nullable closure.
const args = e.args.map((a) => host.emitExpr(a));
let cb = "null";
if (e.fn === "net.listenOptsCb") {
const cbT = e.args[4]!.type;
const reusePort = e.fn === "net.listenOptsReusePort" || e.fn === "net.listenOptsReusePortCb";
const withCallback = e.fn === "net.listenOptsCb" || e.fn === "net.listenOptsReusePortCb";
const cbIndex = reusePort ? 5 : 4;
if (withCallback) {
const cbT = e.args[cbIndex]!.type;
if (cbT.kind === "func") {
host.moveTemp(args[4]!);
cb = args[4]!.name;
host.moveTemp(args[cbIndex]!);
cb = args[cbIndex]!.name;
} else {
if (cbT.kind !== "union") throw new InternalCompilerError("llvm emitter bug: net.listenOptsCb callback shape");
if (cbT.kind !== "union") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback shape`);
const def = host.unionsById.get(cbT.unionId);
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
if (funcTag < 0) throw new InternalCompilerError("llvm emitter bug: net.listenOptsCb union lacks its func arm");
cb = host.unwrapNullableClosure(args[4]!.name, funcTag);
if (funcTag < 0) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its func arm`);
cb = host.unwrapNullableClosure(args[cbIndex]!.name, funcTag);
}
}
const decls = e.args.slice(0, 4).map((a) => (host.llType(a.type) === "i1" ? "i1 zeroext" : host.llType(a.type)));
host.declare(`declare void @scr_net_listen_opts(${decls.join(", ")}, ptr)`);
const valueCount = reusePort ? 5 : 4;
const decls = e.args.slice(0, valueCount).map((a) => (host.llType(a.type) === "i1" ? "i1 zeroext" : host.llType(a.type)));
const runtimeFn = reusePort ? "scr_net_listen_opts_reuse_port" : "scr_net_listen_opts";
host.declare(`declare void @${runtimeFn}(${decls.join(", ")}, ptr)`);
B.line(
`call void @scr_net_listen_opts(${args.slice(0, 4).map((a) => `${host.llType(a.type)} ${a.name}`).join(", ")}, ptr ${cb})`,
`call void @${runtimeFn}(${args.slice(0, valueCount).map((a) => `${host.llType(a.type)} ${a.name}`).join(", ")}, ptr ${cb})`,
);
return { name: "", type: e.type };
}
@@ -677,6 +677,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"sp.finished", "sp.pipeline",
"sc.text", "sc.json", "sc.buffer",
"net.listen", "net.listenCb", "net.listenOpts", "net.listenOptsCb",
"net.listenOptsReusePort", "net.listenOptsReusePortCb",
"net.connect", "net.connectCb", "net.connectLookup", "net.connectAttempt",
"fs.existsChk",
"http.createServer", "http.createServerEmpty",
@@ -954,16 +954,19 @@ function lowerNetServerMethodCall(lowerer: Lowerer, call: ts.CallExpression,
return receiverReturningCall(lowerer, fn, callArgs, NETSERVER_T, loc);
};
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
// The options-object form — listen({ port, host?, ipv6Only? }[, cb]),
// The options-object form — listen({ port, host?, ipv6Only?, reusePort? }[, cb]),
// the portless listenOnProxyInterface shape: host binds that ONE
// address (an IP literal — the runtime has no resolver here; absent
// = Node's host-less dual-stack any), ipv6Only sets IPV6_V6ONLY
// (truthiness, like Node's option handling — `boolean | undefined`
// flows). Every other key fences by name.
// flows). reusePort follows the same truthiness path, but its presence
// selects the additive reuse-port ABI even when the value is false.
// Every other key fences by name.
if (ts.isObjectLiteralExpression(args[0]!)) {
let port: IrExpr | null = null;
let host: IrExpr | null = null;
let v6only: IrExpr | null = null;
let reusePort: IrExpr | null = null;
for (const prop of (args[0] as ts.ObjectLiteralExpression).properties) {
let initializer: ts.Expression | null;
if (ts.isPropertyAssignment(prop) &&
@@ -1007,6 +1010,20 @@ function lowerNetServerMethodCall(lowerer: Lowerer, call: ts.CallExpression,
}
v6only = v;
}
} else if (key === "reusePort") {
if (initializer !== null) {
reusePort = lowerer.lowerCondition(initializer); /* truthiness: `boolean | undefined` flows */
} else {
const v = lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
if (v.type.kind !== "bool") {
lowerer.noLowering(
`a listen 'reusePort' option of '${lowerer.fmt(v.type)}' values`,
prop,
"spell the option out (reusePort: value) so non-boolean values can narrow",
);
}
reusePort = v;
}
} else if (key === "signal" && ts.isPropertyAssignment(prop) &&
isJsSourceFile(call.getSourceFile())) {
// A provably-non-AbortSignal signal (the invalid-input probes:
@@ -1032,13 +1049,13 @@ function lowerNetServerMethodCall(lowerer: Lowerer, call: ts.CallExpression,
lowerer.noLowering(
`listen option 'signal'`,
prop,
"abort-driven close has no lowering yet — port, host, and ipv6Only are the supported listen options",
"abort-driven close has no lowering yet — port, host, ipv6Only, and reusePort are the supported listen options",
);
} else {
lowerer.noLowering(
`listen option '${key}'`,
prop,
"port, host, and ipv6Only are the supported listen options",
"port, host, ipv6Only, and reusePort are the supported listen options",
);
}
}
@@ -1046,13 +1063,17 @@ function lowerNetServerMethodCall(lowerer: Lowerer, call: ts.CallExpression,
lowerer.noLowering(
"listen options without a port",
args[0]!,
"the supported options object is { port, host?, ipv6Only? } — port 0 binds an ephemeral port",
"the supported options object is { port, host?, ipv6Only?, reusePort? } — port 0 binds an ephemeral port",
);
}
host ??= { kind: "strLit", value: "", type: STRING, loc }; /* "" = the dual-stack any default */
v6only ??= boolLit(false, loc);
const listenOptsFn: IrLibFn = reusePort === null ? "net.listenOpts" : "net.listenOptsReusePort";
const listenOptsCbFn: IrLibFn = reusePort === null ? "net.listenOptsCb" : "net.listenOptsReusePortCb";
if (args.length === 1) {
return listenResult("net.listenOpts", [receiver, port, host, v6only]);
return listenResult(listenOptsFn, reusePort === null
? [receiver, port, host, v6only]
: [receiver, port, host, v6only, reusePort]);
}
// The callback may be an OPTIONAL binding — `(() => void) |
// undefined`, portless's listenOnProxyInterface pass-through: the
@@ -1085,7 +1106,9 @@ function lowerNetServerMethodCall(lowerer: Lowerer, call: ts.CallExpression,
`listen callbacks of type '${lowerer.fmt(cbV.type)}' (use () — an optional \`(() => void) | undefined\` binding also flows)`,
);
}
return listenResult("net.listenOptsCb", [receiver, port, host, v6only, cbV]);
return listenResult(listenOptsCbFn, reusePort === null
? [receiver, port, host, v6only, cbV]
: [receiver, port, host, v6only, reusePort, cbV]);
}
const port = lowerer.lowerExprExpecting(args[0]!, F64);
// The optional middle host — listen(port, '127.0.0.1'[, cb]): a
+10 -2
View File
@@ -2384,10 +2384,18 @@ export type IrLibFn =
* (portless's listenOnProxyInterface): args [server, port, host,
* ipv6Only]. host is an IP literal string ("" = the host-less
* dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind.
* Failures are the async 'error', message in Node's listen shape with
* the requested host. Never throws. */
* These are the old-shaped calls, retained for omitted reusePort so
* serialized IR remains compatible. Failures are the async 'error',
* message in Node's listen shape with the requested host. Never throws. */
| "net.listenOpts"
| "net.listenOptsCb"
/** Additive ABI for listen({ ..., reusePort }) — args
* [server, port, host, ipv6Only, reusePort], with the callback following
* those values in the callback form. The final BOOL is the requested
* kernel connection-distribution option; old spellings remain
* four/five-argument calls so serialized IR stays compatible. */
| "net.listenOptsReusePort"
| "net.listenOptsReusePortCb"
| "net.serverPort"
/** server.address() as the full AddressInfo record (the dgram.address
* materialization pattern: the emitter builds the record from the three
+8 -3
View File
@@ -348,6 +348,10 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
// The callback slot is a zero-param void closure OR its
// `(() => void) | undefined` optional-binding union (checked specially).
"net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID },
"net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID },
// The callback is a zero-param void closure OR its optional-binding union;
// the callback follows the reusePort BOOL in this additive ABI.
"net.listenOptsReusePortCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], result: VOID },
"net.serverPort": { argTypes: [NETSERVER_T], result: F64 },
// net.serverAddress's record result is shape-checked in the libCall case
// (the dgram.address sentinel pattern).
@@ -3957,8 +3961,9 @@ function validateFunction(
}
break;
}
if (e.fn === "net.listenOptsCb") {
const t = e.args[4]?.type;
if (e.fn === "net.listenOptsCb" || e.fn === "net.listenOptsReusePortCb") {
const cbIndex = e.fn === "net.listenOptsReusePortCb" ? 5 : 4;
const t = e.args[cbIndex]?.type;
const funcOk = (x: IrType | undefined): boolean =>
x?.kind === "func" && x.params.length === 0 && x.ret.kind === "void";
let ok = funcOk(t);
@@ -3968,7 +3973,7 @@ function validateFunction(
def.arms.some((a) => a.kind === "undefinedT");
}
if (!ok) {
err(`libCall net.listenOptsCb callback shape (frontend must fence)`, e.loc);
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
+101 -18
View File
@@ -107,6 +107,20 @@
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(_AIX) || defined(__sun)
#include <sys/param.h>
#endif
#endif
/* MinGW's errno headers do not expose ENOTSUP on every supported toolchain.
* Keep a stable internal value for the Node/libuv unsupported reuse-port
* result, while preserving the platform's EOPNOTSUPP spelling when present. */
#ifndef ENOTSUP
#ifdef EOPNOTSUPP
#define ENOTSUP EOPNOTSUPP
#else
#define ENOTSUP 4095
#endif
#endif
static void scr_net_oom(void) {
@@ -275,6 +289,10 @@ static const char *scr_net_errname(int err) {
case EHOSTDOWN: return "EHOSTDOWN"; /* absent from the win32 CRT */
#endif
case EINVAL: return "EINVAL";
case ENOTSUP: return "ENOTSUP";
#if defined(EOPNOTSUPP) && (!defined(ENOTSUP) || EOPNOTSUPP != ENOTSUP)
case EOPNOTSUPP: return "EOPNOTSUPP";
#endif
default: return "EUNKNOWN";
}
}
@@ -710,6 +728,62 @@ static void scr_net_listen_reuseaddr(int fd) {
#endif
}
/* Node/libuv's TCP reuse-port matrix. SO_REUSEPORT is deliberately NOT
* selected merely because a platform header happens to expose it: macOS's
* meaning is not the required TCP load-balancing contract, and Windows'
* SO_REUSEADDR is port hijacking rather than reuse-port support. FreeBSD's
* ordinary SO_REUSEPORT does not distribute connections, so FreeBSD 12+
* uses the load-balancing spelling instead. A setsockopt failure is returned
* to the caller so an unsupported kernel cannot silently become a singleton. */
static int scr_net_listen_reuseport(int fd) {
int rc;
#if defined(__FreeBSD__) && defined(__FreeBSD_version) && __FreeBSD_version >= 1200000 && defined(SO_REUSEPORT_LB)
int one = 1;
rc = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT_LB, &one, sizeof one);
#elif (defined(__linux__) || defined(_AIX73) || \
(defined(__DragonFly__) && defined(__DragonFly_version) && __DragonFly_version >= 300600) || \
(defined(__sun) && !defined(__illumos__) && defined(SO_FLOW_NAME))) && \
defined(SO_REUSEPORT)
int one = 1;
rc = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one);
#else
(void)fd;
errno = ENOTSUP;
return -1;
#endif
#if defined(__linux__) && defined(ENOPROTOOPT)
/* Linux kernels predating 3.9 expose no reuse-port TCP support. Keep
* Node's stable unsupported result instead of leaking ENOPROTOOPT. */
if (rc != 0 && errno == ENOPROTOOPT) errno = ENOTSUP;
#endif
return rc;
}
/* Shared bind setup for both host-less and explicit-interface listeners.
* SO_REUSEADDR remains first, reuse-port is requested next, and both are
* complete before bind/listen. The returned errno is the native failure. */
static int scr_net_bind_listen(int fd, const struct sockaddr *sa, socklen_t salen,
bool reuse_port) {
scr_net_listen_reuseaddr(fd);
if (reuse_port && scr_net_listen_reuseport(fd) != 0) return -1;
scr_net_nonblock(fd);
int rc = bind(fd, sa, salen);
if (rc == 0) rc = listen(fd, 511); /* Node's default backlog */
return rc;
}
static const char *scr_net_listen_reason(int err, bool explicit_host) {
if (err == EADDRINUSE) return "address already in use";
#ifdef EADDRNOTAVAIL
if (explicit_host && err == EADDRNOTAVAIL) return "address not available";
#endif
if (err == ENOTSUP) return "operation not supported";
#if defined(EOPNOTSUPP) && (!defined(ENOTSUP) || EOPNOTSUPP != ENOTSUP)
if (err == EOPNOTSUPP) return "operation not supported";
#endif
return "permission denied";
}
/* write(2) to a socket with SIGPIPE suppressed. macOS sets SO_NOSIGPIPE
* per fd above (send() path unchanged: write, exactly the historical
* call); Linux has no SO_NOSIGPIPE — MSG_NOSIGNAL on each send is the
@@ -1280,7 +1354,8 @@ ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, Scr
* 'error'); success defers the listen callback ('listening') to the next
* dispatch pass, Node's next-tick emit. Binds like Node's host-less
* listen: IPv6 any with dual-stack when the kernel allows, IPv4 fallback. */
void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullable*/) {
static void scr_net_listen_any(ScrNetServer *s, double port, bool reuse_port,
ScrClosure *cb /*moves, nullable*/) {
if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true);
if (s->listening || s->close_emitted) return;
if (!scr_net_poller_init()) {
@@ -1300,8 +1375,6 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab
fputs("scriptc: socket() failed\n", stderr);
abort();
}
scr_net_listen_reuseaddr(fd);
scr_net_nonblock(fd);
int rc;
if (v6) {
struct sockaddr_in6 addr;
@@ -1309,23 +1382,22 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
rc = bind(fd, (struct sockaddr *)&addr, sizeof addr);
rc = scr_net_bind_listen(fd, (struct sockaddr *)&addr, sizeof addr, reuse_port);
} else {
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons((uint16_t)p);
rc = bind(fd, (struct sockaddr *)&addr, sizeof addr);
rc = scr_net_bind_listen(fd, (struct sockaddr *)&addr, sizeof addr, reuse_port);
}
if (rc == 0) rc = listen(fd, 511); /* Node's default backlog */
if (rc != 0) {
int err = errno;
close(fd);
char msg[128];
/* Node: "listen EADDRINUSE: address already in use :::4000" */
snprintf(msg, sizeof msg, "listen %s: %s %s:%d", scr_net_errname(err),
err == EADDRINUSE ? "address already in use" : "permission denied",
scr_net_listen_reason(err, false),
v6 ? "::" : "0.0.0.0", p);
if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg));
scr_net_server_register(s); /* the sweep delivers the failure */
@@ -1347,7 +1419,11 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab
scr_net_server_register(s);
}
/* listen({ port, host, ipv6Only }) — the explicit-interface bind
void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullable*/) {
scr_net_listen_any(s, port, false, cb);
}
/* listen({ port, host, ipv6Only, reusePort }) — the explicit-interface bind
* (portless's listenOnProxyInterface): host is an IP literal ("" = the
* host-less dual-stack default above; "localhost" pins to 127.0.0.1, the
* connect-side divergence); ipv6Only sets IPV6_V6ONLY before the bind
@@ -1355,10 +1431,11 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab
* listener, exactly the portless pair). Failures are the async 'error'
* with Node's listen message naming the requested host; a non-IP host is
* the async getaddrinfo ENOTFOUND (no resolver in this slice). */
void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, ScrClosure *cb /*moves, nullable*/) {
static void scr_net_listen_opts_impl(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, bool reuse_port,
ScrClosure *cb /*moves, nullable*/) {
if (host == NULL || host->len == 0) {
scr_net_listen(s, port, cb);
scr_net_listen_any(s, port, reuse_port, cb);
return;
}
if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true);
@@ -1406,18 +1483,13 @@ void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/
int only = ipv6_only ? 1 : 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &only, sizeof only);
}
scr_net_listen_reuseaddr(fd);
scr_net_nonblock(fd);
int rc = bind(fd, sa, salen);
if (rc == 0) rc = listen(fd, 511); /* Node's default backlog */
int rc = scr_net_bind_listen(fd, sa, salen, reuse_port);
if (rc != 0) {
int err = errno;
close(fd);
char msg[160];
snprintf(msg, sizeof msg, "listen %s: %s %s:%d", scr_net_errname(err),
err == EADDRINUSE ? "address already in use"
: err == EADDRNOTAVAIL ? "address not available"
: "permission denied",
scr_net_listen_reason(err, true),
h, p);
if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg));
scr_net_server_register(s);
@@ -1440,6 +1512,17 @@ void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/
scr_net_server_register(s);
}
void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, ScrClosure *cb /*moves, nullable*/) {
scr_net_listen_opts_impl(s, port, host, ipv6_only, false, cb);
}
void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, bool reuse_port,
ScrClosure *cb /*moves, nullable*/) {
scr_net_listen_opts_impl(s, port, host, ipv6_only, reuse_port, cb);
}
double scr_net_server_port(ScrNetServer *s) { return (double)s->port; }
double scr_net_server_timeout_get(ScrNetServer *s, double field) {
+10 -2
View File
@@ -5317,10 +5317,18 @@ void scr_net_sock_release_v(void *p);
ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 */
void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullable*/);
/* listen({ port, host, ipv6Only }): host is an IP literal ("" = the
* dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind. */
/* listen({ port, host, ipv6Only, reusePort }): host is an IP literal ("" =
* the dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind.
* The old entry point ignores reusePort and is retained for old IR. */
void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, ScrClosure *cb /*moves, nullable*/);
/* Additive ABI for listen({ port, host, ipv6Only, reusePort }): reuse_port
* requests the platform matrix documented in scr_net.c. Unsupported
* platforms and kernels fail asynchronously; they never fall back to an
* ordinary single listener. */
void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, bool reuse_port,
ScrClosure *cb /*moves, nullable*/);
double scr_net_server_port(ScrNetServer *s); /* address().port */
/* Writable http.Server timeout property storage. `field` is the compiler
* ABI selector: timeout, keepAliveTimeout, headersTimeout, requestTimeout,
+76
View File
@@ -0,0 +1,76 @@
/* http.Server inherits net.Server.listen({ reusePort: true }) through the
* shared server handle. Fresh requests must reach both HTTP listeners on
* supported platforms; unsupported platforms report the same stable error
* code shape as Node. */
import { createServer } from "node:http";
import { get } from "node:http";
const host = "127.0.0.1";
const wanted = 64;
let receivedA = 0;
let receivedB = 0;
let completed = 0;
let finished = false;
const first = createServer((_req, res) => {
receivedA++;
res.end("a");
});
const second = createServer((_req, res) => {
receivedB++;
res.end("b");
});
function isUnsupported(error: Error): boolean {
const code = (error as NodeJS.ErrnoException).code;
return code === "ENOTSUP" || code === "EOPNOTSUPP";
}
function closeBoth(done: () => void): void {
first.close(() => second.close(done));
}
function finish(): void {
if (finished) return;
finished = true;
closeBoth(() => console.log(receivedA > 0 && receivedB > 0 ? "distributed" : "not distributed"));
}
function requestOne(port: number): void {
const request = get({ hostname: host, port, path: "/" }, (response) => {
response.on("data", () => {});
response.on("end", () => {
completed++;
if (completed === wanted) finish();
});
});
request.on("error", () => {});
}
function requestMany(port: number): void {
for (let i = 0; i < wanted; i++) requestOne(port);
}
first.on("error", (error) => {
if (finished) return;
finished = true;
if (isUnsupported(error)) console.log("unsupported true");
else console.log("unexpected true error");
});
second.on("error", (error) => {
if (finished) return;
finished = true;
first.close(() => console.log(isUnsupported(error) ? "unsupported true" : "unexpected true error"));
});
first.listen({ port: 0, host, reusePort: true }, () => {
const address = first.address();
if (address === null || typeof address === "string") {
console.log("unexpected address");
return;
}
second.listen({ port: address.port, host, reusePort: true }, () => {
requestMany(address.port);
});
});
+10 -5
View File
@@ -1,5 +1,5 @@
/* The portless listenOnProxyInterface shape: listen({ port, host,
* ipv6Only }[, cb]) — the explicit v4/v6 listener PAIR on one port
* ipv6Only, reusePort }[, cb]) — the explicit v4/v6 listener PAIR on one port
* (getProxyBindTargets' loopback targets), each family answered by its
* own server, plus the EADDRINUSE arm (same host, same port) and the
* host that exists on no interface. Ephemeral ports never print; the
@@ -9,10 +9,10 @@ import * as net from "node:net";
const v4 = net.createServer((socket) => socket.end("v4\n"));
const v6 = net.createServer((socket) => socket.end("v6\n"));
type ProxyBindTarget = { host: string; ipv6Only?: boolean };
type ProxyBindTarget = { host: string; ipv6Only?: boolean; reusePort?: boolean };
/* portless's listenOnProxyInterface, verbatim shapes: the target record
* (ipv6Only optional — `boolean | undefined` flows into the option) and
* (ipv6Only and reusePort optional — `boolean | undefined` flows into the option) and
* the OPTIONAL listener binding (`(() => void) | undefined`). */
function listenOnProxyInterface(
server: net.Server,
@@ -20,7 +20,12 @@ function listenOnProxyInterface(
target: ProxyBindTarget,
listener?: () => void
): void {
server.listen({ port, host: target.host, ipv6Only: target.ipv6Only }, listener);
server.listen({
port,
host: target.host,
ipv6Only: target.ipv6Only,
reusePort: target.reusePort,
}, listener);
}
function readFrom(host: string, port: number): Promise<string> {
@@ -52,7 +57,7 @@ async function main(port: number): Promise<void> {
});
// The OMITTED-listener call — the undefined arm of the optional
// callback flows through listenOnProxyInterface's pass-through.
listenOnProxyInterface(clash, port, { host: "127.0.0.1" });
listenOnProxyInterface(clash, port, { host: "127.0.0.1", reusePort: false });
}
listenOnProxyInterface(v4, 0, { host: "127.0.0.1" }, () => {
+3 -1
View File
@@ -16,7 +16,9 @@ first.listen(0, () => {
}
first.close(() => console.log("released"));
});
second.listen(port, () => {
// Explicit false follows the new options ABI but must retain the ordinary
// single-listener EADDRINUSE behavior.
second.listen({ port, host: "127.0.0.1", reusePort: false }, () => {
console.log("bound twice?!");
second.close();
});
+87
View File
@@ -0,0 +1,87 @@
/* Node's net.Server.listen({ reusePort: true }) contract. Linux and the
* other supported Unix targets bind two listeners to one IPv4 endpoint and
* distribute fresh connections between them. Unsupported targets report the
* stable Node error-code shape instead; no platform silently degrades to a
* single listener. */
import { connect, createServer } from "node:net";
const host = "127.0.0.1";
const wanted = 128;
let receivedA = 0;
let receivedB = 0;
let completed = 0;
let finished = false;
const first = createServer((socket) => {
receivedA++;
socket.end();
});
const second = createServer((socket) => {
receivedB++;
socket.end();
});
function isUnsupported(error: Error): boolean {
const code = (error as NodeJS.ErrnoException).code;
return code === "ENOTSUP" || code === "EOPNOTSUPP";
}
function closeBoth(done: () => void): void {
first.close(() => second.close(done));
}
function finish(): void {
if (finished) return;
finished = true;
closeBoth(() => console.log(receivedA > 0 && receivedB > 0 ? "distributed" : "not distributed"));
}
function openOne(port: number): void {
const socket = connect(port, host);
// Consume the server's FIN so the native socket arms its read side just
// as Node does for a socket with no application payload.
socket.on("data", () => {});
socket.on("error", () => {
});
socket.on("close", () => {
completed++;
if (completed === wanted) finish();
});
}
function openMany(port: number): void {
for (let i = 0; i < wanted; i++) openOne(port);
}
first.on("error", (error) => {
if (finished) return;
if (isUnsupported(error)) {
finished = true;
console.log("unsupported true");
} else {
console.log("unexpected true error");
finished = true;
}
});
second.on("error", (error) => {
if (finished) return;
if (isUnsupported(error)) {
finished = true;
first.close(() => console.log("unsupported true"));
} else {
finished = true;
first.close(() => console.log("unexpected true error"));
}
});
first.listen({ port: 0, host, reusePort: true }, () => {
const address = first.address();
if (address === null || typeof address === "string") {
console.log("unexpected address");
return;
}
second.listen({ port: address.port, host, reusePort: true }, () => {
openMany(address.port);
});
});
+25
View File
@@ -143,4 +143,29 @@ describe(`server differential (${cases.length} programs${sanitize ? ", sanitized
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
expect(nativeRes.driverStdout).toBe(nodeRes.driverStdout);
}, 120_000);
test("net-reuse-port emits the additive LLVM ABI", async () => {
const entry = join(fixturesRoot, "cases/net-reuse-port/main.ts");
const outDir = join(cacheDir, `server-llvm-reuse-port${sanitize ? "-san" : ""}`);
mkdirSync(outDir, { recursive: true });
const result = await compile(entry, {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend: "llvm",
});
if (!result.ok) {
throw new Error(
"net-reuse-port LLVM fixture failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
const llvm = readFileSync(result.cPath, "utf8");
expect(llvm).toContain(
"declare void @scr_net_listen_opts_reuse_port(ptr, double, ptr, i1 zeroext, i1 zeroext, ptr)",
);
expect(llvm).toMatch(
/call void @scr_net_listen_opts_reuse_port\(ptr [^,]+, double [^,]+, ptr [^,]+, i1 [^,]+, i1 [^,]+, ptr [^)]+\)/,
);
});
});