mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
feat(net): support reusePort server listen options
Co-authored-by: Flora90001 <163703040+Flora90001@users.noreply.github.com>
This commit is contained in:
+3
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 [^)]+\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user