mirror of
https://github.com/oven-sh/bun
synced 2026-02-08 01:49:33 +00:00
Compare commits
1 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ea89c31e |
@@ -909,6 +909,16 @@ struct SignalHandleValue {
|
||||
};
|
||||
static HashMap<int, SignalHandleValue>* signalToContextIdsMap = nullptr;
|
||||
|
||||
#if OS(WINDOWS)
|
||||
extern "C" bool Bun__hasSIGINTHandler()
|
||||
{
|
||||
if (!signalToContextIdsMap) {
|
||||
return false;
|
||||
}
|
||||
return signalToContextIdsMap->contains(SIGINT);
|
||||
}
|
||||
#endif
|
||||
|
||||
static const NeverDestroyed<String>* getSignalNames()
|
||||
{
|
||||
static const NeverDestroyed<String> signalNames[] = {
|
||||
|
||||
@@ -423,10 +423,17 @@ extern "C" void onExitSignal(int sig)
|
||||
|
||||
#if OS(WINDOWS)
|
||||
extern "C" void Bun__restoreWindowsStdio();
|
||||
extern "C" bool Bun__hasSIGINTHandler();
|
||||
|
||||
BOOL WINAPI Ctrlhandler(DWORD signal)
|
||||
{
|
||||
|
||||
if (signal == CTRL_C_EVENT) {
|
||||
// If there's a JavaScript SIGINT handler registered, let libuv handle it
|
||||
// Return TRUE to prevent Windows from terminating the process
|
||||
if (Bun__hasSIGINTHandler()) {
|
||||
return TRUE;
|
||||
}
|
||||
// No handler registered, restore stdio and let Windows terminate
|
||||
Bun__restoreWindowsStdio();
|
||||
SetConsoleCtrlHandler(Ctrlhandler, FALSE);
|
||||
}
|
||||
|
||||
@@ -623,6 +623,11 @@ pub const PathLike = union(enum) {
|
||||
const s = this.slice();
|
||||
const b = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(b);
|
||||
// Device paths (\\.\, \\?\) and NT object paths (\??\) should not be normalized
|
||||
// because the "." in \\.\pipe\name would be incorrectly stripped as a "current directory" component.
|
||||
if (s.len >= 4 and bun.path.isSepAny(s[0]) and bun.path.isSepAny(s[1]) and (s[2] == '.' or s[2] == '?') and bun.path.isSepAny(s[3])) {
|
||||
return strings.toKernel32Path(@alignCast(std.mem.bytesAsSlice(u16, buf)), s);
|
||||
}
|
||||
if (s.len > 0 and bun.path.isSepAny(s[0])) {
|
||||
const resolve = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBuf(buf, s) catch @panic("Error while resolving path.");
|
||||
const normal = path_handler.normalizeBuf(resolve, b, .windows);
|
||||
|
||||
@@ -1244,84 +1244,94 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
// If you're doing Bun.write(), try to go fast by writing short input on the main thread.
|
||||
// This is a heuristic, but it's a good one.
|
||||
//
|
||||
// except if you're on Windows. Windows I/O is slower. Let's not even try.
|
||||
if (comptime !Environment.isWindows) {
|
||||
if (path_or_blob == .path or
|
||||
// On Windows, we only use the fast path for file descriptor writes (like stdout/stderr)
|
||||
// because path-based writes need async I/O for mkdirp support, and async writes to
|
||||
// the same fd can complete out of order causing scrambled output (issue #11117).
|
||||
// For file descriptors, we use synchronous WriteFile which maintains ordering.
|
||||
const can_use_fast_path = if (comptime Environment.isWindows)
|
||||
// On Windows, only use fast path for fd-based writes (not path-based)
|
||||
(path_or_blob == .blob and
|
||||
path_or_blob.blob.store != null and
|
||||
path_or_blob.blob.store.?.data == .file and
|
||||
path_or_blob.blob.store.?.data.file.pathlike == .fd and
|
||||
path_or_blob.blob.offset == 0 and !path_or_blob.blob.isS3())
|
||||
else
|
||||
(path_or_blob == .path or
|
||||
// If they try to set an offset, its a little more complicated so let's avoid that
|
||||
(path_or_blob.blob.offset == 0 and !path_or_blob.blob.isS3() and
|
||||
// Is this a file that is known to be a pipe? Let's avoid blocking the main thread on it.
|
||||
!(path_or_blob.blob.store != null and
|
||||
path_or_blob.blob.store.?.data == .file and
|
||||
path_or_blob.blob.store.?.data.file.mode != 0 and
|
||||
bun.isRegularFile(path_or_blob.blob.store.?.data.file.mode))))
|
||||
{
|
||||
if (data.isString()) {
|
||||
const len = try data.getLength(globalThis);
|
||||
bun.isRegularFile(path_or_blob.blob.store.?.data.file.mode))));
|
||||
|
||||
if (len < 256 * 1024) {
|
||||
const str = try data.toBunString(globalThis);
|
||||
defer str.deref();
|
||||
if (can_use_fast_path) {
|
||||
if (data.isString()) {
|
||||
const len = try data.getLength(globalThis);
|
||||
|
||||
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
|
||||
path_or_blob.path
|
||||
else
|
||||
path_or_blob.blob.store.?.data.file.pathlike;
|
||||
if (len < 256 * 1024) {
|
||||
const str = try data.toBunString(globalThis);
|
||||
defer str.deref();
|
||||
|
||||
if (pathlike == .path) {
|
||||
const result = writeStringToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
str,
|
||||
&needs_async,
|
||||
true,
|
||||
);
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = writeStringToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
str,
|
||||
&needs_async,
|
||||
false,
|
||||
);
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
|
||||
path_or_blob.path
|
||||
else
|
||||
path_or_blob.blob.store.?.data.file.pathlike;
|
||||
|
||||
if (pathlike == .path) {
|
||||
const result = writeStringToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
str,
|
||||
&needs_async,
|
||||
true,
|
||||
);
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = writeStringToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
str,
|
||||
&needs_async,
|
||||
false,
|
||||
);
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (data.asArrayBuffer(globalThis)) |buffer_view| {
|
||||
if (buffer_view.byte_len < 256 * 1024) {
|
||||
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
|
||||
path_or_blob.path
|
||||
else
|
||||
path_or_blob.blob.store.?.data.file.pathlike;
|
||||
}
|
||||
} else if (data.asArrayBuffer(globalThis)) |buffer_view| {
|
||||
if (buffer_view.byte_len < 256 * 1024) {
|
||||
const pathlike: jsc.Node.PathOrFileDescriptor = if (path_or_blob == .path)
|
||||
path_or_blob.path
|
||||
else
|
||||
path_or_blob.blob.store.?.data.file.pathlike;
|
||||
|
||||
if (pathlike == .path) {
|
||||
const result = writeBytesToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
buffer_view.byteSlice(),
|
||||
&needs_async,
|
||||
true,
|
||||
);
|
||||
if (pathlike == .path) {
|
||||
const result = writeBytesToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
buffer_view.byteSlice(),
|
||||
&needs_async,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = writeBytesToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
buffer_view.byteSlice(),
|
||||
&needs_async,
|
||||
false,
|
||||
);
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
const result = writeBytesToFileFast(
|
||||
globalThis,
|
||||
pathlike,
|
||||
buffer_view.byteSlice(),
|
||||
&needs_async,
|
||||
false,
|
||||
);
|
||||
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
if (!needs_async) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1705,7 +1715,7 @@ fn writeBytesToFileFast(
|
||||
|
||||
if (truncate) {
|
||||
if (Environment.isWindows) {
|
||||
_ = std.os.windows.kernel32.SetEndOfFile(fd.cast());
|
||||
_ = bun.windows.SetEndOfFile(fd.cast());
|
||||
} else {
|
||||
_ = bun.sys.ftruncate(fd, @as(i64, @intCast(written)));
|
||||
}
|
||||
|
||||
@@ -1712,8 +1712,59 @@ pub const BunXFastPath = struct {
|
||||
var i: usize = 0;
|
||||
for (passthrough) |str| {
|
||||
command_line[i] = ' ';
|
||||
const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[1 + i ..], str);
|
||||
i += result.len + 1;
|
||||
i += 1;
|
||||
// Check if argument needs quoting (empty, contains spaces, quotes, or other special chars)
|
||||
const needs_quote = str.len == 0 or for (str) |c| {
|
||||
if (c == ' ' or c == '\t' or c == '"' or c == '\\') break true;
|
||||
} else false;
|
||||
|
||||
if (needs_quote) {
|
||||
command_line[i] = '"';
|
||||
i += 1;
|
||||
// Copy argument, escaping quotes and backslashes before quotes
|
||||
var j: usize = 0;
|
||||
while (j < str.len) {
|
||||
// Count backslashes
|
||||
var num_backslashes: usize = 0;
|
||||
while (j + num_backslashes < str.len and str[j + num_backslashes] == '\\') {
|
||||
num_backslashes += 1;
|
||||
}
|
||||
if (j + num_backslashes >= str.len) {
|
||||
// Backslashes at end: double them (they precede the closing quote)
|
||||
for (0..num_backslashes * 2) |_| {
|
||||
command_line[i] = '\\';
|
||||
i += 1;
|
||||
}
|
||||
j += num_backslashes;
|
||||
} else if (str[j + num_backslashes] == '"') {
|
||||
// Backslashes followed by quote: double backslashes + escape quote
|
||||
for (0..num_backslashes * 2 + 1) |_| {
|
||||
command_line[i] = '\\';
|
||||
i += 1;
|
||||
}
|
||||
command_line[i] = '"';
|
||||
i += 1;
|
||||
j += num_backslashes + 1;
|
||||
} else {
|
||||
// Backslashes not followed by quote: keep as-is
|
||||
for (0..num_backslashes) |_| {
|
||||
command_line[i] = '\\';
|
||||
i += 1;
|
||||
}
|
||||
j += num_backslashes;
|
||||
if (j < str.len) {
|
||||
const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[i..], str[j .. j + 1]);
|
||||
i += result.len;
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
command_line[i] = '"';
|
||||
i += 1;
|
||||
} else {
|
||||
const result = bun.strings.convertUTF8toUTF16InBuffer(command_line[i..], str);
|
||||
i += result.len;
|
||||
}
|
||||
}
|
||||
ctx.passthrough = passthrough;
|
||||
|
||||
|
||||
@@ -482,8 +482,9 @@ function ClientRequest(input, options, cb) {
|
||||
return this[kFetchRequest];
|
||||
};
|
||||
|
||||
if (isIP(host) || !options.lookup) {
|
||||
// Don't need to bother with lookup if it's already an IP address or no lookup function is provided.
|
||||
if (this[kSocketPath] || isIP(host) || !options.lookup) {
|
||||
// Don't need to bother with lookup if it's already an IP address, no lookup function is provided,
|
||||
// or if socketPath is specified (connecting via Unix domain socket / named pipe).
|
||||
const [url, proxy] = getURL(host);
|
||||
go(url, proxy, false);
|
||||
return true;
|
||||
|
||||
@@ -187,10 +187,89 @@ function onDataIncomingMessage(
|
||||
}
|
||||
}
|
||||
|
||||
// Header handling helpers for parser compatibility
|
||||
// Based on Node.js lib/_http_incoming.js
|
||||
function matchKnownFields(field: string) {
|
||||
const low = field.toLowerCase();
|
||||
// Check for fields that should be unique (not joined with comma)
|
||||
switch (low) {
|
||||
case 'content-length':
|
||||
case 'content-type':
|
||||
case 'user-agent':
|
||||
case 'referer':
|
||||
case 'host':
|
||||
case 'authorization':
|
||||
case 'proxy-authorization':
|
||||
case 'if-modified-since':
|
||||
case 'if-unmodified-since':
|
||||
case 'from':
|
||||
case 'location':
|
||||
case 'max-forwards':
|
||||
case 'retry-after':
|
||||
case 'etag':
|
||||
case 'last-modified':
|
||||
case 'server':
|
||||
case 'age':
|
||||
case 'expires':
|
||||
return [low, false]; // Don't join duplicates
|
||||
case 'set-cookie':
|
||||
return [low, 'array']; // Store as array
|
||||
default:
|
||||
return [low, true]; // Join duplicates with comma
|
||||
}
|
||||
}
|
||||
|
||||
function _addHeaderLine(this: any, field: string, value: string, dest: any) {
|
||||
const [key, joinDuplicates] = matchKnownFields(field);
|
||||
|
||||
if (joinDuplicates === 'array') {
|
||||
// set-cookie is always an array
|
||||
if (dest[key] !== undefined) {
|
||||
dest[key].push(value);
|
||||
} else {
|
||||
dest[key] = [value];
|
||||
}
|
||||
} else if (joinDuplicates) {
|
||||
// Join with comma
|
||||
if (dest[key] !== undefined) {
|
||||
dest[key] += ', ' + value;
|
||||
} else {
|
||||
dest[key] = value;
|
||||
}
|
||||
} else {
|
||||
// Don't join - first value wins
|
||||
if (dest[key] === undefined) {
|
||||
dest[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _addHeaderLines(this: any, headers: string[], n: number) {
|
||||
if (!headers || !headers.length) return;
|
||||
|
||||
// Initialize headers object if needed
|
||||
if (!this.headers) {
|
||||
this.headers = Object.create(null);
|
||||
}
|
||||
if (!this.rawHeaders) {
|
||||
this.rawHeaders = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i += 2) {
|
||||
const field = headers[i];
|
||||
const value = headers[i + 1];
|
||||
_addHeaderLine.$call(this, field, value, this.headers);
|
||||
this.rawHeaders.push(field, value);
|
||||
}
|
||||
}
|
||||
|
||||
const IncomingMessagePrototype = {
|
||||
constructor: IncomingMessage,
|
||||
__proto__: Readable.prototype,
|
||||
httpVersion: "1.1",
|
||||
// Parser compatibility methods
|
||||
_addHeaderLine: _addHeaderLine,
|
||||
_addHeaderLines: _addHeaderLines,
|
||||
_construct(callback) {
|
||||
// TODO: streaming
|
||||
const type = this[typeSymbol];
|
||||
|
||||
@@ -58,6 +58,8 @@ const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild"
|
||||
|
||||
const kServerResponse = Symbol("ServerResponse");
|
||||
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
|
||||
const kRealListenNamedPipe = Symbol("kRealListenNamedPipe");
|
||||
const kNamedPipeServer = Symbol("kNamedPipeServer");
|
||||
const GlobalPromise = globalThis.Promise;
|
||||
const kEmptyBuffer = Buffer.alloc(0);
|
||||
const ObjectKeys = Object.keys;
|
||||
@@ -65,6 +67,23 @@ const MathMin = Math.min;
|
||||
|
||||
let cluster;
|
||||
|
||||
// Helper to detect Windows named pipes (\\.\pipe\name or \\?\pipe\name)
|
||||
function isWindowsNamedPipe(path: string): boolean {
|
||||
if (process.platform !== "win32" || path.length <= 9) {
|
||||
return false;
|
||||
}
|
||||
const isSep = (c: string) => c === "/" || c === "\\";
|
||||
return (
|
||||
isSep(path[0]) &&
|
||||
isSep(path[1]) &&
|
||||
(path[2] === "." || path[2] === "?") &&
|
||||
isSep(path[3]) &&
|
||||
path.slice(4, 8).toLowerCase() === "pipe" &&
|
||||
isSep(path[8]) &&
|
||||
!isSep(path[9])
|
||||
);
|
||||
}
|
||||
|
||||
function emitCloseServer(self: Server) {
|
||||
callCloseCallback(self);
|
||||
self.emit("close");
|
||||
@@ -465,6 +484,12 @@ Server.prototype.listen = function () {
|
||||
};
|
||||
|
||||
Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort, onListen) {
|
||||
// On Windows, named pipes need special handling since Bun.serve() doesn't support them.
|
||||
// We use node:net.Server which uses Bun.listen() internally and supports named pipes.
|
||||
if (socketPath && isWindowsNamedPipe(socketPath)) {
|
||||
return this[kRealListenNamedPipe](tls, socketPath, onListen);
|
||||
}
|
||||
|
||||
{
|
||||
const ResponseClass = this[optionsSymbol].ServerResponse || ServerResponse;
|
||||
const RequestClass = this[optionsSymbol].IncomingMessage || IncomingMessage;
|
||||
@@ -739,6 +764,268 @@ Server.prototype.setTimeout = function (msecs, callback) {
|
||||
return this;
|
||||
};
|
||||
|
||||
// Simple ServerResponse for named pipes that writes directly to socket
|
||||
class NamedPipeServerResponse extends Stream.Writable {
|
||||
statusCode = 200;
|
||||
statusMessage = "OK";
|
||||
headersSent = false;
|
||||
finished = false;
|
||||
socket: any;
|
||||
_headers: Map<string, string[]> = new Map();
|
||||
shouldKeepAlive = false;
|
||||
sendDate = true;
|
||||
req: any;
|
||||
|
||||
constructor(req: any, socket: any) {
|
||||
super();
|
||||
this.req = req;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
setHeader(name: string, value: string | string[]) {
|
||||
const key = name.toLowerCase();
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
this._headers.set(key, values);
|
||||
}
|
||||
|
||||
getHeader(name: string) {
|
||||
const values = this._headers.get(name.toLowerCase());
|
||||
return values ? (values.length === 1 ? values[0] : values) : undefined;
|
||||
}
|
||||
|
||||
hasHeader(name: string) {
|
||||
return this._headers.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
removeHeader(name: string) {
|
||||
this._headers.delete(name.toLowerCase());
|
||||
}
|
||||
|
||||
writeHead(statusCode: number, statusMessage?: string | Record<string, string>, headers?: Record<string, string>) {
|
||||
if (this.headersSent) return this;
|
||||
|
||||
this.statusCode = statusCode;
|
||||
if (typeof statusMessage === "string") {
|
||||
this.statusMessage = statusMessage;
|
||||
} else if (typeof statusMessage === "object") {
|
||||
headers = statusMessage;
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
this.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
_flushHeaders() {
|
||||
if (this.headersSent) return;
|
||||
this.headersSent = true;
|
||||
|
||||
let header = `HTTP/1.1 ${this.statusCode} ${this.statusMessage || STATUS_CODES[this.statusCode] || "Unknown"}\r\n`;
|
||||
|
||||
if (this.sendDate && !this._headers.has("date")) {
|
||||
header += `Date: ${new Date().toUTCString()}\r\n`;
|
||||
}
|
||||
|
||||
for (const [name, values] of this._headers) {
|
||||
for (const value of values) {
|
||||
header += `${name}: ${value}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
header += "\r\n";
|
||||
this.socket.write(header);
|
||||
}
|
||||
|
||||
_write(chunk: any, encoding: string, callback: () => void) {
|
||||
this._flushHeaders();
|
||||
this.socket.write(chunk, encoding, callback);
|
||||
}
|
||||
|
||||
end(data?: any, encoding?: any, callback?: () => void) {
|
||||
if (typeof data === "function") {
|
||||
callback = data;
|
||||
data = undefined;
|
||||
} else if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
// Set Content-Length if not set and we have data
|
||||
if (data && !this._headers.has("content-length") && !this._headers.has("transfer-encoding")) {
|
||||
const len = typeof data === "string" ? Buffer.byteLength(data) : data.length;
|
||||
this.setHeader("content-length", String(len));
|
||||
}
|
||||
|
||||
this._flushHeaders();
|
||||
|
||||
if (data) {
|
||||
this.socket.write(data, encoding);
|
||||
}
|
||||
|
||||
this.finished = true;
|
||||
|
||||
if (!this.shouldKeepAlive) {
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
this.emit("finish");
|
||||
return this;
|
||||
}
|
||||
|
||||
writeContinue() {
|
||||
this.socket.write("HTTP/1.1 100 Continue\r\n\r\n");
|
||||
}
|
||||
|
||||
assignSocket(socket: any) {
|
||||
this.socket = socket;
|
||||
socket._httpMessage = this;
|
||||
}
|
||||
|
||||
detachSocket(socket: any) {
|
||||
socket._httpMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Windows named pipe support using node:net.Server with HTTP parsing
|
||||
Server.prototype[kRealListenNamedPipe] = function (tls, socketPath, onListen) {
|
||||
const net = require("node:net");
|
||||
const { parsers, freeParser, kIncomingMessage } = require("node:_http_common");
|
||||
const { HTTPParser, allMethods } = process.binding("http_parser");
|
||||
const httpServer = this;
|
||||
|
||||
// TLS over named pipes is not currently supported
|
||||
if (tls) {
|
||||
throw new Error("TLS is not supported over Windows named pipes");
|
||||
}
|
||||
|
||||
// Create a net.Server to listen on the named pipe
|
||||
const netServer = net.createServer((socket: any) => {
|
||||
// Get a parser from the pool
|
||||
const parser = parsers.alloc();
|
||||
parser.initialize(HTTPParser.REQUEST, {});
|
||||
parser.socket = socket;
|
||||
socket.parser = parser;
|
||||
socket._server = httpServer;
|
||||
socket.server = httpServer;
|
||||
|
||||
// Set up the parser's onIncoming callback - this is called from parserOnHeadersComplete
|
||||
parser.onIncoming = function onIncoming(req: any, shouldKeepAlive: boolean) {
|
||||
// Create a simple ServerResponse that writes directly to the socket
|
||||
const res = new NamedPipeServerResponse(req, socket);
|
||||
res.shouldKeepAlive = shouldKeepAlive;
|
||||
socket._httpMessage = res;
|
||||
|
||||
// Check for upgrade
|
||||
if (req.upgrade) {
|
||||
httpServer.emit("upgrade", req, socket, kEmptyBuffer);
|
||||
return 2; // Skip body
|
||||
} else if (req.headers.expect === "100-continue") {
|
||||
if (httpServer.listenerCount("checkContinue") > 0) {
|
||||
httpServer.emit("checkContinue", req, res);
|
||||
} else {
|
||||
res.writeContinue();
|
||||
httpServer.emit("request", req, res);
|
||||
}
|
||||
} else {
|
||||
httpServer.emit("request", req, res);
|
||||
}
|
||||
|
||||
return shouldKeepAlive ? 0 : 1;
|
||||
};
|
||||
|
||||
socket.on("data", (data: Buffer) => {
|
||||
const ret = parser.execute(data);
|
||||
if (ret instanceof Error) {
|
||||
socket.destroy(ret);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
parser.finish();
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
freeParser(parser, null, socket);
|
||||
});
|
||||
|
||||
socket.on("error", (err: Error) => {
|
||||
if (socket._httpMessage) {
|
||||
socket._httpMessage.destroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Emit connection event after setting up the parser
|
||||
httpServer.emit("connection", socket);
|
||||
});
|
||||
|
||||
// Store the net server
|
||||
this[kNamedPipeServer] = netServer;
|
||||
|
||||
// Store reference to netServer for the wrapper
|
||||
const namedPipeNetServer = netServer;
|
||||
|
||||
// Create a wrapper object that mimics the Bun.serve() server interface
|
||||
this[serverSymbol] = {
|
||||
address: socketPath,
|
||||
port: undefined,
|
||||
hostname: undefined,
|
||||
protocol: tls ? "https" : "http",
|
||||
stop: (closeActiveConnections?: boolean) => {
|
||||
// Use the stored reference to ensure it's correct
|
||||
const ns = httpServer[kNamedPipeServer];
|
||||
if (!ns) {
|
||||
// Already closed
|
||||
callCloseCallback(httpServer);
|
||||
httpServer.emit("close");
|
||||
return;
|
||||
}
|
||||
httpServer[kNamedPipeServer] = undefined;
|
||||
ns.close(() => {
|
||||
// Call the close callback and emit close event
|
||||
callCloseCallback(httpServer);
|
||||
httpServer.emit("close");
|
||||
});
|
||||
if (closeActiveConnections) {
|
||||
// Force close all connections
|
||||
// Note: node:net doesn't have a built-in way to do this
|
||||
}
|
||||
},
|
||||
closeIdleConnections: () => {
|
||||
// Not directly supported for named pipes
|
||||
},
|
||||
ref: () => {
|
||||
netServer.ref();
|
||||
},
|
||||
unref: () => {
|
||||
netServer.unref();
|
||||
},
|
||||
};
|
||||
|
||||
netServer.on("error", (err: Error) => {
|
||||
httpServer.emit("error", err);
|
||||
});
|
||||
|
||||
netServer.on("listening", () => {
|
||||
this.listening = true;
|
||||
httpServer.emit("listening");
|
||||
});
|
||||
|
||||
// Listen on the named pipe
|
||||
netServer.listen(socketPath);
|
||||
|
||||
if ($isCallable(onListen)) {
|
||||
this.once("listening", onListen);
|
||||
}
|
||||
|
||||
if (this._unref) {
|
||||
netServer.unref();
|
||||
}
|
||||
};
|
||||
|
||||
function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPResponseAbortEvent) {
|
||||
const socket: NodeHTTPServerSocket = this;
|
||||
switch (event) {
|
||||
|
||||
@@ -744,6 +744,12 @@ pub const Writer = struct {
|
||||
pub fn offset(this: Writer) usize {
|
||||
return this.connection.#write_buffer.len();
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of the write buffer for in-place manipulation.
|
||||
/// Used for large packet splitting where we need to rearrange data.
|
||||
pub fn slice(this: Writer) []u8 {
|
||||
return this.connection.#write_buffer.byte_list.slice();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn writer(this: *MySQLConnection) NewWriter(Writer) {
|
||||
|
||||
@@ -29,6 +29,9 @@ pub fn NewWriterWrap(
|
||||
try writeFn(this.wrapped, data);
|
||||
}
|
||||
|
||||
/// Maximum payload size for a single MySQL packet (2^24 - 1 = 16,777,215 bytes)
|
||||
pub const MAX_PACKET_PAYLOAD_SIZE: usize = 0xFFFFFF;
|
||||
|
||||
const Packet = struct {
|
||||
header: PacketHeader,
|
||||
offset: usize,
|
||||
@@ -36,12 +39,127 @@ pub fn NewWriterWrap(
|
||||
|
||||
pub fn end(this: *@This()) AnyMySQLError.Error!void {
|
||||
const new_offset = offsetFn(this.ctx.wrapped);
|
||||
// fix position for packet header
|
||||
// Calculate total payload length (excluding initial header)
|
||||
const length = new_offset - this.offset - PacketHeader.size;
|
||||
this.header.length = @intCast(length);
|
||||
debug("writing packet header: {d}", .{this.header.length});
|
||||
|
||||
if (length <= MAX_PACKET_PAYLOAD_SIZE) {
|
||||
// Normal case: payload fits in a single packet
|
||||
this.header.length = @intCast(length);
|
||||
debug("writing packet header: {d}", .{this.header.length});
|
||||
try pwrite(this.ctx, &this.header.encode(), this.offset);
|
||||
} else {
|
||||
// Large payload: needs to be split into multiple packets
|
||||
// MySQL protocol requires splitting payloads > 16MB into multiple packets
|
||||
// Each packet has a 4-byte header (3 bytes length + 1 byte sequence_id)
|
||||
try this.splitLargePacket(length);
|
||||
}
|
||||
}
|
||||
|
||||
fn splitLargePacket(this: *@This(), total_length: usize) AnyMySQLError.Error!void {
|
||||
// For large packets, we need to:
|
||||
// 1. Write the first chunk header at the original offset
|
||||
// 2. Insert additional headers between subsequent chunks
|
||||
//
|
||||
// The data is already in the buffer starting at (this.offset + PacketHeader.size)
|
||||
// We need to insert (num_extra_packets) additional headers
|
||||
const payload_start = this.offset + PacketHeader.size;
|
||||
const sequence_id = this.header.sequence_id;
|
||||
|
||||
// Calculate how many additional packets we need
|
||||
const num_packets = (total_length + MAX_PACKET_PAYLOAD_SIZE - 1) / MAX_PACKET_PAYLOAD_SIZE;
|
||||
const num_extra_headers = num_packets - 1;
|
||||
|
||||
debug("splitting large packet: total_length={d}, num_packets={d}", .{ total_length, num_packets });
|
||||
|
||||
if (num_extra_headers > 0) {
|
||||
// We need to expand the buffer to make room for additional headers
|
||||
// Each extra header is 4 bytes
|
||||
const extra_space = num_extra_headers * PacketHeader.size;
|
||||
var padding: [PacketHeader.size]u8 = undefined;
|
||||
for (0..num_extra_headers) |_| {
|
||||
try writeFn(this.ctx.wrapped, &padding);
|
||||
}
|
||||
|
||||
// Now we need to shift the data to make room for the headers
|
||||
// We'll do this by reading the current buffer content and rewriting it
|
||||
// with headers inserted at the right places
|
||||
//
|
||||
// Strategy: Work backwards from the end to avoid overwriting data we still need
|
||||
var src_offset = payload_start + total_length; // End of original payload
|
||||
var dst_offset = payload_start + total_length + extra_space; // End of expanded buffer
|
||||
|
||||
// Calculate chunk sizes for each packet
|
||||
// First (num_packets - 1) packets are MAX_PACKET_PAYLOAD_SIZE
|
||||
// Last packet is the remainder
|
||||
const last_chunk_size = total_length - (num_packets - 1) * MAX_PACKET_PAYLOAD_SIZE;
|
||||
|
||||
// Process packets from last to first (reverse order to avoid overwriting)
|
||||
var packet_idx = num_packets;
|
||||
while (packet_idx > 0) {
|
||||
packet_idx -= 1;
|
||||
|
||||
// Calculate chunk size: last packet gets the remainder, others get MAX
|
||||
const chunk_size = if (packet_idx == num_packets - 1)
|
||||
last_chunk_size
|
||||
else
|
||||
MAX_PACKET_PAYLOAD_SIZE;
|
||||
|
||||
// Move this chunk's data to its new position
|
||||
src_offset -= chunk_size;
|
||||
dst_offset -= chunk_size;
|
||||
|
||||
if (dst_offset != src_offset) {
|
||||
// Use memmove-style copy (copy via temp buffer to handle overlap)
|
||||
// Since we're working backwards, dst > src, so no overlap issues
|
||||
try this.copyWithinBuffer(src_offset, dst_offset, chunk_size);
|
||||
}
|
||||
|
||||
// Write header for this packet (skip first packet, handled below)
|
||||
if (packet_idx > 0) {
|
||||
dst_offset -= PacketHeader.size;
|
||||
const header = PacketHeader{
|
||||
.length = @intCast(chunk_size),
|
||||
.sequence_id = sequence_id +% @as(u8, @intCast(packet_idx)),
|
||||
};
|
||||
try pwrite(this.ctx, &header.encode(), dst_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the first packet header at the original position
|
||||
// First packet always has MAX_PACKET_PAYLOAD_SIZE bytes (when total > MAX)
|
||||
const first_chunk_size: u24 = if (total_length > MAX_PACKET_PAYLOAD_SIZE)
|
||||
MAX_PACKET_PAYLOAD_SIZE
|
||||
else
|
||||
@intCast(total_length);
|
||||
this.header.length = first_chunk_size;
|
||||
debug("writing first packet header: {d}", .{this.header.length});
|
||||
try pwrite(this.ctx, &this.header.encode(), this.offset);
|
||||
}
|
||||
|
||||
fn copyWithinBuffer(this: *@This(), src_offset: usize, dst_offset: usize, len: usize) AnyMySQLError.Error!void {
|
||||
// Copy data within the buffer from src_offset to dst_offset
|
||||
// We need to be careful about overlapping regions
|
||||
if (src_offset == dst_offset or len == 0) return;
|
||||
|
||||
// Get access to the underlying buffer for copying
|
||||
// Use the slice function if available on the context
|
||||
if (@hasDecl(Context, "slice")) {
|
||||
const buf = this.ctx.wrapped.slice();
|
||||
// Use memmove-style copy for overlapping regions
|
||||
if (dst_offset > src_offset) {
|
||||
// Copy backwards to handle overlap (dst is after src)
|
||||
std.mem.copyBackwards(u8, buf[dst_offset..][0..len], buf[src_offset..][0..len]);
|
||||
} else {
|
||||
// Copy forwards
|
||||
std.mem.copyForwards(u8, buf[dst_offset..][0..len], buf[src_offset..][0..len]);
|
||||
}
|
||||
} else {
|
||||
// Fallback: cannot copy without direct buffer access
|
||||
// This should not happen since Writer has slice()
|
||||
return error.Overflow;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn start(this: @This(), sequence_id: u8) AnyMySQLError.Error!Packet {
|
||||
|
||||
18
src/sys.zig
18
src/sys.zig
@@ -3986,8 +3986,22 @@ pub fn moveFileZSlowMaybe(from_dir: bun.FileDescriptor, filename: [:0]const u8,
|
||||
.err => |e| return .{ .err = e },
|
||||
};
|
||||
defer in_handle.close();
|
||||
_ = from_dir.unlinkat(filename);
|
||||
return copyFileZSlowWithHandle(in_handle, to_dir, destination);
|
||||
// On Windows, we must not delete the source file until after the copy completes.
|
||||
// copyFileZSlowWithHandle on Windows uses GetFinalPathNameByHandleW to get the path
|
||||
// from the handle, then uses CopyFileW which requires the source path to still exist.
|
||||
// Additionally, Windows won't allow deleting a file with an open handle unless
|
||||
// the handle was opened with FILE_SHARE_DELETE.
|
||||
if (comptime !Environment.isWindows) {
|
||||
_ = from_dir.unlinkat(filename);
|
||||
}
|
||||
const result = copyFileZSlowWithHandle(in_handle, to_dir, destination);
|
||||
if (comptime Environment.isWindows) {
|
||||
// Delete the source file after a successful copy
|
||||
if (result == .result) {
|
||||
_ = from_dir.unlinkat(filename);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn copyFileZSlowWithHandle(in_handle: bun.FileDescriptor, to_dir: bun.FileDescriptor, destination: [:0]const u8) Maybe(void) {
|
||||
|
||||
61
test/regression/issue/11117.test.ts
Normal file
61
test/regression/issue/11117.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/11117
|
||||
// On Windows, Bun.write() to stdout in a loop would produce scrambled output
|
||||
// because async writes were not serialized properly.
|
||||
test("Bun.write to stdout maintains write order", async () => {
|
||||
const testString = "This is a test\n";
|
||||
|
||||
// Run the test multiple times to catch any race conditions
|
||||
for (let run = 0; run < 10; run++) {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const test = (s) => { const l = s.length; for (let i = 0; i < l; i++) { Bun.write(Bun.stdout, s[i]); } }; test(${JSON.stringify(testString)});`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stderr).toBe("");
|
||||
expect(stdout).toBe(testString);
|
||||
expect(exitCode).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun.write to stderr maintains write order", async () => {
|
||||
const testString = "Error message test\n";
|
||||
|
||||
// Run the test multiple times to catch any race conditions
|
||||
for (let run = 0; run < 10; run++) {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`const test = (s) => { const l = s.length; for (let i = 0; i < l; i++) { Bun.write(Bun.stderr, s[i]); } }; test(${JSON.stringify(testString)});`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toBe("");
|
||||
expect(stderr).toBe(testString);
|
||||
expect(exitCode).toBe(0);
|
||||
}
|
||||
});
|
||||
253
test/regression/issue/13040.test.ts
Normal file
253
test/regression/issue/13040.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/13040
|
||||
// process.on('SIGINT', handler) was being ignored on Windows - Ctrl+C would
|
||||
// immediately terminate the process instead of calling the handler.
|
||||
//
|
||||
// The fix (in c-bindings.cpp) modifies the Windows console control handler
|
||||
// (Ctrlhandler) to check if there's a JavaScript SIGINT handler registered.
|
||||
// If there is, it returns TRUE to prevent Windows from terminating the process,
|
||||
// allowing libuv's signal handler to invoke the JavaScript callback.
|
||||
//
|
||||
// Note: On Windows, process.kill(pid, "SIGINT") uses uv_kill which may
|
||||
// terminate the process directly (via TerminateProcess) when
|
||||
// GenerateConsoleCtrlEvent fails for processes not in the same console group.
|
||||
// The fix specifically addresses the console Ctrl+C scenario.
|
||||
//
|
||||
// These tests verify the signal handler registration and emission works correctly.
|
||||
// Manual testing is required to verify the actual Ctrl+C behavior:
|
||||
// 1. Run: bun -e "process.on('SIGINT', () => { console.log('SIGINT'); process.exit(0); }); setInterval(() => {}, 1000);"
|
||||
// 2. Press Ctrl+C
|
||||
// 3. Expected: "SIGINT" should be printed, then the process exits with code 0
|
||||
|
||||
test("SIGINT handler can be registered and receives events", async () => {
|
||||
// This test verifies that:
|
||||
// 1. SIGINT handler can be registered
|
||||
// 2. The handler code path works when the signal is emitted
|
||||
using dir = tempDir("sigint-test", {
|
||||
"sigint-handler.js": `
|
||||
let handlerCalled = false;
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
handlerCalled = true;
|
||||
console.log("SIGINT_HANDLER_CALLED");
|
||||
process.exit(42);
|
||||
});
|
||||
|
||||
// Manually emit SIGINT to test the handler
|
||||
console.log("READY");
|
||||
process.emit("SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-handler.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toContain("SIGINT_HANDLER_CALLED");
|
||||
expect(exitCode).toBe(42);
|
||||
});
|
||||
|
||||
test("SIGINT handler with async work", async () => {
|
||||
// Test that async operations work in SIGINT handler
|
||||
using dir = tempDir("sigint-async-test", {
|
||||
"sigint-async.js": `
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("START");
|
||||
await Bun.sleep(100);
|
||||
console.log("END");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
console.log("READY");
|
||||
process.emit("SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-async.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toContain("START");
|
||||
expect(stdout).toContain("END");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("multiple SIGINT handlers", async () => {
|
||||
using dir = tempDir("sigint-multi-handler", {
|
||||
"sigint-multi.js": `
|
||||
let calls = [];
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
calls.push("handler1");
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
calls.push("handler2");
|
||||
console.log(calls.join(","));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.emit("SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-multi.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toContain("handler1,handler2");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("removing SIGINT handler", async () => {
|
||||
using dir = tempDir("sigint-remove", {
|
||||
"sigint-remove.js": `
|
||||
let calls = [];
|
||||
|
||||
const handler1 = () => {
|
||||
calls.push("handler1");
|
||||
};
|
||||
|
||||
const handler2 = () => {
|
||||
calls.push("handler2");
|
||||
console.log(calls.join(","));
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on("SIGINT", handler1);
|
||||
process.on("SIGINT", handler2);
|
||||
process.off("SIGINT", handler1);
|
||||
|
||||
process.emit("SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-remove.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
// Only handler2 should have been called
|
||||
expect(stdout).toContain("handler2");
|
||||
expect(stdout).not.toContain("handler1,");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
// Test the workaround from the issue
|
||||
test("readline SIGINT workaround from issue", async () => {
|
||||
using dir = tempDir("sigint-readline", {
|
||||
"sigint-readline.js": `
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
let sigintReceived = false;
|
||||
|
||||
rl.on("SIGINT", function () {
|
||||
process.emit("SIGINT");
|
||||
});
|
||||
|
||||
process.on("SIGINT", function () {
|
||||
sigintReceived = true;
|
||||
console.log("SIGINT_RECEIVED");
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Emit SIGINT through readline
|
||||
rl.emit("SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-readline.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toContain("SIGINT_RECEIVED");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
// On non-Windows platforms, test that process.kill works
|
||||
test.skipIf(isWindows)("SIGINT via process.kill on POSIX", async () => {
|
||||
using dir = tempDir("sigint-kill-posix", {
|
||||
"sigint-kill.js": `
|
||||
process.on("SIGINT", () => {
|
||||
console.log("SIGINT_HANDLER_CALLED");
|
||||
process.exit(42);
|
||||
});
|
||||
|
||||
console.log("READY");
|
||||
// Send SIGINT to self
|
||||
process.kill(process.pid, "SIGINT");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "sigint-kill.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toContain("SIGINT_HANDLER_CALLED");
|
||||
expect(exitCode).toBe(42);
|
||||
});
|
||||
116
test/regression/issue/13316.test.ts
Normal file
116
test/regression/issue/13316.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { test, expect, describe } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/13316
|
||||
// bunx cowsay "" panicked on Windows due to improper handling of empty string arguments
|
||||
// The issue was in the BunXFastPath.tryLaunch function which didn't properly quote
|
||||
// empty string arguments for the Windows command line.
|
||||
describe.if(isWindows)("#13316 - bunx with empty string arguments", () => {
|
||||
test("bunx does not panic with empty string argument", async () => {
|
||||
// Create a minimal package that echoes its arguments
|
||||
using dir = tempDir("issue-13316", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-project",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"echo-args-test": "file:./echo-args-test"
|
||||
}
|
||||
}),
|
||||
"echo-args-test/package.json": JSON.stringify({
|
||||
name: "echo-args-test",
|
||||
version: "1.0.0",
|
||||
bin: {
|
||||
"echo-args-test": "./index.js"
|
||||
}
|
||||
}),
|
||||
"echo-args-test/index.js": `#!/usr/bin/env node
|
||||
console.log(JSON.stringify(process.argv.slice(2)));
|
||||
`
|
||||
});
|
||||
|
||||
// Install to create the .bunx shim in node_modules/.bin
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
await installProc.exited;
|
||||
|
||||
// Verify the .bunx file was created (this is what triggers the fast path)
|
||||
const bunxPath = path.join(String(dir), "node_modules", ".bin", "echo-args-test.bunx");
|
||||
expect(fs.existsSync(bunxPath)).toBe(true);
|
||||
|
||||
// Run with an empty string argument - this was triggering the panic
|
||||
// We use `bun run` which goes through the same BunXFastPath when .bunx exists
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "echo-args-test", ""],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
// The main assertion is that the process doesn't panic (exit code 3)
|
||||
// If the bug is present, this would crash with "reached unreachable code"
|
||||
expect(exitCode).not.toBe(3); // panic exit code
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// The empty string argument should be passed correctly
|
||||
expect(JSON.parse(stdout.trim())).toEqual([""]);
|
||||
});
|
||||
|
||||
test("bunx handles multiple arguments including empty strings", async () => {
|
||||
using dir = tempDir("issue-13316-multi", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "test-project",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"echo-args-test": "file:./echo-args-test"
|
||||
}
|
||||
}),
|
||||
"echo-args-test/package.json": JSON.stringify({
|
||||
name: "echo-args-test",
|
||||
version: "1.0.0",
|
||||
bin: {
|
||||
"echo-args-test": "./index.js"
|
||||
}
|
||||
}),
|
||||
"echo-args-test/index.js": `#!/usr/bin/env node
|
||||
console.log(JSON.stringify(process.argv.slice(2)));
|
||||
`
|
||||
});
|
||||
|
||||
await using installProc = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
await installProc.exited;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "echo-args-test", "hello", "", "world"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(exitCode).not.toBe(3); // panic exit code
|
||||
expect(exitCode).toBe(0);
|
||||
expect(JSON.parse(stdout.trim())).toEqual(["hello", "", "world"]);
|
||||
});
|
||||
});
|
||||
175
test/regression/issue/18653.test.ts
Normal file
175
test/regression/issue/18653.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createServer, request } from "http";
|
||||
import { isWindows, tmpdirSync } from "harness";
|
||||
import { join } from "path";
|
||||
import { rmSync } from "fs";
|
||||
|
||||
/**
|
||||
* Regression test for issue #18653: http.request with socketPath connects to localhost on Windows
|
||||
*
|
||||
* Issue: When using http.request with a socketPath option (for Unix domain sockets / named pipes),
|
||||
* Bun incorrectly connects to localhost instead of the socket path because the DNS lookup
|
||||
* path was taken even when socketPath was provided.
|
||||
*
|
||||
* The fix ensures that when socketPath is specified, DNS lookup is bypassed and the connection
|
||||
* goes directly to the Unix domain socket.
|
||||
*
|
||||
* Note: Windows named pipes (\\.\pipe\name) are not yet supported by Bun's HTTP client.
|
||||
* This test only covers Unix domain sockets which work on non-Windows platforms.
|
||||
*/
|
||||
describe("http.request with socketPath (#18653)", () => {
|
||||
// Unix socket tests (non-Windows)
|
||||
test.skipIf(isWindows)("should connect via Unix socket with socketPath option", async () => {
|
||||
const tmpDir = tmpdirSync();
|
||||
const socketPath = join(tmpDir, `bun-test-${Math.random().toString(36).slice(2)}.sock`);
|
||||
|
||||
// Create a server listening on the Unix socket
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.end(`path:${req.url}`);
|
||||
});
|
||||
|
||||
const { promise: serverReady, resolve: serverReadyResolve } = Promise.withResolvers<void>();
|
||||
server.listen(socketPath, () => {
|
||||
serverReadyResolve();
|
||||
});
|
||||
|
||||
await serverReady;
|
||||
|
||||
try {
|
||||
// Make a request using socketPath
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const req = request(
|
||||
{
|
||||
socketPath,
|
||||
path: "/test-path",
|
||||
method: "GET",
|
||||
},
|
||||
res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
res.on("error", reject);
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
|
||||
const response = await promise;
|
||||
expect(response).toBe("path:/test-path");
|
||||
} finally {
|
||||
server.close();
|
||||
try {
|
||||
rmSync(socketPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(isWindows)("should correctly pass socketPath to fetch via unix option", async () => {
|
||||
const tmpDir = tmpdirSync();
|
||||
const socketPath = join(tmpDir, `bun-test-${Math.random().toString(36).slice(2)}.sock`);
|
||||
|
||||
// Create a Bun server listening on the Unix socket
|
||||
const server = Bun.serve({
|
||||
unix: socketPath,
|
||||
fetch(req) {
|
||||
return new Response(`path:${new URL(req.url).pathname}`);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Make a request using node:http with socketPath
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const req = request(
|
||||
{
|
||||
socketPath,
|
||||
path: "/another-path",
|
||||
method: "GET",
|
||||
},
|
||||
res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
res.on("error", reject);
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
|
||||
const response = await promise;
|
||||
expect(response).toBe("path:/another-path");
|
||||
} finally {
|
||||
server.stop(true);
|
||||
try {
|
||||
rmSync(socketPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.skipIf(isWindows)("should work with POST requests via socketPath", async () => {
|
||||
const tmpDir = tmpdirSync();
|
||||
const socketPath = join(tmpDir, `bun-test-${Math.random().toString(36).slice(2)}.sock`);
|
||||
|
||||
// Create a Bun server listening on the Unix socket
|
||||
const server = Bun.serve({
|
||||
unix: socketPath,
|
||||
async fetch(req) {
|
||||
const body = await req.text();
|
||||
return new Response(`received:${body}`);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Make a POST request using node:http with socketPath
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const req = request(
|
||||
{
|
||||
socketPath,
|
||||
path: "/post-test",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
},
|
||||
res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
res.on("error", reject);
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.write("hello world");
|
||||
req.end();
|
||||
|
||||
const response = await promise;
|
||||
expect(response).toBe("received:hello world");
|
||||
} finally {
|
||||
server.stop(true);
|
||||
try {
|
||||
rmSync(socketPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
75
test/regression/issue/18875.test.ts
Normal file
75
test/regression/issue/18875.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// Test for issue #18875: bun patch --commit error on windows
|
||||
// https://github.com/oven-sh/bun/issues/18875
|
||||
//
|
||||
// On Windows, bun patch --commit would fail with:
|
||||
// "EPERM: Operation not permitted: failed renaming patch file to patches dir (copyfile)"
|
||||
//
|
||||
// The issue was that when the temp directory and project directory are on different
|
||||
// volumes (cross-device), the rename operation fails. The fallback code for cross-device
|
||||
// moves (moveFileZSlowMaybe) was deleting the source file BEFORE copying it, which
|
||||
// doesn't work on Windows because:
|
||||
// 1. Windows can't delete a file that has an open handle unless FILE_SHARE_DELETE is used
|
||||
// 2. The Windows implementation uses GetFinalPathNameByHandleW to get the path, then
|
||||
// CopyFileW, which requires the source path to still exist.
|
||||
|
||||
import { $, ShellOutput } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
const expectNoError = (o: ShellOutput) => expect(o.stderr.toString()).not.toContain("error");
|
||||
|
||||
test("bun patch --commit should work (issue #18875)", async () => {
|
||||
// This test verifies that bun patch --commit works correctly.
|
||||
// The original issue occurred on Windows when the temp directory was on a different
|
||||
// volume than the project directory, causing a cross-device rename failure.
|
||||
const tempdir = tempDirWithFiles("issue-18875", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "bun-patch-test-18875",
|
||||
module: "index.ts",
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"is-even": "1.0.0",
|
||||
},
|
||||
}),
|
||||
"index.ts": `import isEven from 'is-even'; console.log(isEven(420))`,
|
||||
});
|
||||
|
||||
// Install dependencies
|
||||
expectNoError(await $`${bunExe()} i`.env(bunEnv).cwd(tempdir));
|
||||
|
||||
// Start patching
|
||||
const patchResult = await $`${bunExe()} patch is-even`.env(bunEnv).cwd(tempdir);
|
||||
expect(patchResult.stderr.toString()).not.toContain("error");
|
||||
|
||||
// Make a simple change to the package
|
||||
const patchedCode = `/*!
|
||||
* is-even <https://github.com/jonschlinkert/is-even>
|
||||
*
|
||||
* Copyright (c) 2015, 2017, Jon Schlinkert.
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var isOdd = require('is-odd');
|
||||
|
||||
module.exports = function isEven(i) {
|
||||
console.log("Patched via issue #18875 test");
|
||||
return !isOdd(i);
|
||||
};
|
||||
`;
|
||||
|
||||
await $`echo ${patchedCode} > node_modules/is-even/index.js`.env(bunEnv).cwd(tempdir);
|
||||
|
||||
// Commit the patch - this is where the bug occurred on Windows
|
||||
const commitResult = await $`${bunExe()} patch --commit node_modules/is-even`.env(bunEnv).cwd(tempdir);
|
||||
|
||||
// Verify no EPERM error occurred
|
||||
expect(commitResult.stderr.toString()).not.toContain("EPERM");
|
||||
expect(commitResult.stderr.toString()).not.toContain("error");
|
||||
|
||||
// Verify the patch was applied correctly by running the patched code
|
||||
const runResult = await $`${bunExe()} run index.ts`.env(bunEnv).cwd(tempdir);
|
||||
expect(runResult.stdout.toString()).toContain("Patched via issue #18875 test");
|
||||
expect(runResult.stdout.toString()).toContain("true");
|
||||
});
|
||||
72
test/regression/issue/23292.test.ts
Normal file
72
test/regression/issue/23292.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// https://github.com/oven-sh/bun/issues/23292
|
||||
// fs.access() and fs.accessSync() should work with Windows named pipes
|
||||
import { expect, test } from "bun:test";
|
||||
import { isWindows } from "harness";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { once } from "node:events";
|
||||
|
||||
test.if(isWindows)("fs.accessSync should work with named pipes", async () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-${randomUUID()}`;
|
||||
|
||||
const server = net.createServer();
|
||||
server.listen(pipeName);
|
||||
await once(server, "listening");
|
||||
|
||||
try {
|
||||
// Should not throw - the pipe exists
|
||||
fs.accessSync(pipeName, fs.constants.F_OK);
|
||||
|
||||
// Test with R_OK as well
|
||||
fs.accessSync(pipeName, fs.constants.R_OK);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.if(isWindows)("fs.access should work with named pipes", async () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-${randomUUID()}`;
|
||||
|
||||
const server = net.createServer();
|
||||
server.listen(pipeName);
|
||||
await once(server, "listening");
|
||||
|
||||
try {
|
||||
// Test fs.access with callback
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
fs.access(pipeName, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
await promise;
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.if(isWindows)("fs.promises.access should work with named pipes", async () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-${randomUUID()}`;
|
||||
|
||||
const server = net.createServer();
|
||||
server.listen(pipeName);
|
||||
await once(server, "listening");
|
||||
|
||||
try {
|
||||
// Should not throw - the pipe exists
|
||||
await fs.promises.access(pipeName, fs.constants.F_OK);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.if(isWindows)("fs.accessSync should throw ENOENT for non-existent named pipe", () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-nonexistent-${randomUUID()}`;
|
||||
|
||||
expect(() => {
|
||||
fs.accessSync(pipeName, fs.constants.F_OK);
|
||||
}).toThrow();
|
||||
});
|
||||
87
test/regression/issue/23924.test.ts
Normal file
87
test/regression/issue/23924.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { test, expect, describe } from "bun:test";
|
||||
import { $ } from "bun";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import { copyFile, mkdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
describe("issue #23924 - Bun.$ with quoted executable paths containing spaces", () => {
|
||||
// The original issue:
|
||||
// await Bun.$`"C:/Program Files/gs/gs10.06.0/bin/gswin64c.exe" -dNOPAUSE ...`
|
||||
// Error: bun: command not found: "C:/Program Files/gs/gs10.06.0/bin/gswin64c.exe"
|
||||
// Note: quotes were INCLUDED in the error message, meaning they weren't stripped
|
||||
|
||||
test.skipIf(!isWindows)("should run executable with quoted absolute path", async () => {
|
||||
// Use where.exe which is guaranteed to exist on Windows
|
||||
const result = await $`"C:/Windows/System32/where.exe" where`.nothrow();
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.toString()).toContain("where.exe");
|
||||
});
|
||||
|
||||
test.skipIf(!isWindows)("should run executable with unquoted absolute path", async () => {
|
||||
const result = await $`C:/Windows/System32/where.exe where`.nothrow();
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.toString()).toContain("where.exe");
|
||||
});
|
||||
|
||||
test.skipIf(!isWindows)("should run executable with raw quoted path (exact issue pattern)", async () => {
|
||||
// This is the exact pattern from the issue using raw template strings
|
||||
const result = await $`${{ raw: '"C:/Windows/System32/where.exe" where' }}`.nothrow();
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.toString()).toContain("where.exe");
|
||||
});
|
||||
|
||||
test.skipIf(!isWindows)("should run executable with path containing spaces (Program Files style)", async () => {
|
||||
// Create a temp directory with spaces like "Program Files"
|
||||
using dir = tempDir("test Program Files", {});
|
||||
const subDir = join(String(dir), "gs", "bin");
|
||||
await mkdir(subDir, { recursive: true });
|
||||
|
||||
// Copy bun executable to the path with spaces
|
||||
const bunDest = join(subDir, "bun-test.exe");
|
||||
await copyFile(bunExe(), bunDest);
|
||||
|
||||
// Test the exact issue pattern - raw string with quoted path containing spaces
|
||||
const pathWithSpaces = bunDest.replaceAll("\\", "/");
|
||||
const result = await $`${{ raw: `"${pathWithSpaces}" -e "console.log('success')"` }}`
|
||||
.nothrow()
|
||||
.env(bunEnv);
|
||||
|
||||
expect(result.stderr.toString()).toBe("");
|
||||
expect(result.stdout.toString().trim()).toBe("success");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.skipIf(!isWindows)("should run script in directory with spaces via JS interpolation", async () => {
|
||||
// Create a temp directory with a space in it - simulating "Program Files"
|
||||
using dir = tempDir("test dir with spaces", {
|
||||
"script.ts": `console.log("hello from script");`,
|
||||
});
|
||||
|
||||
// Using JS interpolation (this was reported as working in the issue)
|
||||
const scriptPath = `${dir}/script.ts`;
|
||||
const result = await $`${bunExe()} ${scriptPath}`.nothrow().env(bunEnv);
|
||||
|
||||
expect(result.stderr.toString()).toBe("");
|
||||
expect(result.stdout.toString().trim()).toBe("hello from script");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("should handle quoted executable paths with spaces on all platforms", async () => {
|
||||
// Create a temp directory with a space in it
|
||||
using dir = tempDir("test dir with spaces", {
|
||||
"script.ts": `console.log("hello from script");`,
|
||||
});
|
||||
|
||||
const scriptPath = `${dir}/script.ts`;
|
||||
|
||||
// Test running bun with a quoted path that has spaces
|
||||
const result = await $`"${bunExe()}" "${scriptPath}"`.nothrow().env(bunEnv);
|
||||
|
||||
expect(result.stderr.toString()).toBe("");
|
||||
expect(result.stdout.toString().trim()).toBe("hello from script");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
196
test/regression/issue/24682.test.ts
Normal file
196
test/regression/issue/24682.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
// Test for GitHub issue #24682: node:http server cannot listen on Windows named pipes
|
||||
// https://github.com/oven-sh/bun/issues/24682
|
||||
|
||||
import { test, expect, describe } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows } from "harness";
|
||||
|
||||
describe.skipIf(!isWindows)("node:http Windows named pipe support", () => {
|
||||
test("http server can listen and respond on Windows named pipe", async () => {
|
||||
// Use a unique pipe name based on process ID and timestamp to avoid conflicts
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-${process.pid}-${Date.now()}`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
const http = require("node:http");
|
||||
const net = require("node:net");
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.end("Hello from named pipe!");
|
||||
});
|
||||
|
||||
server.listen("${pipeName.replace(/\\/g, "\\\\")}", () => {
|
||||
console.log("Server listening on pipe");
|
||||
|
||||
// Use net.connect to make a raw HTTP request to the named pipe
|
||||
const client = net.connect("${pipeName.replace(/\\/g, "\\\\")}", () => {
|
||||
console.log("Client connected");
|
||||
client.write("GET / HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n");
|
||||
});
|
||||
|
||||
let response = "";
|
||||
client.on("data", (data) => {
|
||||
response += data.toString();
|
||||
});
|
||||
|
||||
client.on("close", () => {
|
||||
console.log("Got response");
|
||||
if (response.includes("Hello from named pipe!")) {
|
||||
console.log("Response contains expected body");
|
||||
}
|
||||
if (response.includes("HTTP/1.1 200")) {
|
||||
console.log("Got HTTP 200 response");
|
||||
}
|
||||
// Force exit after receiving response - close callback has known issues
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
console.error("Client error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
console.error("Server error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
console.error("Timeout");
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log("stdout:", stdout);
|
||||
console.log("stderr:", stderr);
|
||||
}
|
||||
|
||||
expect(stdout).toContain("Server listening on pipe");
|
||||
expect(stdout).toContain("Client connected");
|
||||
expect(stdout).toContain("Response contains expected body");
|
||||
expect(stdout).toContain("Got HTTP 200 response");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("http server emits listening event on named pipe", async () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-listening-${process.pid}-${Date.now()}`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
const http = require("node:http");
|
||||
|
||||
const server = http.createServer();
|
||||
|
||||
server.on("listening", () => {
|
||||
const addr = server.address();
|
||||
console.log("Listening event fired");
|
||||
console.log("Address type:", typeof addr);
|
||||
// Force exit - close callback has known issues
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
console.error("Error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.listen("${pipeName.replace(/\\/g, "\\\\")}");
|
||||
|
||||
setTimeout(() => {
|
||||
console.error("Timeout");
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log("stdout:", stdout);
|
||||
console.log("stderr:", stderr);
|
||||
}
|
||||
|
||||
expect(stdout).toContain("Listening event fired");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("http server callback fires on named pipe listen", async () => {
|
||||
const pipeName = `\\\\.\\pipe\\bun-test-callback-${process.pid}-${Date.now()}`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
const http = require("node:http");
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end("OK");
|
||||
});
|
||||
|
||||
server.listen("${pipeName.replace(/\\/g, "\\\\")}", () => {
|
||||
console.log("Callback fired");
|
||||
// Force exit - close callback has known issues
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
console.error("Error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
console.error("Timeout");
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout.text(),
|
||||
proc.stderr.text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
console.log("stdout:", stdout);
|
||||
console.log("stderr:", stderr);
|
||||
}
|
||||
|
||||
expect(stdout).toContain("Callback fired");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
94
test/regression/issue/24844.test.ts
Normal file
94
test/regression/issue/24844.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/24844
|
||||
// MySQL panics on large query payloads (>16MB) due to integer truncation
|
||||
//
|
||||
// This test verifies that large payloads don't cause a panic.
|
||||
// The fix involves properly splitting packets larger than 16MB according to MySQL protocol.
|
||||
|
||||
import { SQL, randomUUIDv7 } from "bun";
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { describeWithContainer, isDockerEnabled } from "harness";
|
||||
|
||||
// Only run this test if Docker is available
|
||||
if (isDockerEnabled()) {
|
||||
describeWithContainer(
|
||||
"MySQL large payload (#24844)",
|
||||
{
|
||||
image: "mysql_plain",
|
||||
concurrent: false, // Large payload test should run alone
|
||||
},
|
||||
(container) => {
|
||||
let sql: SQL;
|
||||
|
||||
beforeAll(async () => {
|
||||
await container.ready;
|
||||
sql = new SQL({
|
||||
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Test that a payload just under 16MB works (baseline)
|
||||
test("handles payload just under 16MB threshold", async () => {
|
||||
await using db = new SQL({
|
||||
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
});
|
||||
using sql = await db.reserve();
|
||||
|
||||
// Create a large string just under 16MB (16,777,215 - some overhead)
|
||||
// Using 16MB - 1KB to account for query overhead
|
||||
const largeData = Buffer.alloc(16 * 1024 * 1024 - 1024, "A").toString();
|
||||
|
||||
const tableName = "test_large_" + randomUUIDv7("hex").replaceAll("-", "");
|
||||
await sql`CREATE TEMPORARY TABLE ${sql(tableName)} (id INT, data LONGTEXT)`;
|
||||
|
||||
// This should work without panic
|
||||
await sql`INSERT INTO ${sql(tableName)} (id, data) VALUES (1, ${largeData})`;
|
||||
|
||||
const result = await sql`SELECT LENGTH(data) as len FROM ${sql(tableName)} WHERE id = 1`;
|
||||
expect(result[0].len).toBe(largeData.length);
|
||||
}, 60000); // 60 second timeout for large data
|
||||
|
||||
// Test that a payload over 16MB works (the actual regression)
|
||||
// This test requires max_allowed_packet to be set high enough on the MySQL server
|
||||
test("handles payload over 16MB threshold without panic", async () => {
|
||||
await using db = new SQL({
|
||||
url: `mysql://root@${container.host}:${container.port}/bun_sql_test`,
|
||||
max: 1,
|
||||
});
|
||||
using sql = await db.reserve();
|
||||
|
||||
// Create a string over 16MB (this is what caused the panic in #24844)
|
||||
// The reporter mentioned 18,730,521 chars caused the crash
|
||||
// We'll test with 17MB to be safe and avoid memory issues in CI
|
||||
const largeData = Buffer.alloc(17 * 1024 * 1024, "A").toString();
|
||||
|
||||
const tableName = "test_large_17mb_" + randomUUIDv7("hex").replaceAll("-", "");
|
||||
await sql`CREATE TEMPORARY TABLE ${sql(tableName)} (id INT, data LONGTEXT)`;
|
||||
|
||||
// This should NOT panic anymore after the fix
|
||||
// It may still fail if max_allowed_packet is not high enough, but it shouldn't panic
|
||||
try {
|
||||
await sql`INSERT INTO ${sql(tableName)} (id, data) VALUES (1, ${largeData})`;
|
||||
|
||||
const result = await sql`SELECT LENGTH(data) as len FROM ${sql(tableName)} WHERE id = 1`;
|
||||
expect(result[0].len).toBe(largeData.length);
|
||||
} catch (e: unknown) {
|
||||
// If the error is about max_allowed_packet, that's expected and acceptable
|
||||
// The important thing is that we didn't panic
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("max_allowed_packet")) {
|
||||
console.log("Note: max_allowed_packet limit reached, but no panic occurred");
|
||||
expect().pass();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}, 120000); // 120 second timeout for very large data
|
||||
}
|
||||
);
|
||||
} else {
|
||||
describe("MySQL large payload (#24844)", () => {
|
||||
test.skip("requires Docker to be enabled", () => {});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user