diff --git a/docs/runtime/child-process.mdx b/docs/runtime/child-process.mdx index c4774c34db7d..debaff19231f 100644 --- a/docs/runtime/child-process.mdx +++ b/docs/runtime/child-process.mdx @@ -596,11 +596,12 @@ interface Subprocess extends AsyncDisposable { interface SyncSubprocess { stdout: Buffer | undefined; stderr: Buffer | undefined; - exitCode: number; + exitCode: number | null; // null if a signal ended the process success: boolean; resourceUsage: ResourceUsage; signalCode?: NodeJS.Signals | number; - exitedDueToTimeout?: true; + exitedDueToTimeout?: boolean; + exitedDueToMaxBuffer?: boolean; pid: number; } diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 8279a41e75f7..81785fee48b4 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -7259,39 +7259,72 @@ declare module "bun" { /** * IP protocol family used for the remote endpoint of the socket + * + * `undefined` when there is no remote IP address to report: the socket is not + * an IP socket (for example a Unix domain socket), it is not connected, or the + * peer has already disconnected. That can be the case in any handler, `open` + * included. Bun caches the first read, so a value read while the peer was + * connected is still there after the socket closes. * @example "IPv4" | "IPv6" */ - readonly remoteFamily: "IPv4" | "IPv6"; + readonly remoteFamily: "IPv4" | "IPv6" | undefined; /** * Remote IP address connected to the socket + * + * `undefined` when there is no remote IP address to report: the socket is not + * an IP socket (for example a Unix domain socket), it is not connected, or the + * peer has already disconnected. That can be the case in any handler, `open` + * included. Bun caches the first read, so a value read while the peer was + * connected is still there after the socket closes. * @example "192.168.1.100" | "2001:db8::1" */ - readonly remoteAddress: string; + readonly remoteAddress: string | undefined; /** * Remote port connected to the socket + * + * `undefined` when there is no remote port to report: the socket is not an IP + * socket (for example a Unix domain socket), it is not connected, or the peer + * has already disconnected. That can be the case in any handler, `open` + * included. Bun does not cache this value, so it is `undefined` after the + * socket closes. * @example 8080 */ - readonly remotePort: number; + readonly remotePort: number | undefined; /** * IP protocol family used for the local endpoint of the socket + * + * `undefined` when there is no local IP address to report: the socket is not + * an IP socket (for example a Unix domain socket), or it is not connected, as + * in `connectError` and after it closes. Bun caches the first read, so a value + * read while the socket was connected is still there after it closes. * @example "IPv4" | "IPv6" */ - readonly localFamily: "IPv4" | "IPv6"; + readonly localFamily: "IPv4" | "IPv6" | undefined; /** * Local IP address connected to the socket + * + * `undefined` when there is no local IP address to report: the socket is not + * an IP socket (for example a Unix domain socket), or it is not connected, as + * in `connectError` and after it closes. Bun caches the first read, so a value + * read while the socket was connected is still there after it closes. * @example "192.168.1.100" | "2001:db8::1" */ - readonly localAddress: string; + readonly localAddress: string | undefined; /** * Local port connected to the socket + * + * `undefined` when there is no local port to report: the socket is not + * an IP socket (for example a Unix domain socket), or it is not connected, as + * in `connectError` and after it closes. Bun caches the first read, so a value + * read while the socket was connected is still there after it closes. * @example 8080 */ - readonly localPort: number; + readonly localPort: number | undefined; /** * `true` if the peer certificate was signed by one of the CAs @@ -7392,7 +7425,12 @@ declare module "bun" { * @return A certificate object. */ getPeerCertificate(): import("node:tls").PeerCertificate; - getPeerX509Certificate(): import("node:crypto").X509Certificate; + /** + * Returns the peer's certificate as an `X509Certificate`. + * + * `undefined` if the peer sent no certificate, or if the socket has closed. + */ + getPeerX509Certificate(): import("node:crypto").X509Certificate | undefined; /** * See [SSL\_get\_shared\_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html) for more information. @@ -7434,7 +7472,7 @@ declare module "bun" { * Returns a string containing the negotiated SSL/TLS protocol version of the * current connection. The value `'unknown'` is returned for connected * sockets that have not completed the handshaking process. The value `null` is - * returned for server sockets or disconnected client sockets. + * returned for a socket without TLS and for a socket that has closed. * * Protocol versions are: * @@ -7445,7 +7483,7 @@ declare module "bun" { * * `'TLSv1.3'` * */ - getTLSVersion(): string; + getTLSVersion(): string | null; /** * **TLS only:** Checks if the current TLS session was resumed from a previous session. @@ -7575,9 +7613,12 @@ declare module "bun" { /** * Returns the servername of the socket. * + * `undefined` for a socket without TLS, for a socket that has closed, and + * when the client sent no server name. + * * @see {@link setServername} */ - getServername(): string; + getServername(): string | undefined; /** * Sets the servername of the socket. @@ -8620,7 +8661,7 @@ declare module "bun" { /** * Synchronously get the exit code of the process * - * `null` if the process hasn't exited yet + * `null` if the process hasn't exited yet, or if a signal ended it */ readonly exitCode: number | null; @@ -8697,7 +8738,14 @@ declare module "bun" { > { stdout: SpawnOptions.ReadableToSyncIO; stderr: SpawnOptions.ReadableToSyncIO; - exitCode: number; + /** + * The exit code of the process + * + * `null` if a signal ended the process. {@link SyncSubprocess.signalCode} is + * then the signal. The `timeout` and `maxBuffer` options send a signal + * (`killSignal`) to stop the process. + */ + exitCode: number | null; success: boolean; /** * Resource usage of the process, such as max RSS and CPU time diff --git a/packages/bun-types/serve.d.ts b/packages/bun-types/serve.d.ts index 5cad6313351f..ed17176461f1 100644 --- a/packages/bun-types/serve.d.ts +++ b/packages/bun-types/serve.d.ts @@ -265,12 +265,18 @@ declare module "bun" { /** * The IP address of the client. * + * `undefined` when there is no IP address to report: the connection does not + * use an IP socket (`Bun.serve({ unix })`), it has closed, or the client has + * already disconnected. That can be the case in any handler, `open` included. + * Bun caches the first read, so a value read while the client was connected + * is still there in `close`. + * * @example * ```ts * console.log(socket.remoteAddress); // "127.0.0.1" * ``` */ - readonly remoteAddress: string; + readonly remoteAddress: string | undefined; /** * The ready state of the client. diff --git a/test/bundler/compile-trim-features.ts b/test/bundler/compile-trim-features.ts index d0c8683d6151..af3f819d67fb 100644 --- a/test/bundler/compile-trim-features.ts +++ b/test/bundler/compile-trim-features.ts @@ -64,7 +64,7 @@ const table: TrimTable | undefined = readTableOfThisBun(); export const canTrim: boolean = table !== undefined; /** What a build with --trim says. */ -export function probeTrim(): { stderr: string; exitCode: number } { +export function probeTrim(): { stderr: string; exitCode: number | null } { using probe = tempDir("compile-trim-probe", { "hello.ts": 'console.log("hello world");' }); const probed = Bun.spawnSync({ cmd: [bunExe(), "build", "--compile", "--trim", "hello.ts", "--outfile", "hello"], diff --git a/test/cli/lint/oracle/react-compiler/expected.ts b/test/cli/lint/oracle/react-compiler/expected.ts index 0e4fa74e5922..e0f9f5aa6611 100644 --- a/test/cli/lint/oracle/react-compiler/expected.ts +++ b/test/cli/lint/oracle/react-compiler/expected.ts @@ -133,7 +133,7 @@ function inDirectory(files: Files, then: (stdout: string, exit: number, direc writeFileSync(join(directory, name), text); } const result = spawn(["-f", "json"], directory); - const found = then(result.stdout.toString(), result.exitCode, directory); + const found = then(result.stdout.toString(), result.exitCode ?? -1, directory); if (found === null) throw new Error(`No report (exit ${result.exitCode}): ${result.stderr.toString().slice(0, 1000)}`); return found; diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 47e6ab29c5fc..b99ba7ebc309 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -691,8 +691,6 @@ describe("@types/bun integration test", () => { "resolve_message.bind.ts: new ResolveMessage().[Symbol.toPrimitive] is not published", // As for `match` of FileSystemRouter. "response.bind.ts: new Request().clone() has no keepalive, duplex", - // `undefined` once the socket is closed. - "server.bind.ts: new ServerWebSocket().remoteAddress is string | undefined, not string", // `pendingDeclarations` in test/internal/source-lints/redis-client-types.test.ts. "valkey.bind.ts: new RedisClient().psubscribe is not published", "valkey.bind.ts: new RedisClient().pubsub is not published", diff --git a/test/integration/bun-types/fixture/spawn.ts b/test/integration/bun-types/fixture/spawn.ts index fc850e6b6a1f..038ec993023c 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -188,6 +188,10 @@ tsd.expectAssignable(Bun.spawn([], { stdio: [null, null, null] } tsd.expectAssignable>(Bun.spawnSync([], {})); +// `null` when a signal ended the process. +tsd.expectType(Bun.spawnSync(["sleep", "10"], { timeout: 1 }).exitCode).is(); +tsd.expectType(Bun.spawnSync({ cmd: ["sleep", "10"], timeout: 1 }).exitCode).is(); + // Lazy option types (async only) { // valid: lazy usable with async spawn diff --git a/test/integration/bun-types/fixture/tcp.ts b/test/integration/bun-types/fixture/tcp.ts index f3d0094c43aa..6737b9d1d5ad 100644 --- a/test/integration/bun-types/fixture/tcp.ts +++ b/test/integration/bun-types/fixture/tcp.ts @@ -1,4 +1,6 @@ import * as Bun from "bun"; +import type { X509Certificate } from "node:crypto"; +import { expectType } from "./utilities"; await Bun.connect({ data: { arg: "asdf" }, @@ -166,3 +168,23 @@ await Bun.connect({ hostname: "localhost", port: 1, }); + +// A Unix domain socket has no IP address, and a socket that is not connected has none to read. +// A socket without TLS has no TLS version, no server name and no peer certificate. +Bun.listen({ + socket: { + open(socket) { + expectType(socket.remoteFamily).is<"IPv4" | "IPv6" | undefined>(); + expectType(socket.remoteAddress).is(); + expectType(socket.remotePort).is(); + expectType(socket.localFamily).is<"IPv4" | "IPv6" | undefined>(); + expectType(socket.localAddress).is(); + expectType(socket.localPort).is(); + expectType(socket.getTLSVersion()).is(); + expectType(socket.getServername()).is(); + expectType(socket.getPeerX509Certificate()).is(); + }, + data() {}, + }, + unix: "asdf", +}); diff --git a/test/integration/bun-types/fixture/websocket.ts b/test/integration/bun-types/fixture/websocket.ts index 46e94fc02bba..58c840be7696 100644 --- a/test/integration/bun-types/fixture/websocket.ts +++ b/test/integration/bun-types/fixture/websocket.ts @@ -346,3 +346,15 @@ import { expectType } from "./utilities"; expectType(corked).is>(); }); } + +// A connection over a Unix domain socket has no IP address, and one that has closed has none to read. +Bun.serve({ + unix: "asdf", + fetch: (request, server) => (server.upgrade(request) ? undefined : new Response("not a WebSocket")), + websocket: { + message() {}, + close(ws) { + expectType(ws.remoteAddress).is(); + }, + }, +}); diff --git a/test/internal/source-lints/host-export-callers.test.ts b/test/internal/source-lints/host-export-callers.test.ts index f4c27f24ac00..ea638c48f560 100644 --- a/test/internal/source-lints/host-export-callers.test.ts +++ b/test/internal/source-lints/host-export-callers.test.ts @@ -76,8 +76,10 @@ function gitGrep(args: string[]): string[] { stdout: "pipe", stderr: "pipe", }); - // Exit code 1 is "no match", anything above is a real failure. - if (r.exitCode > 1) throw new Error(`git grep failed (${r.exitCode}): ${r.stderr.toString()}`); + // Exit code 1 is "no match". Any other code but 0 is a real failure, and so is a signal (`null`). + if (r.exitCode !== 0 && r.exitCode !== 1) { + throw new Error(`git grep failed (${r.exitCode ?? r.signalCode}): ${r.stderr.toString()}`); + } return r.stdout.toString().split("\n").filter(Boolean); } diff --git a/test/js/bun/http/bun-connect-x509.test.ts b/test/js/bun/http/bun-connect-x509.test.ts index 9aad2c579006..1c031da7a5d6 100644 --- a/test/js/bun/http/bun-connect-x509.test.ts +++ b/test/js/bun/http/bun-connect-x509.test.ts @@ -19,7 +19,7 @@ describe("bun.connect", () => { }, }); await defer.promise; - const x509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate(); + const x509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate()!; expect(x509.checkHost("example.com")).toBe("example.com"); }); @@ -55,7 +55,7 @@ describe("bun.connect", () => { void client; const socket = await defer.promise; expect(socket.authorized).toBe(false); - const peerX509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate(); + const peerX509: import("node:crypto").X509Certificate = socket.getPeerX509Certificate()!; expect(peerX509.checkHost("localhost")).toBe("localhost"); }); @@ -95,7 +95,7 @@ describe("bun.connect", () => { using client = await defer2.promise; function check() { const x509: import("node:crypto").X509Certificate = server.getX509Certificate()!; - const peerX509: import("node:crypto").X509Certificate = client.getPeerX509Certificate(); + const peerX509: import("node:crypto").X509Certificate = client.getPeerX509Certificate()!; expect(x509.checkHost("localhost")).toBe("localhost"); expect(peerX509.checkHost("localhost")).toBe("localhost"); } diff --git a/test/js/bun/net/socket-dns-error.test.ts b/test/js/bun/net/socket-dns-error.test.ts index 19d275cd19b6..82999279d0a6 100644 --- a/test/js/bun/net/socket-dns-error.test.ts +++ b/test/js/bun/net/socket-dns-error.test.ts @@ -109,7 +109,7 @@ test.each([ ["12\t7.0.0.1", "ENOTFOUND"], ])("Bun.connect to %j: %s", async (hostname, expected) => { // On every address, so that a connection to 127.1.0.0 also arrives. - const dialed: string[] = []; + const dialed: (string | undefined)[] = []; using listener = Bun.listen({ hostname: "0.0.0.0", port: 0, diff --git a/test/js/bun/spawn/spawn-kill-signal.test.ts b/test/js/bun/spawn/spawn-kill-signal.test.ts index bda46aad3034..3016c21eef33 100644 --- a/test/js/bun/spawn/spawn-kill-signal.test.ts +++ b/test/js/bun/spawn/spawn-kill-signal.test.ts @@ -143,7 +143,7 @@ describe.concurrent.skipIf(isWindows)("signal names map to the OS's own numbers" timeout: 1, killSignal: number, }); - expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: name }); + expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: name }); }); }); }); @@ -200,6 +200,6 @@ describe.concurrent.skipIf(!isLinux)("a signal with no name", () => { test("spawnSync reports the number too", () => { const { exitCode, signalCode } = Bun.spawnSync({ cmd: ["sh", "-c", "kill -40 $$"], ...quiet }); - expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: 40 }); + expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: 40 }); }); }); diff --git a/test/js/bun/spawn/spawn-maxbuf.test.ts b/test/js/bun/spawn/spawn-maxbuf.test.ts index 12cd7a55a924..1dc6ce77fde6 100644 --- a/test/js/bun/spawn/spawn-maxbuf.test.ts +++ b/test/js/bun/spawn/spawn-maxbuf.test.ts @@ -46,7 +46,7 @@ describe("yes is killed", () => { stdio: ["pipe", "pipe", "pipe"], }); expect(proc.exitedDueToMaxBuffer).toBe(true); - expect(proc.exitCode as number | null).toBe(null); + expect(proc.exitCode).toBe(null); expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP"); const timeEnd = Date.now(); expect(timeEnd - timeStart).toBeLessThan(killWindow); @@ -234,7 +234,7 @@ describe("timeout kills the process", () => { stdio: ["pipe", "pipe", "pipe"], }); expect(proc.exitedDueToTimeout).toBe(true); - expect(proc.exitCode as number | null).toBe(null); + expect(proc.exitCode).toBe(null); expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP"); const timeEnd = Date.now(); // The timeout deadline is CLOCK_MONOTONIC at nanosecond precision, so by the diff --git a/test/js/bun/websocket/websocket-server.test.ts b/test/js/bun/websocket/websocket-server.test.ts index 93060d659170..3a124d929b7d 100644 --- a/test/js/bun/websocket/websocket-server.test.ts +++ b/test/js/bun/websocket/websocket-server.test.ts @@ -853,7 +853,7 @@ describe("ServerWebSocket", () => { })); test("remoteAddress", done => ({ open(ws) { - expect(isIP(ws.remoteAddress)).toBeGreaterThan(0); + expect(isIP(ws.remoteAddress ?? "")).toBeGreaterThan(0); done(); }, }));