Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/runtime/child-process.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
72 changes: 60 additions & 12 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
*
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -8697,7 +8738,14 @@ declare module "bun" {
> {
stdout: SpawnOptions.ReadableToSyncIO<Out>;
stderr: SpawnOptions.ReadableToSyncIO<Err>;
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
Expand Down
8 changes: 7 additions & 1 deletion packages/bun-types/serve.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion test/bundler/compile-trim-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion test/cli/lint/oracle/react-compiler/expected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function inDirectory<T>(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;
Expand Down
2 changes: 0 additions & 2 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions test/integration/bun-types/fixture/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ tsd.expectAssignable<NullSubprocess>(Bun.spawn([], { stdio: [null, null, null] }

tsd.expectAssignable<SyncSubprocess<Bun.SpawnOptions.Readable, Bun.SpawnOptions.Readable>>(Bun.spawnSync([], {}));

// `null` when a signal ended the process.
tsd.expectType(Bun.spawnSync(["sleep", "10"], { timeout: 1 }).exitCode).is<number | null>();
tsd.expectType(Bun.spawnSync({ cmd: ["sleep", "10"], timeout: 1 }).exitCode).is<number | null>();

// Lazy option types (async only)
{
// valid: lazy usable with async spawn
Expand Down
22 changes: 22 additions & 0 deletions test/integration/bun-types/fixture/tcp.ts
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down Expand Up @@ -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<string | undefined>();
expectType(socket.remotePort).is<number | undefined>();
expectType(socket.localFamily).is<"IPv4" | "IPv6" | undefined>();
expectType(socket.localAddress).is<string | undefined>();
expectType(socket.localPort).is<number | undefined>();
expectType(socket.getTLSVersion()).is<string | null>();
expectType(socket.getServername()).is<string | undefined>();
expectType(socket.getPeerX509Certificate()).is<X509Certificate | undefined>();
},
data() {},
},
unix: "asdf",
});
12 changes: 12 additions & 0 deletions test/integration/bun-types/fixture/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,15 @@ import { expectType } from "./utilities";
expectType(corked).is<Bun.ServerWebSocket<{ id: number }>>();
});
}

// 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<string | undefined>();
},
},
});
6 changes: 4 additions & 2 deletions test/internal/source-lints/host-export-callers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions test/js/bun/http/bun-connect-x509.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down Expand Up @@ -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");
});

Expand Down Expand Up @@ -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");
}
Expand Down
2 changes: 1 addition & 1 deletion test/js/bun/net/socket-dns-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions test/js/bun/spawn/spawn-kill-signal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>({ exitCode: null, signalCode: name });
expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: name });
});
});
});
Expand Down Expand Up @@ -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<unknown>({ exitCode: null, signalCode: 40 });
expect({ exitCode, signalCode }).toEqual({ exitCode: null, signalCode: 40 });
});
});
4 changes: 2 additions & 2 deletions test/js/bun/spawn/spawn-maxbuf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/js/bun/websocket/websocket-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ describe("ServerWebSocket", () => {
}));
test("remoteAddress", done => ({
open(ws) {
expect(isIP(ws.remoteAddress)).toBeGreaterThan(0);
expect(isIP(ws.remoteAddress ?? "")).toBeGreaterThan(0);
done();
},
}));
Expand Down
Loading