Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
bc67e976e4 fix: Add systemd socket activation support (fixes #22559)
- Add us_socket_context_listen_from_fd() to uws for listening on existing file descriptors
- Remove the explicit rejection of fd listening in Listener.zig
- Enable systemd socket activation by allowing servers to listen on pre-existing sockets passed via file descriptors
- This allows Bun to work with systemd-socket-activate and similar tools

The implementation adds a new function to uws that creates a listen socket from an existing file descriptor, which is exactly what systemd socket activation needs. When LISTEN_FDS environment variable is set, systemd passes listening sockets starting from fd 3, and applications can now listen on these pre-existing sockets.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 21:47:54 +00:00
8 changed files with 347 additions and 8 deletions

View File

@@ -395,6 +395,34 @@ struct us_listen_socket_t *us_socket_context_listen(int ssl, struct us_socket_co
return ls;
}
struct us_listen_socket_t *us_socket_context_listen_from_fd(int ssl, struct us_socket_context_t *context, LIBUS_SOCKET_DESCRIPTOR fd, int options, int socket_ext_size) {
#ifndef LIBUS_NO_SSL
if (ssl) {
return us_internal_ssl_socket_context_listen_from_fd((struct us_internal_ssl_socket_context_t *) context, fd, options, socket_ext_size);
}
#endif
struct us_poll_t *p = us_create_poll(context->loop, 0, sizeof(struct us_listen_socket_t));
us_poll_init(p, fd, POLL_TYPE_SEMI_SOCKET);
us_poll_start(p, context->loop, LIBUS_SOCKET_READABLE);
struct us_listen_socket_t *ls = (struct us_listen_socket_t *) p;
struct us_socket_t* s = &ls->s;
s->context = context;
s->timeout = 255;
s->long_timeout = 255;
s->flags.low_prio_state = 0;
s->flags.is_paused = 0;
s->flags.is_ipc = 0;
s->next = 0;
s->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN);
us_internal_socket_context_link_listen_socket(ssl, context, ls);
ls->socket_ext_size = socket_ext_size;
return ls;
}
struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, struct us_socket_context_t *context, const char *path, size_t pathlen, int options, int socket_ext_size, int* error) {
#ifndef LIBUS_NO_SSL
if (ssl) {

View File

@@ -1596,6 +1596,15 @@ struct us_listen_socket_t *us_internal_ssl_socket_context_listen_unix(
socket_ext_size, error);
}
struct us_listen_socket_t *us_internal_ssl_socket_context_listen_from_fd(
struct us_internal_ssl_socket_context_t *context, LIBUS_SOCKET_DESCRIPTOR fd,
int options, int socket_ext_size) {
return us_socket_context_listen_from_fd(0, &context->sc, fd, options,
sizeof(struct us_internal_ssl_socket_t) -
sizeof(struct us_socket_t) +
socket_ext_size);
}
// https://github.com/oven-sh/bun/issues/16995
static void us_internal_zero_ssl_data_for_connected_socket_before_onopen(struct us_internal_ssl_socket_t *s) {
s->ssl = NULL;

View File

@@ -408,6 +408,10 @@ struct us_listen_socket_t *us_internal_ssl_socket_context_listen_unix(
us_internal_ssl_socket_context_r context, const char *path,
size_t pathlen, int options, int socket_ext_size, int* error);
struct us_listen_socket_t *us_internal_ssl_socket_context_listen_from_fd(
us_internal_ssl_socket_context_r context, LIBUS_SOCKET_DESCRIPTOR fd,
int options, int socket_ext_size);
struct us_socket_t *us_internal_ssl_socket_context_connect(
us_internal_ssl_socket_context_r context, const char *host,
int port, int options, int socket_ext_size, int* is_resolved);

View File

@@ -315,6 +315,10 @@ struct us_listen_socket_t *us_socket_context_listen(int ssl, us_socket_context_r
struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, us_socket_context_r context,
const char *path, size_t pathlen, int options, int socket_ext_size, int* error);
/* Listen on an existing file descriptor (for systemd socket activation) */
struct us_listen_socket_t *us_socket_context_listen_from_fd(int ssl, us_socket_context_r context,
LIBUS_SOCKET_DESCRIPTOR fd, int options, int socket_ext_size);
/* listen_socket.c/.h */
void us_listen_socket_close(int ssl, struct us_listen_socket_t *ls) nonnull_fn_decl;

View File

@@ -266,14 +266,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa
break :brk socket_context.listenUnix(ssl_enabled, host, host.len, socket_flags, 8, &errno);
},
.fd => |fd| {
const err: bun.jsc.SystemError = .{
.errno = @intFromEnum(bun.sys.SystemErrno.EINVAL),
.code = .static("EINVAL"),
.message = .static("Bun does not support listening on a file descriptor."),
.syscall = .static("listen"),
.fd = fd.uv(),
};
return globalObject.throwValue(err.toErrorInstance(globalObject));
break :brk socket_context.listenFromFd(ssl_enabled, fd.uv(), socket_flags, 8);
},
}
} orelse {

View File

@@ -216,6 +216,10 @@ pub const SocketContext = opaque {
return c.us_socket_context_listen_unix(@intFromBool(ssl), this, path, pathlen, options, socket_ext_size, err);
}
pub fn listenFromFd(this: *SocketContext, ssl: bool, fd: uws.LIBUS_SOCKET_DESCRIPTOR, options: i32, socket_ext_size: i32) ?*ListenSocket {
return c.us_socket_context_listen_from_fd(@intFromBool(ssl), this, fd, options, socket_ext_size);
}
pub fn loop(this: *SocketContext, ssl: bool) ?*Loop {
return c.us_socket_context_loop(@intFromBool(ssl), this);
}
@@ -261,6 +265,7 @@ pub const c = struct {
pub extern fn us_socket_context_get_native_handle(ssl: i32, context: *SocketContext) ?*anyopaque;
pub extern fn us_socket_context_listen(ssl: i32, context: *SocketContext, host: ?[*:0]const u8, port: i32, options: i32, socket_ext_size: i32, err: *c_int) ?*ListenSocket;
pub extern fn us_socket_context_listen_unix(ssl: i32, context: *SocketContext, path: [*:0]const u8, pathlen: usize, options: i32, socket_ext_size: i32, err: *c_int) ?*ListenSocket;
pub extern fn us_socket_context_listen_from_fd(ssl: i32, context: *SocketContext, fd: uws.LIBUS_SOCKET_DESCRIPTOR, options: i32, socket_ext_size: i32) ?*ListenSocket;
pub extern fn us_socket_context_loop(ssl: i32, context: *SocketContext) ?*Loop;
pub extern fn us_socket_context_on_close(ssl: i32, context: *SocketContext, on_close: ?*const fn (*us_socket_t, i32, ?*anyopaque) callconv(.C) ?*us_socket_t) void;
pub extern fn us_socket_context_on_connect_error(ssl: i32, context: *SocketContext, on_connect_error: ?*const fn (*uws.ConnectingSocket, i32) callconv(.C) ?*uws.ConnectingSocket) void;

View File

@@ -0,0 +1,113 @@
import { test, expect } from "bun:test";
import net from "net";
import { bunEnv, bunExe } from "harness";
test("file descriptor listening support (issue #22559)", async () => {
// This test verifies that Bun no longer rejects listening on file descriptors
// Previously, it would throw "Bun does not support listening on a file descriptor"
const server = net.createServer();
// This should not throw the "does not support" error anymore
// It will fail with EBADF because fd 3 doesn't exist, but that's expected
let errorCaught = false;
let errorMessage = "";
server.on("error", (err) => {
errorCaught = true;
errorMessage = err.message;
});
server.listen({ fd: 3 });
// Wait a bit for the error to be caught
await Bun.sleep(10);
expect(errorCaught).toBe(true);
expect(errorMessage).not.toContain("does not support listening on a file descriptor");
server.close();
});
test("Bun.listen with fd parameter", async () => {
// Test that Bun.listen also accepts fd parameter
let errorCaught = false;
let errorMessage = "";
try {
const listener = Bun.listen({
fd: 3,
socket: {
data: {},
open(socket) {},
close(socket) {},
drain(socket) {},
},
});
// If we get here, it means fd listening is supported
// Close the listener if it was created
if (listener) {
listener.stop();
}
} catch (err: any) {
errorCaught = true;
errorMessage = err.message || err.toString();
}
// We expect an error because fd 3 doesn't exist, but not the "does not support" error
if (errorCaught) {
expect(errorMessage).not.toContain("does not support listening on a file descriptor");
}
});
test("systemd-socket-activate command simulation", async () => {
// This test simulates what systemd-socket-activate does
// It creates a script that listens on fd 3 when LISTEN_FDS is set
const testScript = `
const net = require("net");
// Check for systemd socket activation environment variables
const listenPid = process.env.LISTEN_PID;
const listenFds = process.env.LISTEN_FDS;
if (listenFds === "1") {
// systemd socket activation mode - listen on fd 3
const server = net.createServer((socket) => {
socket.write("systemd-activated");
socket.end();
});
server.listen({ fd: 3 }, () => {
console.log("LISTENING_ON_FD_3");
});
server.on("error", (err) => {
console.error("ERROR:", err.message);
process.exit(1);
});
} else {
console.log("NO_SYSTEMD_ACTIVATION");
process.exit(0);
}
`;
// Test with LISTEN_FDS set
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", testScript],
env: {
...bunEnv,
LISTEN_PID: "12345",
LISTEN_FDS: "1",
},
stdout: "pipe",
stderr: "pipe",
});
const output = await proc.stdout.text();
// The important thing is that it doesn't fail with "does not support listening on a file descriptor"
// It should either work or fail with a different error (like EBADF)
expect(output).toContain("LISTENING_ON_FD_3");
});

View File

@@ -0,0 +1,183 @@
import { test, expect } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import net from "net";
import { createServer as createHTTPServer } from "http";
test("systemd socket activation with net.createServer (issue #22559)", async () => {
// Create a listening socket that we'll pass as fd 3
const listenerServer = net.createServer();
await new Promise((resolve) => {
listenerServer.listen(0, "127.0.0.1", () => resolve(undefined));
});
const { port } = listenerServer.address() as net.AddressInfo;
// Create a test script that listens on fd 3
using dir = tempDir("systemd-test", {
"server.js": `
const net = require("net");
// Simulate systemd socket activation - listen on fd 3
const server = net.createServer((socket) => {
socket.write("Hello from systemd activated server\\n");
socket.end();
});
server.listen({ fd: 3 }, () => {
console.log("Server listening on fd 3");
});
// Keep the server running for the test
setTimeout(() => {}, 5000);
`,
});
// Start the child process with the socket on fd 3
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: {
...bunEnv,
LISTEN_PID: "$$", // Current process PID
LISTEN_FDS: "1", // One socket being passed
},
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
// Pass the listening socket as fd 3
stdio: ["inherit", "inherit", "inherit", listenerServer._handle.fd],
});
// Wait for the server to start
await Bun.sleep(100);
// Test connecting to the server
const client = net.createConnection(port, "127.0.0.1");
const response = await new Promise<string>((resolve, reject) => {
let data = "";
client.on("data", (chunk) => {
data += chunk.toString();
});
client.on("end", () => {
resolve(data);
});
client.on("error", reject);
});
expect(response).toBe("Hello from systemd activated server\n");
// Clean up
listenerServer.close();
proc.kill();
});
test("systemd socket activation with http.createServer", async () => {
// Create a listening socket that we'll pass as fd 3
const listenerServer = net.createServer();
await new Promise((resolve) => {
listenerServer.listen(0, "127.0.0.1", () => resolve(undefined));
});
const { port } = listenerServer.address() as net.AddressInfo;
// Create a test script that uses http server with fd
using dir = tempDir("systemd-http-test", {
"server.js": `
const http = require("http");
// Simulate systemd socket activation - listen on fd 3
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end("Hello from systemd HTTP server");
});
server.listen({ fd: 3 }, () => {
console.log("HTTP server listening on fd 3");
});
// Keep the server running for the test
setTimeout(() => {}, 5000);
`,
});
// Start the child process with the socket on fd 3
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: {
...bunEnv,
LISTEN_PID: "$$",
LISTEN_FDS: "1",
},
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
stdio: ["inherit", "inherit", "inherit", listenerServer._handle.fd],
});
// Wait for the server to start
await Bun.sleep(100);
// Test HTTP request
const response = await fetch(`http://127.0.0.1:${port}/`);
const text = await response.text();
expect(text).toBe("Hello from systemd HTTP server");
// Clean up
listenerServer.close();
proc.kill();
});
test("Bun.serve with file descriptor", async () => {
// Create a listening socket that we'll pass as fd 3
const listenerServer = net.createServer();
await new Promise((resolve) => {
listenerServer.listen(0, "127.0.0.1", () => resolve(undefined));
});
const { port } = listenerServer.address() as net.AddressInfo;
// Create a test script that uses Bun.serve with fd
using dir = tempDir("bun-serve-fd-test", {
"server.js": `
Bun.serve({
fd: 3,
fetch(req) {
return new Response("Hello from Bun.serve with fd");
},
});
console.log("Bun.serve listening on fd 3");
// Keep the server running for the test
setTimeout(() => {}, 5000);
`,
});
// Start the child process with the socket on fd 3
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: {
...bunEnv,
LISTEN_PID: "$$",
LISTEN_FDS: "1",
},
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
stdio: ["inherit", "inherit", "inherit", listenerServer._handle.fd],
});
// Wait for the server to start
await Bun.sleep(100);
// Test HTTP request
const response = await fetch(`http://127.0.0.1:${port}/`);
const text = await response.text();
expect(text).toBe("Hello from Bun.serve with fd");
// Clean up
listenerServer.close();
proc.kill();
});