diff --git a/docs/api/spawn.md b/docs/api/spawn.md index 644f8cee38..6dff192719 100644 --- a/docs/api/spawn.md +++ b/docs/api/spawn.md @@ -186,6 +186,7 @@ proc.unref(); ## Inter-process communication (IPC) Bun supports direct inter-process communication channel between two `bun` processes. To receive messages from a spawned Bun subprocess, specify an `ipc` handler. + {%callout%} **Note** — This API is only compatible with other `bun` processes. Use `process.execPath` to get a path to the currently running `bun` executable. {%/callout%} @@ -227,8 +228,6 @@ process.on("message", (message) => { }); ``` -All messages are serialized using the JSC `serialize` API, which allows for the same set of [transferrable types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) supported by `postMessage` and `structuredClone`, including strings, typed arrays, streams, and objects. - ```ts#child.ts // send a string process.send("Hello from child as string"); @@ -237,6 +236,11 @@ process.send("Hello from child as string"); process.send({ message: "Hello from child as object" }); ``` +The `ipcMode` option controls the underlying communication format between the two processes: + +- `advanced`: (default) Messages are serialized using the JSC `serialize` API, which supports cloning [everything `structuredClone` supports](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This does not support transferring ownership of objects. +- `json`: Messages are serialized using `JSON.stringify` and `JSON.parse`, which does not support as many object types as `advanced` does. + ## Blocking API (`Bun.spawnSync()`) Bun provides a synchronous equivalent of `Bun.spawn` called `Bun.spawnSync`. This is a blocking API that supports the same inputs and parameters as `Bun.spawn`. It returns a `SyncSubprocess` object, which differs from `Subprocess` in a few ways. diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 9228644889..6008cb24ba 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -18,7 +18,7 @@ This page is updated regularly to reflect compatibility status of the latest ver ### [`node:child_process`](https://nodejs.org/api/child_process.html) -🟡 Missing `Stream` stdio, `proc.gid` `proc.uid`. IPC has partial support and only current only works with other `bun` processes. +🟡 Missing `Stream` stdio, `proc.gid` `proc.uid`. IPC cannot send socket handles and only works with other `bun` processes. ### [`node:cluster`](https://nodejs.org/api/cluster.html) diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 0d96332e1f..fd9009f8b3 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1216,13 +1216,23 @@ pub const Formatter = struct { const CellType = JSC.C.CellType; threadlocal var name_buf: [512]u8 = undefined; + /// https://console.spec.whatwg.org/#formatter + const PercentTag = enum { + s, // s + i, // i or d + f, // f + o, // o + O, // O + c, // c + }; + fn writeWithFormatting( this: *ConsoleObject.Formatter, comptime Writer: type, writer_: Writer, comptime Slice: type, slice_: Slice, - globalThis: *JSGlobalObject, + global: *JSGlobalObject, comptime enable_ansi_colors: bool, ) void { var writer = WrappedWriter(Writer){ @@ -1246,12 +1256,13 @@ pub const Formatter = struct { if (i >= len) break; - const token = switch (slice[i]) { - 's' => Tag.String, - 'f' => Tag.Double, - 'o' => Tag.Undefined, - 'O' => Tag.Object, - 'd', 'i' => Tag.Integer, + const token: PercentTag = switch (slice[i]) { + 's' => .s, + 'f' => .f, + 'o' => .o, + 'O' => .O, + 'd', 'i' => .i, + 'c' => .c, else => continue, }; @@ -1268,16 +1279,143 @@ pub const Formatter = struct { len = @as(u32, @truncate(slice.len)); const next_value = this.remaining_values[0]; this.remaining_values = this.remaining_values[1..]; + + // https://console.spec.whatwg.org/#formatter + const max_before_e_notation = 1000000000000000000000; + const min_before_e_notation = 0.000001; switch (token) { - Tag.String => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Double => this.printAs(Tag.Double, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Object => this.printAs(Tag.Object, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), - Tag.Integer => this.printAs(Tag.Integer, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + .s => this.printAs(Tag.String, Writer, writer_, next_value, next_value.jsType(), enable_ansi_colors), + .i => { + // 1. If Type(current) is Symbol, let converted be NaN + // 2. Otherwise, let converted be the result of Call(%parseInt%, undefined, current, 10) + const int: i64 = brk: { + // This logic is convoluted because %parseInt% will coerce the argument to a string + // first. As an optimization, we can check if the argument is a number and + // skip such coercion. + if (next_value.isInt32()) { + // Already an int, parseInt will parse to itself. + break :brk next_value.asInt32(); + } - // undefined is overloaded to mean the '%o" field - Tag.Undefined => this.format(Tag.get(next_value, globalThis), Writer, writer_, next_value, globalThis, enable_ansi_colors), + if (next_value.isNumber() or !next_value.isSymbol()) double_convert: { + var value = next_value.coerceToDouble(global); - else => unreachable, + if (!std.math.isFinite(value)) { + // for NaN and the string Infinity and -Infinity, parseInt returns NaN + break :double_convert; + } + + // simulate parseInt, which converts the argument to a string and + // then back to a number, without converting it to a string + if (value == 0) { + break :brk 0; + } + + const sign: i64 = if (value < 0) -1 else 1; + value = @abs(value); + if (value >= max_before_e_notation) { + // toString prints 1.000+e0, which parseInt will stop at + // the '.' or the '+', this gives us a single digit value. + while (value >= 10) value /= 10; + break :brk @as(i64, @intFromFloat(@floor(value))) * sign; + } else if (value < min_before_e_notation) { + // toString prints 1.000-e0, which parseInt will stop at + // the '.' or the '-', this gives us a single digit value. + while (value < 1) value *= 10; + break :brk @as(i64, @intFromFloat(@floor(value))) * sign; + } + + // parsing stops at '.', so this is equal to @floor + break :brk @as(i64, @intFromFloat(@floor(value))) * sign; + } + + // for NaN and the string Infinity and -Infinity, parseInt returns NaN + this.addForNewLine("NaN".len); + writer.print("NaN", .{}); + continue; + }; + + if (int < std.math.maxInt(u32)) { + const is_negative = int < 0; + const digits = if (i != 0) + bun.fmt.fastDigitCount(@as(u64, @intCast(@abs(int)))) + @as(u64, @intFromBool(is_negative)) + else + 1; + this.addForNewLine(digits); + } else { + this.addForNewLine(bun.fmt.count("{d}", .{int})); + } + writer.print("{d}", .{int}); + }, + + .f => { + // 1. If Type(current) is Symbol, let converted be NaN + // 2. Otherwise, let converted be the result of Call(%parseFloat%, undefined, [current]). + const converted: f64 = brk: { + if (next_value.isInt32()) { + const int = next_value.asInt32(); + const is_negative = int < 0; + const digits = if (i != 0) + bun.fmt.fastDigitCount(@as(u64, @intCast(@abs(int)))) + @as(u64, @intFromBool(is_negative)) + else + 1; + this.addForNewLine(digits); + writer.print("{d}", .{int}); + continue; + } + if (next_value.isNumber()) { + break :brk next_value.asNumber(); + } + if (next_value.isSymbol()) { + break :brk std.math.nan(f64); + } + // TODO: this is not perfectly emulating parseFloat, + // because spec says to convert the value to a string + // and then parse as a number, but we are just coercing + // a number. + break :brk next_value.coerceToDouble(global); + }; + + const abs = @abs(converted); + if (abs < max_before_e_notation and abs >= min_before_e_notation) { + this.addForNewLine(bun.fmt.count("{d}", .{converted})); + writer.print("{d}", .{converted}); + } else if (std.math.isNan(converted)) { + this.addForNewLine("NaN".len); + writer.writeAll("NaN"); + } else if (std.math.isInf(converted)) { + this.addForNewLine("Infinity".len + @as(usize, @intFromBool(converted < 0))); + if (converted < 0) { + writer.writeAll("-"); + } + writer.writeAll("Infinity"); + } else { + var buf: [124]u8 = undefined; + const formatted = bun.fmt.FormatDouble.dtoa(&buf, converted); + this.addForNewLine(formatted.len); + writer.print("{s}", .{formatted}); + } + }, + + inline .o, .O => |t| { + if (t == .o) { + // TODO: Node.js applies the following extra formatter options. + // + // this.max_depth = 4; + // this.show_proxy = true; + // this.show_hidden = true; + // + // Spec defines %o as: + // > An object with optimally useful formatting is an + // > implementation-specific, potentially-interactive representation + // > of an object judged to be maximally useful and informative. + } + this.format(Tag.get(next_value, global), Writer, writer_, next_value, global, enable_ansi_colors); + }, + + .c => { + // TODO: Implement %c + }, } if (this.remaining_values.len == 0) break; }, @@ -1523,7 +1661,7 @@ pub const Formatter = struct { parent: JSValue, const enable_ansi_colors = enable_ansi_colors_; pub fn handleFirstProperty(this: *@This(), globalThis: *JSC.JSGlobalObject, value: JSValue) void { - if (!value.jsType().isFunction()) { + if (value.isCell() and !value.jsType().isFunction()) { var writer = WrappedWriter(Writer){ .ctx = this.writer, .failed = false, @@ -1735,6 +1873,7 @@ pub const Formatter = struct { this.writeWithFormatting(Writer, writer_, @TypeOf(slice), slice, this.globalThis, enable_ansi_colors); }, .String => { + // This is called from the '%s' formatter, so it can actually be any value const str: bun.String = bun.String.tryFromJS(value, this.globalThis) orelse { writer.failed = true; return; @@ -1849,8 +1988,10 @@ pub const Formatter = struct { this.addForNewLine("NaN".len); writer.print(comptime Output.prettyFmt("NaN", enable_ansi_colors), .{}); } else { - this.addForNewLine(std.fmt.count("{d}", .{num})); - writer.print(comptime Output.prettyFmt("{d}", enable_ansi_colors), .{num}); + var buf: [124]u8 = undefined; + const formatted = bun.fmt.FormatDouble.dtoaWithNegativeZero(&buf, num); + this.addForNewLine(formatted.len); + writer.print(comptime Output.prettyFmt("{s}", enable_ansi_colors), .{formatted}); } }, .Undefined => { @@ -2735,6 +2876,7 @@ pub const Formatter = struct { writer.writeAll(" />"); }, .Object => { + std.debug.assert(value.isCell()); const prev_quote_strings = this.quote_strings; this.quote_strings = true; defer this.quote_strings = prev_quote_strings; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index f4aa21aae9..8198edbf0d 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -173,9 +173,9 @@ pub const Subprocess = struct { has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), this_jsvalue: JSC.JSValue = .zero, - ipc_mode: IPCMode, + /// `null` indicates all of the IPC data is uninitialized. + ipc_data: ?IPC.IPCData, ipc_callback: JSC.Strong = .{}, - ipc: IPC.IPCData, flags: Flags = .{}, weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, @@ -188,10 +188,14 @@ pub const Subprocess = struct { pub const SignalCode = bun.SignalCode; - pub const IPCMode = enum { - none, - bun, - // json, + pub const Poll = union(enum) { + poll_ref: ?*Async.FilePoll, + wait_thread: WaitThreadPoll, + }; + + pub const WaitThreadPoll = struct { + ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + poll_ref: Async.KeepAlive = .{}, }; pub fn resourceUsage( @@ -234,7 +238,7 @@ pub const Subprocess = struct { } pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { - if (this.ipc_mode != .none) { + if (this.ipc_data != null) { return true; } @@ -648,10 +652,14 @@ pub const Subprocess = struct { } pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { - if (this.ipc_mode == .none) { - global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); + const ipc_data = &(this.ipc_data orelse { + if (this.hasExited()) { + global.throw("Subprocess.send() cannot be used after the process has exited.", .{}); + } else { + global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); + } return .zero; - } + }); if (callFrame.argumentsCount() == 0) { global.throwInvalidArguments("Subprocess.send() requires one argument", .{}); @@ -660,16 +668,16 @@ pub const Subprocess = struct { const value = callFrame.argument(0); - const success = this.ipc.serializeAndSend(global, value); + const success = ipc_data.serializeAndSend(global, value); if (!success) return .zero; return JSC.JSValue.jsUndefined(); } pub fn disconnect(this: *Subprocess) void { - if (this.ipc_mode == .none) return; - this.ipc.socket.close(0, null); - this.ipc_mode = .none; + const ipc_data = this.ipc_data orelse return; + ipc_data.socket.close(0, null); + this.ipc_data = null; } pub fn pid(this: *const Subprocess) i32 { @@ -701,7 +709,7 @@ pub const Subprocess = struct { this.observable_getters.insert(.stdio); var pipes = this.stdio_pipes.items; - if (this.ipc_mode != .none) { + if (this.ipc_data != null) { array.push(global, .null); pipes = pipes[@min(1, pipes.len)..]; } @@ -1577,7 +1585,7 @@ pub const Subprocess = struct { var cmd_value = JSValue.zero; var detached = false; var args = args_; - var ipc_mode = IPCMode.none; + var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null; var ipc_callback: JSValue = .zero; var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); var argv0: ?[*:0]const u8 = null; @@ -1690,18 +1698,36 @@ pub const Subprocess = struct { if (args != .zero and args.isObject()) { // This must run before the stdio parsing happens - if (args.getTruthy(globalThis, "ipc")) |val| { - if (val.isCell() and val.isCallable(globalThis.vm())) { - // In the future, we should add a way to use a different IPC serialization format, specifically `json`. - // but the only use case this has is doing interop with node.js IPC and other programs. - ipc_mode = .bun; - ipc_callback = val.withAsyncContextIfNeeded(globalThis); - - if (Environment.isPosix) { - extra_fds.append(.{ .buffer = {} }) catch { - globalThis.throwOutOfMemory(); - return .zero; + if (!is_sync) { + if (args.getTruthy(globalThis, "ipc")) |val| { + if (val.isCell() and val.isCallable(globalThis.vm())) { + maybe_ipc_mode = ipc_mode: { + if (args.get(globalThis, "serialization")) |mode_val| { + if (mode_val.isString()) { + const mode_str = mode_val.toBunString(globalThis); + defer mode_str.deref(); + const slice = mode_str.toUTF8(bun.default_allocator); + defer slice.deinit(); + break :ipc_mode IPC.Mode.fromString(slice.slice()) orelse { + globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{}); + return .zero; + }; + } else { + globalThis.throwInvalidArguments("serialization must be a 'string'", .{}); + return .zero; + } + } + break :ipc_mode .advanced; }; + + ipc_callback = val.withAsyncContextIfNeeded(globalThis); + + if (Environment.isPosix) { + extra_fds.append(.{ .buffer = {} }) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; + } } } } @@ -1871,13 +1897,8 @@ pub const Subprocess = struct { } } - var windows_ipc_env_buf: if (Environment.isWindows) ["BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_00000000-0000-0000-0000-000000000000\x00".len]u8 else void = undefined; - if (ipc_mode != .none) { - if (comptime is_sync) { - globalThis.throwInvalidArguments("IPC is not supported in Bun.spawnSync", .{}); - return .zero; - } - + var windows_ipc_env_buf: if (Environment.isWindows) ["NODE_CHANNEL_FD=\\\\.\\pipe\\BUN_IPC_00000000-0000-0000-0000-000000000000\x00".len]u8 else void = undefined; + if (!is_sync) if (maybe_ipc_mode) |ipc_mode| { // IPC is currently implemented in a very limited way. // // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special @@ -1890,17 +1911,25 @@ pub const Subprocess = struct { // behavior, where this workaround suffices. // // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: - env_array.ensureUnusedCapacity(allocator, 2) catch |err| return globalThis.handleError(err, "in Bun.spawn"); + env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.handleError(err, "in Bun.spawn"); if (Environment.isPosix) { - env_array.appendAssumeCapacity("BUN_INTERNAL_IPC_FD=3"); + env_array.appendAssumeCapacity("NODE_CHANNEL_FD=3"); } else { const uuid = globalThis.bunVM().rareData().nextUUID(); - const pipe_env = std.fmt.bufPrintZ(&windows_ipc_env_buf, "BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_{s}", .{uuid}) catch |err| switch (err) { + const pipe_env = std.fmt.bufPrintZ( + &windows_ipc_env_buf, + "NODE_CHANNEL_FD=\\\\.\\pipe\\BUN_IPC_{s}", + .{uuid}, + ) catch |err| switch (err) { error.NoSpaceLeft => unreachable, // upper bound for this string is known }; env_array.appendAssumeCapacity(pipe_env); } - } + + env_array.appendAssumeCapacity(switch (ipc_mode) { + inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t), + }); + }; env_array.append(allocator, null) catch { globalThis.throwOutOfMemory(); @@ -1968,8 +1997,8 @@ pub const Subprocess = struct { }; var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; - if (Environment.isPosix) { - if (ipc_mode != .none) { + if (Environment.isPosix and !is_sync) { + if (maybe_ipc_mode != null) { posix_ipc_info = .{ // we initialize ext later in the function .socket = uws.us_socket_from_fd( @@ -2022,32 +2051,41 @@ pub const Subprocess = struct { ), .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), .on_exit_callback = if (on_exit_callback != .zero) JSC.Strong.create(on_exit_callback, globalThis) else .{}, - .ipc_mode = ipc_mode, - // will be assigned in the block below - .ipc = if (Environment.isWindows) .{} else .{ .socket = posix_ipc_info }, - .ipc_callback = if (ipc_callback != .zero) JSC.Strong.create(ipc_callback, globalThis) else undefined, + .ipc_data = if (!is_sync) + if (maybe_ipc_mode) |ipc_mode| + if (Environment.isWindows) .{ + .mode = ipc_mode, + } else .{ + .socket = posix_ipc_info, + .mode = ipc_mode, + } + else + null + else + null, + .ipc_callback = if (ipc_callback != .zero) JSC.Strong.create(ipc_callback, globalThis) else .{}, .flags = .{ .is_sync = is_sync, }, }; subprocess.process.setExitHandler(subprocess); - if (ipc_mode != .none) { + if (subprocess.ipc_data) |*ipc_data| { if (Environment.isPosix) { const ptr = posix_ipc_info.ext(*Subprocess); ptr.?.* = subprocess; } else { - if (subprocess.ipc.configureServer( + if (ipc_data.configureServer( Subprocess, subprocess, - windows_ipc_env_buf["BUN_INTERNAL_IPC_FD=".len..], + windows_ipc_env_buf["NODE_CHANNEL_FD=".len..], ).asErr()) |err| { process_allocator.destroy(subprocess); globalThis.throwValue(err.toJSC(globalThis)); return .zero; } } - subprocess.ipc.writeVersionPacket(); + ipc_data.writeVersionPacket(); } if (subprocess.stdin == .pipe) { @@ -2180,9 +2218,13 @@ pub const Subprocess = struct { } pub fn handleIPCClose(this: *Subprocess) void { - this.ipc_mode = .none; + this.ipc_data = null; this.updateHasPendingActivity(); } + pub fn ipc(this: *Subprocess) *IPC.IPCData { + return &this.ipc_data.?; + } + pub const IPCHandler = IPC.NewIPCHandler(Subprocess); }; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 2f91013e42..f3e549726d 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -103,6 +103,7 @@ extern "C" uint8_t Bun__setExitCode(void*, uint8_t); extern "C" void* Bun__getVM(); extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); extern "C" bool Bun__GlobalObject__hasIPC(JSGlobalObject*); +extern "C" bool Bun__ensureProcessIPCInitialized(JSGlobalObject*); extern "C" const char* Bun__githubURL; extern "C" JSC_DECLARE_HOST_FUNCTION(Bun__Process__send); extern "C" JSC_DECLARE_HOST_FUNCTION(Bun__Process__disconnect); @@ -733,8 +734,10 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e // IPC handlers if (eventName.string() == "message"_s) { if (isAdded) { - if (Bun__GlobalObject__hasIPC(eventEmitter.scriptExecutionContext()->jsGlobalObject()) + auto* global = eventEmitter.scriptExecutionContext()->jsGlobalObject(); + if (Bun__GlobalObject__hasIPC(global) && eventEmitter.listenerCount(eventName) == 1) { + Bun__ensureProcessIPCInitialized(global); eventEmitter.scriptExecutionContext()->refEventLoop(); eventEmitter.m_hasIPCRef = true; } @@ -1675,13 +1678,13 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int JSC::JSArray* resultObject = JSC::jsCast(result); #if OS(WINDOWS) - Zig::GlobalObject* globalThis = jsCast(globalObject); - // Node.js docs - https://nodejs.org/api/process.html#a-note-on-process-io - // > Files: synchronous on Windows and POSIX - // > TTYs (Terminals): asynchronous on Windows, synchronous on POSIX - // > Pipes (and sockets): synchronous on Windows, asynchronous on POSIX - // > Synchronous writes avoid problems such as output written with console.log() or console.error() being unexpectedly interleaved, or not written at all if process.exit() is called before an asynchronous write completes. See process.exit() for more information. - Bun__ForceFileSinkToBeSynchronousOnWindows(globalThis, JSValue::encode(resultObject->getIndex(globalObject, 1))); + Zig::GlobalObject* globalThis = jsCast(globalObject); + // Node.js docs - https://nodejs.org/api/process.html#a-note-on-process-io + // > Files: synchronous on Windows and POSIX + // > TTYs (Terminals): asynchronous on Windows, synchronous on POSIX + // > Pipes (and sockets): synchronous on Windows, asynchronous on POSIX + // > Synchronous writes avoid problems such as output written with console.log() or console.error() being unexpectedly interleaved, or not written at all if process.exit() is called before an asynchronous write completes. See process.exit() for more information. + Bun__ForceFileSinkToBeSynchronousOnWindows(globalThis, JSValue::encode(resultObject->getIndex(globalObject, 1))); #endif return resultObject->getIndex(globalObject, 0); diff --git a/src/bun.js/bindings/DoubleFormatter.cpp b/src/bun.js/bindings/DoubleFormatter.cpp new file mode 100644 index 0000000000..019476f0f1 --- /dev/null +++ b/src/bun.js/bindings/DoubleFormatter.cpp @@ -0,0 +1,11 @@ +#include "root.h" +#include "wtf/dtoa.h" +#include + +/// Must be called with a buffer of exactly 124 +/// Find the length by scanning for the 0 +extern "C" void WTF__dtoa(char* buf_124_bytes, double number) +{ + NumberToStringBuffer& buf = *reinterpret_cast(buf_124_bytes); + WTF::numberToString(number, buf); +} diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 52c5c39612..0215b7ab6f 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -5116,6 +5116,14 @@ pub const JSValue = enum(JSValueReprInt) { } pub fn asInt32(this: JSValue) i32 { + // TODO: add this assertion. currently, there is a mistake in + // argumentCount that mistakenly uses a JSValue instead of a c_int. This + // mistake performs the correct conversion instructions for it's use + // case but is bad code practice to misuse JSValue casts. + // + // if (bun.Environment.allow_assert) { + // std.debug.assert(this.isInt32()); + // } return FFI.JSVALUE_TO_INT32(.{ .asJSValue = this }); } diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 4d4019dbd9..bc8cd134cb 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -14,8 +14,24 @@ const JSGlobalObject = JSC.JSGlobalObject; pub const log = Output.scoped(.IPC, false); -pub const ipcHeaderLength = @sizeOf(u8) + @sizeOf(u32); -pub const ipcVersion = 1; +/// Mode of Inter-Process Communication. +pub const Mode = enum { + /// Uses SerializedScriptValue to send data. Only valid for bun <--> bun communication. + /// The first packet sent here is a version packet so that the version of the other end is known. + advanced, + /// Uses JSON messages, one message per line. + /// This must match the behavior of node.js, and supports bun <--> node.js/etc communication. + json, + + const Map = std.ComptimeStringMap(Mode, .{ + .{ "advanced", .advanced }, + .{ "json", .json }, + }); + + pub fn fromString(s: []const u8) ?Mode { + return Map.get(s); + } +}; pub const DecodedIPCMessage = union(enum) { version: u32, @@ -27,70 +43,198 @@ pub const DecodeIPCMessageResult = struct { message: DecodedIPCMessage, }; -pub const IPCDecodeError = error{ NotEnoughBytes, InvalidFormat }; - -pub const IPCMessageType = enum(u8) { - Version = 1, - SerializedMessage = 2, - _, +pub const IPCDecodeError = error{ + /// There werent enough bytes, recall this function again when new data is available. + NotEnoughBytes, + /// Format could not be recognized. Report an error and close the socket. + InvalidFormat, }; -/// Given potentially unfinished buffer `data`, attempt to decode and process a message from it. -/// Returns `NotEnoughBytes` if there werent enough bytes -/// Returns `InvalidFormat` if the message was invalid, probably close the socket in this case -/// otherwise returns the number of bytes consumed. -pub fn decodeIPCMessage( - data: []const u8, - globalThis: *JSC.JSGlobalObject, -) IPCDecodeError!DecodeIPCMessageResult { - JSC.markBinding(@src()); - if (data.len < ipcHeaderLength) { +pub const IPCSerializationError = error{ + /// Value could not be serialized. + SerializationFailed, + /// Out of memory + OutOfMemory, +}; + +const advanced = struct { + pub const header_length = @sizeOf(IPCMessageType) + @sizeOf(u32); + pub const version: u32 = 1; + + pub const IPCMessageType = enum(u8) { + Version = 1, + SerializedMessage = 2, + _, + }; + + const VersionPacket = extern struct { + type: IPCMessageType align(1) = .Version, + version: u32 align(1) = version, + }; + + pub fn decodeIPCMessage(data: []const u8, global: *JSC.JSGlobalObject) IPCDecodeError!DecodeIPCMessageResult { + if (data.len < header_length) { + log("Not enough bytes to decode IPC message header, have {d} bytes", .{data.len}); + return IPCDecodeError.NotEnoughBytes; + } + + const message_type: IPCMessageType = @enumFromInt(data[0]); + const message_len: u32 = @as(*align(1) const u32, @ptrCast(data[1 .. @sizeOf(u32) + 1])).*; + + log("Received IPC message type {d} ({s}) len {d}", .{ + @intFromEnum(message_type), + std.enums.tagName(IPCMessageType, message_type) orelse "unknown", + message_len, + }); + + switch (message_type) { + .Version => { + return .{ + .bytes_consumed = header_length, + .message = .{ .version = message_len }, + }; + }, + .SerializedMessage => { + if (data.len < (header_length + message_len)) { + log("Not enough bytes to decode IPC message body of len {d}, have {d} bytes", .{ message_len, data.len }); + return IPCDecodeError.NotEnoughBytes; + } + + const message = data[header_length .. header_length + message_len]; + const deserialized = JSValue.deserialize(message, global); + + if (deserialized == .zero) { + return IPCDecodeError.InvalidFormat; + } + + return .{ + .bytes_consumed = header_length + message_len, + .message = .{ .data = deserialized }, + }; + }, + else => { + return IPCDecodeError.InvalidFormat; + }, + } + } + + pub inline fn getVersionPacket() []const u8 { + return comptime std.mem.asBytes(&VersionPacket{}); + } + + pub fn serialize(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { + const serialized = value.serialize(global) orelse + return IPCSerializationError.SerializationFailed; + defer serialized.deinit(); + + const size: u32 = @intCast(serialized.data.len); + + const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; + + try writer.ensureUnusedCapacity(payload_length); + + writer.writeTypeAsBytesAssumeCapacity(IPCMessageType, .SerializedMessage); + writer.writeTypeAsBytesAssumeCapacity(u32, size); + writer.writeAssumeCapacity(serialized.data); + + return payload_length; + } +}; + +const json = struct { + fn jsonIPCDataStringFreeCB(context: *anyopaque, _: *anyopaque, _: u32) callconv(.C) void { + @as(*bool, @ptrCast(context)).* = true; + } + + pub fn getVersionPacket() []const u8 { + return &.{}; + } + + pub fn decodeIPCMessage( + data: []const u8, + globalThis: *JSC.JSGlobalObject, + ) IPCDecodeError!DecodeIPCMessageResult { + if (bun.strings.indexOfChar(data, '\n')) |idx| { + const json_data = data[0..idx]; + + const is_ascii = bun.strings.isAllASCII(json_data); + var was_ascii_string_freed = false; + + // Use ExternalString to avoid copying data if possible. + // This is only possible for ascii data, as that fits into latin1 + // otherwise we have to convert it utf-8 into utf16-le. + var str = if (is_ascii) + bun.String.createExternal(json_data, true, &was_ascii_string_freed, jsonIPCDataStringFreeCB) + else + bun.String.fromUTF8(json_data); + defer { + str.deref(); + if (is_ascii and !was_ascii_string_freed) { + @panic("Expected ascii string to be freed by ExternalString, but it wasn't. This is a bug in Bun."); + } + } + + const deserialized = str.toJSByParseJSON(globalThis); + + return .{ + .bytes_consumed = idx + 1, + .message = .{ .data = deserialized }, + }; + } return IPCDecodeError.NotEnoughBytes; } - const message_type: IPCMessageType = @enumFromInt(data[0]); - const message_len: u32 = @as(*align(1) const u32, @ptrCast(data[1 .. @sizeOf(u32) + 1])).*; + pub fn serialize(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { + var out: bun.String = undefined; + value.jsonStringify(global, 0, &out); + defer out.deref(); - log("Received IPC message type {d} ({s}) len {d}", .{ - @intFromEnum(message_type), - std.enums.tagName(IPCMessageType, message_type) orelse "unknown", - message_len, - }); + if (out.tag == .Dead) return IPCSerializationError.SerializationFailed; - switch (message_type) { - .Version => { - return .{ - .bytes_consumed = ipcHeaderLength, - .message = .{ .version = message_len }, - }; - }, - .SerializedMessage => { - if (data.len < (ipcHeaderLength + message_len)) { - return IPCDecodeError.NotEnoughBytes; - } + // TODO: it would be cool to have a 'toUTF8Into' which can write directly into 'ipc_data.outgoing.list' + const str = out.toUTF8(bun.default_allocator); + defer str.deinit(); - const message = data[ipcHeaderLength .. ipcHeaderLength + message_len]; - const deserialized = JSValue.deserialize(message, globalThis); + const slice = str.slice(); - if (deserialized == .zero) { - return IPCDecodeError.InvalidFormat; - } + try writer.ensureUnusedCapacity(slice.len + 1); - return .{ - .bytes_consumed = ipcHeaderLength + message_len, - .message = .{ .data = deserialized }, - }; - }, - else => { - return IPCDecodeError.InvalidFormat; - }, + writer.writeAssumeCapacity(slice); + writer.writeAssumeCapacity("\n"); + + return slice.len + 1; } +}; + +/// Given potentially unfinished buffer `data`, attempt to decode and process a message from it. +pub fn decodeIPCMessage(mode: Mode, data: []const u8, global: *JSC.JSGlobalObject) IPCDecodeError!DecodeIPCMessageResult { + return switch (mode) { + inline else => |t| @field(@This(), @tagName(t)).decodeIPCMessage(data, global), + }; +} + +/// Returns the initialization packet for the given mode. Can be zero-length. +pub fn getVersionPacket(mode: Mode) []const u8 { + return switch (mode) { + inline else => |t| @field(@This(), @tagName(t)).getVersionPacket(), + }; +} + +/// Given a writer interface, serialize and write a value. +/// Returns true if the value was written, false if it was not. +pub fn serialize(data: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { + return switch (data.mode) { + inline else => |t| @field(@This(), @tagName(t)).serialize(data, writer, global, value), + }; } pub const Socket = uws.NewSocketHandler(false); -pub const SocketIPCData = struct { +/// Used on POSIX +const SocketIPCData = struct { socket: Socket, + mode: Mode, + incoming: bun.ByteList = .{}, // Maybe we should use StreamBuffer here as well outgoing: bun.io.StreamBuffer = .{}, @@ -100,45 +244,33 @@ pub const SocketIPCData = struct { if (Environment.allow_assert) { std.debug.assert(this.has_written_version == 0); } - const VersionPacket = extern struct { - type: IPCMessageType align(1) = .Version, - version: u32 align(1) = ipcVersion, - }; - const bytes = comptime std.mem.asBytes(&VersionPacket{}); - const n = this.socket.write(bytes, false); - if (n != bytes.len) { - this.outgoing.write(bytes) catch bun.outOfMemory(); + const bytes = getVersionPacket(this.mode); + if (bytes.len > 0) { + const n = this.socket.write(bytes, false); + if (n != bytes.len) { + this.outgoing.write(bytes[@intCast(n)..]) catch bun.outOfMemory(); + } } if (Environment.allow_assert) { this.has_written_version = 1; } } - pub fn serializeAndSend(ipc_data: *SocketIPCData, globalThis: *JSGlobalObject, value: JSValue) bool { + pub fn serializeAndSend(ipc_data: *SocketIPCData, global: *JSGlobalObject, value: JSValue) bool { if (Environment.allow_assert) { std.debug.assert(ipc_data.has_written_version == 1); } - const serialized = value.serialize(globalThis) orelse return false; - defer serialized.deinit(); - - const size: u32 = @intCast(serialized.data.len); - - const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; - - ipc_data.outgoing.ensureUnusedCapacity(payload_length) catch bun.outOfMemory(); - //TODO: probably we should not direct access ipc_data.outgoing.list.items here + // TODO: probably we should not direct access ipc_data.outgoing.list.items here const start_offset = ipc_data.outgoing.list.items.len; - ipc_data.outgoing.writeTypeAsBytesAssumeCapacity(u8, @intFromEnum(IPCMessageType.SerializedMessage)); - ipc_data.outgoing.writeTypeAsBytesAssumeCapacity(u32, size); - ipc_data.outgoing.writeAssumeCapacity(serialized.data); + const payload_length = serialize(ipc_data, &ipc_data.outgoing, global, value) catch + return false; std.debug.assert(ipc_data.outgoing.list.items.len == start_offset + payload_length); if (start_offset == 0) { std.debug.assert(ipc_data.outgoing.cursor == 0); - const n = ipc_data.socket.write(ipc_data.outgoing.list.items.ptr[start_offset..payload_length], false); if (n == payload_length) { ipc_data.outgoing.reset(); @@ -151,17 +283,23 @@ pub const SocketIPCData = struct { } }; +/// Used on Windows const NamedPipeIPCData = struct { const uv = bun.windows.libuv; + + mode: Mode, + // we will use writer pipe as Duplex writer: bun.io.StreamingWriter(NamedPipeIPCData, onWrite, onError, null, onClientClose) = .{}, incoming: bun.ByteList = .{}, // Maybe we should use IPCBuffer here as well connected: bool = false, - has_written_version: if (Environment.allow_assert) u1 else u0 = 0, connect_req: uv.uv_connect_t = std.mem.zeroes(uv.uv_connect_t), server: ?*uv.Pipe = null, onClose: ?CloseHandler = null, + + has_written_version: if (Environment.allow_assert) u1 else u0 = 0, + const CloseHandler = struct { callback: *const fn (*anyopaque) void, context: *anyopaque, @@ -210,42 +348,29 @@ const NamedPipeIPCData = struct { if (Environment.allow_assert) { std.debug.assert(this.has_written_version == 0); } - const VersionPacket = extern struct { - type: IPCMessageType align(1) = .Version, - version: u32 align(1) = ipcVersion, - }; - + const bytes = getVersionPacket(this.mode); + if (bytes.len > 0) { + if (this.connected) { + _ = this.writer.write(bytes); + } else { + // enqueue to be sent after connecting + this.writer.outgoing.write(bytes) catch bun.outOfMemory(); + } + } if (Environment.allow_assert) { this.has_written_version = 1; } - const bytes = comptime std.mem.asBytes(&VersionPacket{}); - if (this.connected) { - _ = this.writer.write(bytes); - } else { - // enqueue to be sent after connecting - this.writer.outgoing.write(bytes) catch bun.outOfMemory(); - } } - pub fn serializeAndSend(this: *NamedPipeIPCData, globalThis: *JSGlobalObject, value: JSValue) bool { + pub fn serializeAndSend(this: *NamedPipeIPCData, global: *JSGlobalObject, value: JSValue) bool { if (Environment.allow_assert) { std.debug.assert(this.has_written_version == 1); } - const serialized = value.serialize(globalThis) orelse return false; - defer serialized.deinit(); - - const size: u32 = @intCast(serialized.data.len); - log("serializeAndSend {d}", .{size}); - - const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; - - this.writer.outgoing.ensureUnusedCapacity(payload_length) catch @panic("OOM"); const start_offset = this.writer.outgoing.list.items.len; - this.writer.outgoing.writeTypeAsBytesAssumeCapacity(u8, @intFromEnum(IPCMessageType.SerializedMessage)); - this.writer.outgoing.writeTypeAsBytesAssumeCapacity(u32, size); - this.writer.outgoing.writeAssumeCapacity(serialized.data); + const payload_length: usize = serialize(this, &this.writer.outgoing, global, value) catch + return false; std.debug.assert(this.writer.outgoing.list.items.len == start_offset + payload_length); @@ -327,7 +452,8 @@ const NamedPipeIPCData = struct { pub const IPCData = if (Environment.isWindows) NamedPipeIPCData else SocketIPCData; -pub fn NewSocketIPCHandler(comptime Context: type) type { +/// Used on POSIX +fn NewSocketIPCHandler(comptime Context: type) type { return struct { pub fn onOpen( _: *anyopaque, @@ -348,7 +474,7 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { _: c_int, _: ?*anyopaque, ) void { - // ?! does uSockets .close call onClose? + // Note: uSockets has already freed the underlying socket, so calling Socket.close() can segfault log("onClose\n", .{}); this.handleIPCClose(); } @@ -356,9 +482,10 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { pub fn onData( this: *Context, socket: Socket, - data_: []const u8, + all_data: []const u8, ) void { - var data = data_; + var data = all_data; + const ipc = this.ipc(); log("onData {}", .{std.fmt.fmtSliceHexLower(data)}); // In the VirtualMachine case, `globalThis` is an optional, in case @@ -378,11 +505,11 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { // Decode the message with just the temporary buffer, and if that // fails (not enough bytes) then we allocate to .ipc_buffer - if (this.ipc.incoming.len == 0) { + if (ipc.incoming.len == 0) { while (true) { - const result = decodeIPCMessage(data, globalThis) catch |e| switch (e) { + const result = decodeIPCMessage(ipc.mode, data, globalThis) catch |e| switch (e) { error.NotEnoughBytes => { - _ = this.ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); + _ = ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); log("hit NotEnoughBytes", .{}); return; }, @@ -404,15 +531,15 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { } } - _ = this.ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); + _ = ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); - var slice = this.ipc.incoming.slice(); + var slice = ipc.incoming.slice(); while (true) { - const result = decodeIPCMessage(slice, globalThis) catch |e| switch (e) { + const result = decodeIPCMessage(ipc.mode, slice, globalThis) catch |e| switch (e) { error.NotEnoughBytes => { // copy the remaining bytes to the start of the buffer - bun.copy(u8, this.ipc.incoming.ptr[0..slice.len], slice); - this.ipc.incoming.len = @truncate(slice.len); + bun.copy(u8, ipc.incoming.ptr[0..slice.len], slice); + ipc.incoming.len = @truncate(slice.len); log("hit NotEnoughBytes2", .{}); return; }, @@ -430,7 +557,7 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { slice = slice[result.bytes_consumed..]; } else { // clear the buffer - this.ipc.incoming.len = 0; + ipc.incoming.len = 0; return; } } @@ -440,18 +567,18 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { context: *Context, socket: Socket, ) void { - const to_write = context.ipc.outgoing.slice(); + const to_write = context.ipc().outgoing.slice(); if (to_write.len == 0) { - context.ipc.outgoing.reset(); - context.ipc.outgoing.reset(); + context.ipc().outgoing.reset(); + context.ipc().outgoing.reset(); return; } const n = socket.write(to_write, false); if (n == to_write.len) { - context.ipc.outgoing.reset(); - context.ipc.outgoing.reset(); + context.ipc().outgoing.reset(); + context.ipc().outgoing.reset(); } else if (n > 0) { - context.ipc.outgoing.cursor += @intCast(n); + context.ipc().outgoing.cursor += @intCast(n); } } @@ -480,14 +607,16 @@ pub fn NewSocketIPCHandler(comptime Context: type) type { }; } +/// Used on Windows fn NewNamedPipeIPCHandler(comptime Context: type) type { const uv = bun.windows.libuv; return struct { fn onReadAlloc(this: *Context, suggested_size: usize) []u8 { - var available = this.ipc.incoming.available(); + const ipc = this.ipc(); + var available = ipc.incoming.available(); if (available.len < suggested_size) { - this.ipc.incoming.ensureUnusedCapacity(bun.default_allocator, suggested_size) catch bun.outOfMemory(); - available = this.ipc.incoming.available(); + ipc.incoming.ensureUnusedCapacity(bun.default_allocator, suggested_size) catch bun.outOfMemory(); + available = ipc.incoming.available(); } log("onReadAlloc {d}", .{suggested_size}); return available.ptr[0..suggested_size]; @@ -495,16 +624,18 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { fn onReadError(this: *Context, err: bun.C.E) void { log("onReadError {}", .{err}); - this.ipc.close(); + this.ipc().close(); } fn onRead(this: *Context, buffer: []const u8) void { - log("onRead {d}", .{buffer.len}); - this.ipc.incoming.len += @as(u32, @truncate(buffer.len)); - var slice = this.ipc.incoming.slice(); + const ipc = this.ipc(); - std.debug.assert(this.ipc.incoming.len <= this.ipc.incoming.cap); - std.debug.assert(bun.isSliceInBuffer(buffer, this.ipc.incoming.allocatedSlice())); + log("onRead {d}", .{buffer.len}); + ipc.incoming.len += @as(u32, @truncate(buffer.len)); + var slice = ipc.incoming.slice(); + + std.debug.assert(ipc.incoming.len <= ipc.incoming.cap); + std.debug.assert(bun.isSliceInBuffer(buffer, ipc.incoming.allocatedSlice())); const globalThis = switch (@typeInfo(@TypeOf(this.globalThis))) { .Pointer => this.globalThis, @@ -512,23 +643,23 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { if (this.globalThis) |global| { break :brk global; } - this.ipc.close(); + ipc.close(); return; }, else => @panic("Unexpected globalThis type: " ++ @typeName(@TypeOf(this.globalThis))), }; while (true) { - const result = decodeIPCMessage(slice, globalThis) catch |e| switch (e) { + const result = decodeIPCMessage(ipc.mode, slice, globalThis) catch |e| switch (e) { error.NotEnoughBytes => { // copy the remaining bytes to the start of the buffer - bun.copy(u8, this.ipc.incoming.ptr[0..slice.len], slice); - this.ipc.incoming.len = @truncate(slice.len); + bun.copy(u8, ipc.incoming.ptr[0..slice.len], slice); + ipc.incoming.len = @truncate(slice.len); log("hit NotEnoughBytes2", .{}); return; }, error.InvalidFormat => { Output.printErrorln("InvalidFormatError during IPC message handling", .{}); - this.ipc.close(); + ipc.close(); return; }, }; @@ -539,19 +670,20 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { slice = slice[result.bytes_consumed..]; } else { // clear the buffer - this.ipc.incoming.len = 0; + ipc.incoming.len = 0; return; } } } pub fn onNewClientConnect(this: *Context, status: uv.ReturnCode) void { + const ipc = this.ipc(); log("onNewClientConnect {d}", .{status.int()}); if (status.errEnum()) |_| { Output.printErrorln("Failed to connect IPC pipe", .{}); return; } - const server = this.ipc.server orelse { + const server = ipc.server orelse { Output.printErrorln("Failed to connect IPC pipe", .{}); return; }; @@ -562,7 +694,7 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { return; }; - this.ipc.writer.startWithPipe(client).unwrap() catch { + ipc.writer.startWithPipe(client).unwrap() catch { bun.default_allocator.destroy(client); Output.printErrorln("Failed to start IPC pipe", .{}); return; @@ -570,17 +702,17 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { switch (server.accept(client)) { .err => { - this.ipc.close(); + ipc.close(); return; }, .result => { - this.ipc.connected = true; + ipc.connected = true; client.readStart(this, onReadAlloc, onReadError, onRead).unwrap() catch { - this.ipc.close(); + ipc.close(); Output.printErrorln("Failed to connect IPC pipe", .{}); return; }; - _ = this.ipc.writer.flush(); + _ = ipc.writer.flush(); }, } } @@ -590,25 +722,27 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { } fn onConnect(this: *Context, status: uv.ReturnCode) void { + const ipc = this.ipc(); + log("onConnect {d}", .{status.int()}); - this.ipc.connected = true; + ipc.connected = true; if (status.errEnum()) |_| { Output.printErrorln("Failed to connect IPC pipe", .{}); return; } - const stream = this.ipc.writer.getStream() orelse { - this.ipc.close(); + const stream = ipc.writer.getStream() orelse { + ipc.close(); Output.printErrorln("Failed to connect IPC pipe", .{}); return; }; stream.readStart(this, onReadAlloc, onReadError, onRead).unwrap() catch { - this.ipc.close(); + ipc.close(); Output.printErrorln("Failed to connect IPC pipe", .{}); return; }; - _ = this.ipc.writer.flush(); + _ = ipc.writer.flush(); } }; } @@ -618,12 +752,9 @@ fn NewNamedPipeIPCHandler(comptime Context: type) type { /// `Context` must be a struct that implements this interface: /// struct { /// globalThis: ?*JSGlobalObject, -/// ipc: IPCData, /// +/// fn ipc(*Context) *IPCData, /// fn handleIPCMessage(*Context, DecodedIPCMessage) void /// fn handleIPCClose(*Context) void /// } -pub fn NewIPCHandler(comptime Context: type) type { - const IPCHandler = if (Environment.isWindows) NewNamedPipeIPCHandler else NewSocketIPCHandler; - return IPCHandler(Context); -} +pub const NewIPCHandler = if (Environment.isWindows) NewNamedPipeIPCHandler else NewSocketIPCHandler; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index c142d172ba..91222ed2a6 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -300,8 +300,8 @@ pub export fn Bun__Process__send( return .zero; } const vm = globalObject.bunVM(); - if (vm.ipc) |ipc_instance| { - const success = ipc_instance.ipc.serializeAndSend(globalObject, callFrame.argument(0)); + if (vm.getIPCInstance()) |ipc_instance| { + const success = ipc_instance.data.serializeAndSend(globalObject, callFrame.argument(0)); return if (success) .undefined else .zero; } else { globalObject.throw("IPC Socket is no longer open.", .{}); @@ -323,6 +323,15 @@ pub export fn Bun__Process__disconnect( return .undefined; } +/// When IPC environment variables are passed, the socket is not immediately opened, +/// but rather we wait for process.on('message') or process.send() to be called, THEN +/// we open the socket. This is to avoid missing messages at the start of the program. +pub export fn Bun__ensureProcessIPCInitialized(globalObject: *JSGlobalObject) void { + // getIPC() will initialize a "waiting" ipc instance so this is enough. + // it will do nothing if IPC is not enabled. + _ = globalObject.bunVM().getIPCInstance(); +} + /// This function is called on the main thread /// The bunVM() call will assert this pub export fn Bun__queueTask(global: *JSGlobalObject, task: *JSC.CppTask) void { @@ -598,7 +607,7 @@ pub const VirtualMachine = struct { gc_controller: JSC.GarbageCollectionController = .{}, worker: ?*JSC.WebWorker = null, - ipc: ?*IPCInstance = null, + ipc: ?IPCInstanceUnion = null, debugger: ?Debugger = null, has_started_debugger: bool = false, @@ -767,13 +776,20 @@ pub const VirtualMachine = struct { this.hide_bun_stackframes = false; } - if (map.map.fetchSwapRemove("BUN_INTERNAL_IPC_FD")) |kv| { + if (map.map.fetchSwapRemove("NODE_CHANNEL_FD")) |kv| { + const mode = if (map.map.fetchSwapRemove("NODE_CHANNEL_SERIALIZATION_MODE")) |mode_kv| + IPC.Mode.fromString(mode_kv.value.value) orelse .json + else + .json; + IPC.log("IPC environment variables: NODE_CHANNEL_FD={d}, NODE_CHANNEL_SERIALIZATION_MODE={s}", .{ kv.value.value, @tagName(mode) }); if (Environment.isWindows) { - this.initIPCInstance(kv.value.value); - } else if (std.fmt.parseInt(i32, kv.value.value, 10) catch null) |fd| { - this.initIPCInstance(bun.toFD(fd)); + this.initIPCInstance(kv.value.value, mode); } else { - Output.printErrorln("Failed to parse BUN_INTERNAL_IPC_FD", .{}); + if (std.fmt.parseInt(i32, kv.value.value, 10)) |fd| { + this.initIPCInstance(bun.toFD(fd), mode); + } else |_| { + Output.warn("Failed to parse IPC channel number '{s}'", .{kv.value.value}); + } } } @@ -3207,13 +3223,27 @@ pub const VirtualMachine = struct { extern fn Process__emitMessageEvent(global: *JSGlobalObject, value: JSValue) void; extern fn Process__emitDisconnectEvent(global: *JSGlobalObject) void; + pub const IPCInstanceUnion = union(enum) { + /// IPC is put in this "enabled but not started" state when IPC is detected + /// but the client JavaScript has not yet done `.on("message")` + waiting: struct { + info: IPCInfoType, + mode: IPC.Mode, + }, + initialized: *IPCInstance, + }; + pub const IPCInstance = struct { globalThis: ?*JSGlobalObject, context: if (Environment.isPosix) *uws.SocketContext else u0, - ipc: IPC.IPCData, + data: IPC.IPCData, pub usingnamespace bun.New(@This()); + pub fn ipc(this: *IPCInstance) *IPC.IPCData { + return &this.data; + } + pub fn handleIPCMessage( this: *IPCInstance, message: IPC.DecodedIPCMessage, @@ -3235,7 +3265,6 @@ pub const VirtualMachine = struct { } pub fn handleIPCClose(this: *IPCInstance) void { - JSC.markBinding(@src()); if (this.globalThis) |global| { var vm = global.bunVM(); vm.ipc = null; @@ -3251,45 +3280,70 @@ pub const VirtualMachine = struct { }; const IPCInfoType = if (Environment.isWindows) []const u8 else bun.FileDescriptor; - pub fn initIPCInstance(this: *VirtualMachine, info: IPCInfoType) void { - if (Environment.isWindows) { - var instance = IPCInstance.new(.{ - .globalThis = this.global, - .context = 0, - .ipc = .{}, - }); - instance.ipc.configureClient(IPCInstance, instance, info) catch { - instance.destroy(); - Output.printErrorln("Unable to start IPC pipe", .{}); - return; - }; - - this.ipc = instance; - instance.ipc.writeVersionPacket(); - return; - } - this.event_loop.ensureWaker(); - const context = uws.us_create_socket_context(0, this.event_loop_handle.?, @sizeOf(usize), .{}).?; - IPC.Socket.configure(context, true, *IPCInstance, IPCInstance.Handlers); - - var instance = IPCInstance.new(.{ - .globalThis = this.global, - .context = context, - .ipc = undefined, - }); - const socket = IPC.Socket.fromFd(context, info, IPCInstance, instance, null) orelse { - instance.destroy(); - Output.printErrorln("Unable to start IPC socket", .{}); - return; + pub fn initIPCInstance(this: *VirtualMachine, info: IPCInfoType, mode: IPC.Mode) void { + IPC.log("initIPCInstance {" ++ (if (Environment.isWindows) "s" else "") ++ "}", .{info}); + this.ipc = .{ + .waiting = .{ .info = info, .mode = mode }, }; - socket.setTimeout(0); - instance.ipc = .{ .socket = socket }; - - const ptr = socket.ext(*IPCInstance); - ptr.?.* = instance; - this.ipc = instance; - instance.ipc.writeVersionPacket(); } + + pub fn getIPCInstance(this: *VirtualMachine) ?*IPCInstance { + if (this.ipc == null) return null; + if (this.ipc.? != .waiting) return this.ipc.?.initialized; + const opts = this.ipc.?.waiting; + + IPC.log("getIPCInstance {" ++ (if (Environment.isWindows) "s" else "") ++ "}", .{opts.info}); + + this.event_loop.ensureWaker(); + + const instance = switch (Environment.os) { + else => instance: { + const context = uws.us_create_socket_context(0, this.event_loop_handle.?, @sizeOf(usize), .{}).?; + IPC.Socket.configure(context, true, *IPCInstance, IPCInstance.Handlers); + + var instance = IPCInstance.new(.{ + .globalThis = this.global, + .context = context, + .data = undefined, + }); + + const socket = IPC.Socket.fromFd(context, opts.info, IPCInstance, instance, null) orelse { + instance.destroy(); + this.ipc = null; + Output.warn("Unable to start IPC socket", .{}); + return null; + }; + socket.setTimeout(0); + + instance.data = .{ .socket = socket, .mode = opts.mode }; + + break :instance instance; + }, + .windows => instance: { + var instance = IPCInstance.new(.{ + .globalThis = this.global, + .context = 0, + .data = .{ .mode = opts.mode }, + }); + + instance.data.configureClient(IPCInstance, instance, opts.info) catch { + instance.destroy(); + this.ipc = null; + Output.warn("Unable to start IPC pipe '{s}'", .{opts.info}); + return null; + }; + + break :instance instance; + }, + }; + + this.ipc = .{ .initialized = instance }; + + instance.data.writeVersionPacket(); + + return instance; + } + comptime { if (!JSC.is_bindgen) _ = Bun__remapStackFramePositions; diff --git a/src/cli.zig b/src/cli.zig index 5449eb8e77..9eaa09a4c3 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -21,6 +21,8 @@ const js_ast = bun.JSAst; const linker = @import("linker.zig"); const RegularExpression = bun.RegularExpression; +const debug = Output.scoped(.CLI, true); + const sync = @import("./sync.zig"); const Api = @import("api/schema.zig").Api; const resolve_path = @import("./resolver/resolve_path.zig"); @@ -1393,6 +1395,8 @@ pub const Command = struct { return; } + debug("argv: [{s}]", .{bun.fmt.fmtSlice(bun.argv(), ", ")}); + const tag = which(); switch (tag) { diff --git a/src/codegen/client-js.ts b/src/codegen/client-js.ts index d50368a995..6ae7a3863a 100644 --- a/src/codegen/client-js.ts +++ b/src/codegen/client-js.ts @@ -1,8 +1,10 @@ import { pathToUpperSnakeCase } from "./helpers"; // This is the implementation for $debug +// TODO: interop with $BUN_DEBUG export function createLogClientJS(filepath: string, publicName: string) { return ` +let $debug_trace = Bun.env.TRACE && Bun.env.TRACE === '1'; let $debug_log_enabled = ((env) => ( // The rationale for checking all these variables is just so you don't have to exactly remember which one you set. (env.BUN_DEBUG_ALL && env.BUN_DEBUG_ALL !== '0') @@ -13,7 +15,7 @@ let $debug_log_enabled = ((env) => ( let $debug_pid_prefix = Bun.env.SHOW_PID === '1'; let $debug_log = $debug_log_enabled ? (...args) => { // warn goes to stderr without colorizing - console.warn(($debug_pid_prefix ? \`[\${process.pid}] \` : '') + (Bun.enableANSIColors ? '\\x1b[90m[${publicName}]\\x1b[0m' : '[${publicName}]'), ...args); + console[$debug_trace ? 'trace' : 'warn'](($debug_pid_prefix ? \`[\${process.pid}] \` : '') + (Bun.enableANSIColors ? '\\x1b[90m[${publicName}]\\x1b[0m' : '[${publicName}]'), ...args); } : () => {}; `; } diff --git a/src/fmt.zig b/src/fmt.zig index a66d56cd1f..8933849549 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1275,3 +1275,34 @@ fn FormatSlice(comptime T: type, comptime delim: []const u8) type { } }; } + +/// Uses WebKit's double formatter +pub fn fmtDouble(number: f64) FormatDouble { + return .{ .number = number }; +} + +pub const FormatDouble = struct { + number: f64, + + extern "C" fn WTF__dtoa(buf_124_bytes: *[124]u8, number: f64) void; + + pub fn dtoa(buf: *[124]u8, number: f64) []const u8 { + WTF__dtoa(buf, number); + return bun.sliceTo(buf, 0); + } + + pub fn dtoaWithNegativeZero(buf: *[124]u8, number: f64) []const u8 { + if (std.math.isNegativeZero(number)) { + return "-0"; + } + + WTF__dtoa(buf, number); + return bun.sliceTo(buf, 0); + } + + pub fn format(self: @This(), comptime _: []const u8, _: fmt.FormatOptions, writer: anytype) !void { + var buf: [124]u8 = undefined; + const slice = dtoa(&buf, self.number); + try writer.writeAll(slice); + } +}; diff --git a/src/js/node/child_process.js b/src/js/node/child_process.js index 6e37c11f75..96f4b255f1 100644 --- a/src/js/node/child_process.js +++ b/src/js/node/child_process.js @@ -69,9 +69,6 @@ var ReadableFromWeb; // gid Sets the group identity of the process (see setgid(2)). // detached Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). -// TODO: After IPC channels can be opened -// serialization Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. - // TODO: Add support for ipc option, verify only one IPC channel in array // stdio | Child's stdio configuration (see options.stdio). // Support wrapped ipc types (e.g. net.Socket, dgram.Socket, TTY, etc.) @@ -1150,24 +1147,8 @@ class ChildProcess extends EventEmitter { spawn(options) { validateObject(options, "options"); - // validateOneOf(options.serialization, "options.serialization", [ - // undefined, - // "json", - // // "advanced", // TODO - // ]); - // const serialization = options.serialization || "json"; - - // if (ipc !== undefined) { - // // Let child process know about opened IPC channel - // if (options.envPairs === undefined) options.envPairs = []; - // else validateArray(options.envPairs, "options.envPairs"); - - // $arrayPush(options.envPairs, `NODE_CHANNEL_FD=${ipcFd}`); - // $arrayPush( - // options.envPairs, - // `NODE_CHANNEL_SERIALIZATION_MODE=${serialization}` - // ); - // } + validateOneOf(options.serialization, "options.serialization", [undefined, "json", "advanced"]); + const serialization = options.serialization || "json"; validateString(options.file, "options.file"); // NOTE: This is confusing... So node allows you to pass a file name @@ -1222,6 +1203,7 @@ class ChildProcess extends EventEmitter { }, lazy: true, ipc: ipc ? this.#emitIpcMessage.bind(this) : undefined, + serialization, argv0, }); this.pid = this.#handle.pid; diff --git a/src/output.zig b/src/output.zig index f70ba20f24..36c274f14b 100644 --- a/src/output.zig +++ b/src/output.zig @@ -590,10 +590,10 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn { if (!evaluated_disable) { evaluated_disable = true; - if (bun.getenvZ("BUN_DEBUG_ALL") != null or - bun.getenvZ("BUN_DEBUG_" ++ tagname) != null) - { - really_disable = false; + if (bun.getenvZ("BUN_DEBUG_" ++ tagname)) |val| { + really_disable = strings.eqlComptime(val, "0"); + } else if (bun.getenvZ("BUN_DEBUG_ALL")) |val| { + really_disable = strings.eqlComptime(val, "0"); } else if (bun.getenvZ("BUN_DEBUG_QUIET_LOGS")) |val| { really_disable = really_disable or !strings.eqlComptime(val, "0"); } @@ -612,7 +612,7 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn { lock.lock(); defer lock.unlock(); - if (Output.enable_ansi_colors_stdout and buffered_writer.unbuffered_writer.context.handle == writer().context.handle) { + if (Output.enable_ansi_colors_stdout and source_set and buffered_writer.unbuffered_writer.context.handle == writer().context.handle) { out.print(comptime prettyFmt("[" ++ tagname ++ "] " ++ fmt, true), args) catch { really_disable = true; return; @@ -931,6 +931,9 @@ pub fn disableScopedDebugWriter() void { pub fn enableScopedDebugWriter() void { ScopedDebugWriter.disable_inside_log -= 1; } + +extern "c" fn getpid() c_int; + pub fn initScopedDebugWriterAtStartup() void { std.debug.assert(source_set); @@ -941,9 +944,15 @@ pub fn initScopedDebugWriterAtStartup() void { } // do not use libuv through this code path, since it might not be initialized yet. + const pid = std.fmt.allocPrint(bun.default_allocator, "{d}", .{getpid()}) catch @panic("failed to allocate path"); + defer bun.default_allocator.free(pid); + + const path_fmt = std.mem.replaceOwned(u8, bun.default_allocator, path, "{pid}", pid) catch @panic("failed to allocate path"); + defer bun.default_allocator.free(path_fmt); + const fd = std.os.openat( std.fs.cwd().fd, - path, + path_fmt, std.os.O.CREAT | std.os.O.WRONLY, // on windows this is u0 if (Environment.isWindows) 0 else 0o644, diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 21326a6ae4..877f6b455f 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -904,8 +904,7 @@ pub const Resolver = struct { if (DataURL.parse(import_path) catch { return .{ .failure = error.InvalidDataURL }; - }) |_data_url| { - const data_url: DataURL = _data_url; + }) |data_url| { // "import 'data:text/javascript,console.log(123)';" // "@import 'data:text/css,body{background:white}';" const mime = data_url.decodeMimeType(); diff --git a/src/string.zig b/src/string.zig index f0a19f15e7..d58d891021 100644 --- a/src/string.zig +++ b/src/string.zig @@ -541,7 +541,12 @@ pub const String = extern struct { callback: ?*const fn (*anyopaque, *anyopaque, u32) callconv(.C) void, ) String; - pub fn createExternal(bytes: []const u8, isLatin1: bool, ctx: ?*anyopaque, callback: ?*const fn (*anyopaque, *anyopaque, u32) callconv(.C) void) String { + /// ctx is the pointer passed into `createExternal` + /// buffer is the pointer to the buffer, either [*]u8 or [*]u16 + /// len is the number of characters in that buffer. + pub const ExternalStringImplFreeFunction = fn (ctx: *anyopaque, buffer: *anyopaque, len: u32) callconv(.C) void; + + pub fn createExternal(bytes: []const u8, isLatin1: bool, ctx: ?*anyopaque, callback: ?*const ExternalStringImplFreeFunction) String { JSC.markBinding(@src()); std.debug.assert(bytes.len > 0); return BunString__createExternal(bytes.ptr, bytes.len, isLatin1, ctx, callback); @@ -925,8 +930,6 @@ pub const String = extern struct { } } - pub const unref = deref; - pub fn eqlComptime(this: String, comptime value: []const u8) bool { return this.toZigString().eqlComptime(value); } diff --git a/test/integration/next/default-pages-dir/.eslintrc.json b/test/integration/next-pages/.eslintrc.json similarity index 100% rename from test/integration/next/default-pages-dir/.eslintrc.json rename to test/integration/next-pages/.eslintrc.json diff --git a/test/integration/next/default-pages-dir/.gitignore b/test/integration/next-pages/.gitignore similarity index 100% rename from test/integration/next/default-pages-dir/.gitignore rename to test/integration/next-pages/.gitignore diff --git a/test/integration/next/default-pages-dir/README.md b/test/integration/next-pages/README.md similarity index 100% rename from test/integration/next/default-pages-dir/README.md rename to test/integration/next-pages/README.md diff --git a/test/integration/next/default-pages-dir/bun.lockb b/test/integration/next-pages/bun.lockb similarity index 100% rename from test/integration/next/default-pages-dir/bun.lockb rename to test/integration/next-pages/bun.lockb diff --git a/test/integration/next/default-pages-dir/next.config.js b/test/integration/next-pages/next.config.js similarity index 100% rename from test/integration/next/default-pages-dir/next.config.js rename to test/integration/next-pages/next.config.js diff --git a/test/integration/next/default-pages-dir/package.json b/test/integration/next-pages/package.json similarity index 100% rename from test/integration/next/default-pages-dir/package.json rename to test/integration/next-pages/package.json diff --git a/test/integration/next/default-pages-dir/postcss.config.js b/test/integration/next-pages/postcss.config.js similarity index 100% rename from test/integration/next/default-pages-dir/postcss.config.js rename to test/integration/next-pages/postcss.config.js diff --git a/test/integration/next/default-pages-dir/public/favicon.ico b/test/integration/next-pages/public/favicon.ico similarity index 100% rename from test/integration/next/default-pages-dir/public/favicon.ico rename to test/integration/next-pages/public/favicon.ico diff --git a/test/integration/next/default-pages-dir/public/next.svg b/test/integration/next-pages/public/next.svg similarity index 100% rename from test/integration/next/default-pages-dir/public/next.svg rename to test/integration/next-pages/public/next.svg diff --git a/test/integration/next/default-pages-dir/public/vercel.svg b/test/integration/next-pages/public/vercel.svg similarity index 100% rename from test/integration/next/default-pages-dir/public/vercel.svg rename to test/integration/next-pages/public/vercel.svg diff --git a/test/integration/next/default-pages-dir/src/Counter1.txt b/test/integration/next-pages/src/Counter1.txt similarity index 100% rename from test/integration/next/default-pages-dir/src/Counter1.txt rename to test/integration/next-pages/src/Counter1.txt diff --git a/test/integration/next/default-pages-dir/src/Counter2.txt b/test/integration/next-pages/src/Counter2.txt similarity index 100% rename from test/integration/next/default-pages-dir/src/Counter2.txt rename to test/integration/next-pages/src/Counter2.txt diff --git a/test/integration/next/default-pages-dir/src/pages/_app.tsx b/test/integration/next-pages/src/pages/_app.tsx similarity index 100% rename from test/integration/next/default-pages-dir/src/pages/_app.tsx rename to test/integration/next-pages/src/pages/_app.tsx diff --git a/test/integration/next/default-pages-dir/src/pages/_document.tsx b/test/integration/next-pages/src/pages/_document.tsx similarity index 100% rename from test/integration/next/default-pages-dir/src/pages/_document.tsx rename to test/integration/next-pages/src/pages/_document.tsx diff --git a/test/integration/next/default-pages-dir/src/pages/api/hello.ts b/test/integration/next-pages/src/pages/api/hello.ts similarity index 100% rename from test/integration/next/default-pages-dir/src/pages/api/hello.ts rename to test/integration/next-pages/src/pages/api/hello.ts diff --git a/test/integration/next/default-pages-dir/src/pages/index.tsx b/test/integration/next-pages/src/pages/index.tsx similarity index 100% rename from test/integration/next/default-pages-dir/src/pages/index.tsx rename to test/integration/next-pages/src/pages/index.tsx diff --git a/test/integration/next/default-pages-dir/src/styles/globals.css b/test/integration/next-pages/src/styles/globals.css similarity index 100% rename from test/integration/next/default-pages-dir/src/styles/globals.css rename to test/integration/next-pages/src/styles/globals.css diff --git a/test/integration/next/default-pages-dir/tailwind.config.ts b/test/integration/next-pages/tailwind.config.ts similarity index 100% rename from test/integration/next/default-pages-dir/tailwind.config.ts rename to test/integration/next-pages/tailwind.config.ts diff --git a/test/integration/next/default-pages-dir/test/dev-server-puppeteer.ts b/test/integration/next-pages/test/dev-server-puppeteer.ts similarity index 96% rename from test/integration/next/default-pages-dir/test/dev-server-puppeteer.ts rename to test/integration/next-pages/test/dev-server-puppeteer.ts index 1b6de6db47..3093a17120 100644 --- a/test/integration/next/default-pages-dir/test/dev-server-puppeteer.ts +++ b/test/integration/next-pages/test/dev-server-puppeteer.ts @@ -13,7 +13,7 @@ if (process.argv.length > 2) { } const b = await launch({ - headless: true, + headless: (process.env.BUN_TEST_HEADLESS ?? "1") === "0", dumpio: true, }); @@ -34,8 +34,9 @@ async function main() { return promise; } - await p.goto(url); - await waitForConsoleMessage(p, /counter a/); + const console_promise = waitForConsoleMessage(p, /counter a/); + p.goto(url); + await console_promise; console.error("Loaded page"); assert.strictEqual(await p.$eval("code.font-bold", x => x.innerText), Bun.version); diff --git a/test/integration/next/default-pages-dir/test/dev-server.test.ts b/test/integration/next-pages/test/dev-server.test.ts similarity index 98% rename from test/integration/next/default-pages-dir/test/dev-server.test.ts rename to test/integration/next-pages/test/dev-server.test.ts index 2b2fd9c620..d6bbd34a71 100644 --- a/test/integration/next/default-pages-dir/test/dev-server.test.ts +++ b/test/integration/next-pages/test/dev-server.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { bunEnv, bunExe } from "../../../../harness"; +import { bunEnv, bunExe } from "../../../harness"; import { Subprocess } from "bun"; import { copyFileSync, rmSync } from "fs"; import { join } from "path"; diff --git a/test/integration/next/default-pages-dir/test/next-build.test.ts b/test/integration/next-pages/test/next-build.test.ts similarity index 90% rename from test/integration/next/default-pages-dir/test/next-build.test.ts rename to test/integration/next-pages/test/next-build.test.ts index 06ae9cb75b..23167aab93 100644 --- a/test/integration/next/default-pages-dir/test/next-build.test.ts +++ b/test/integration/next-pages/test/next-build.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe } from "../../../../harness"; +import { bunEnv, bunExe } from "../../../harness"; import { copyFileSync, cpSync, mkdtempSync, readFileSync, rmSync, symlinkSync, promises as fs } from "fs"; import { tmpdir } from "os"; import { join } from "path"; @@ -56,20 +56,12 @@ async function hashAllFiles(dir: string) { } function normalizeOutput(stdout: string) { - return ( - stdout - // remove timestamps from output - .replace(/\(\d+(?:\.\d+)? m?s\)/gi, "") - // TODO: this should not be necessary. it indicates a subtle bug in bun. - // normalize displayed bytes (round down to 0) - .replace(/\d(?:\.\d+)?(?= k?B)/g, "0") - // TODO: this should not be necessary. it indicates a subtle bug in bun. - // normalize multiple spaces to single spaces (must perform last) - .replace(/\s{2,}/g, " ") - ); + // remove timestamps from output + return stdout.replace(/\(\d+(?:\.\d+)? m?s\)/gi, data => " ".repeat(data.length)); } test("next build works", async () => { + rmSync(join(root, ".next"), { recursive: true, force: true }); copyFileSync(join(root, "src/Counter1.txt"), join(root, "src/Counter.tsx")); const install = Bun.spawn([bunExe(), "i"], { @@ -121,6 +113,9 @@ test("next build works", async () => { const bunCliOutput = normalizeOutput(await new Response(bunBuild.stdout).text()); const nodeCliOutput = normalizeOutput(await new Response(nodeBuild.stdout).text()); + console.log("bun", bunCliOutput); + console.log("node", nodeCliOutput); + expect(bunCliOutput).toBe(nodeCliOutput); const bunBuildDir = join(bunDir, ".next"); @@ -171,6 +166,6 @@ test("next build works", async () => { } build_passed = true; -}, 600000); +}, 60_0000); const version_string = "[production needs a constant string]"; diff --git a/test/integration/next/default-pages-dir/tsconfig.json b/test/integration/next-pages/tsconfig.json similarity index 100% rename from test/integration/next/default-pages-dir/tsconfig.json rename to test/integration/next-pages/tsconfig.json diff --git a/test/integration/next/default-pages-dir/tsconfig_for_build.json b/test/integration/next-pages/tsconfig_for_build.json similarity index 100% rename from test/integration/next/default-pages-dir/tsconfig_for_build.json rename to test/integration/next-pages/tsconfig_for_build.json diff --git a/test/js/bun/spawn/spawn.ipc.test.ts b/test/js/bun/spawn/spawn.ipc.test.ts index f492aa3ce1..dc734eb881 100644 --- a/test/js/bun/spawn/spawn.ipc.test.ts +++ b/test/js/bun/spawn/spawn.ipc.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "bun:test"; import { gcTick, bunExe } from "harness"; import path from "path"; -describe("ipc", () => { +describe.each(["advanced", "json"])("ipc mode %s", mode => { it("the subprocess should be defined and the child should send", done => { gcTick(); const returned_subprocess = spawn([bunExe(), path.join(__dirname, "bun-ipc-child.js")], { @@ -14,6 +14,8 @@ describe("ipc", () => { done(); gcTick(); }, + stdio: ["inherit", "inherit", "inherit"], + serialization: mode, }); }); @@ -28,6 +30,8 @@ describe("ipc", () => { done(); gcTick(); }, + stdio: ["inherit", "inherit", "inherit"], + serialization: mode, }); childProc.send(parentMessage); diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 6c06e1ba18..32db26e8ce 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -25,6 +25,11 @@ Symbol(Symbol Description) b: 456, c: 789, } +1.7976931348623157e+308 +5e-324 +9e+59 +0.3 +0.29999999999999993 { a: { b: { @@ -250,3 +255,27 @@ myCustomName { } custom inspect +| 0 | 0 | 132 | -42 | 4 | -8 | +| NaN | NaN | NaN | 0 | NaN | NaN | +| 0 | 0 | 132 | -42 | -4 | 8 | +| NaN | NaN | NaN | 0 | NaN | NaN | +4 +| 0 | 0.2 | 132.51 | -42.52 | 4.127888538432189e+22 | -8.5e-12 | +| NaN | Infinity | -Infinity | 0 | NaN | NaN | +0.5 | 0.005 +0 +504252 +-491952 +8589934592 +-8589934592 +0.0005 +-0.0005 +1.7976931348623157e+308 1 +-1.7976931348623157e+308 1 +5e-324 4 +-5e-324 4 +5e-324 9 +-5e-324 4 +0.30000000000000004 +Hello World 123 +Hello %vWorld 123 diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index c378cf0dc4..b2128c29ee 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -17,6 +17,11 @@ console.log(new Date(Math.pow(2, 34) * 56)); console.log([123, 456, 789]); console.log({ name: "foo" }); console.log({ a: 123, b: 456, c: 789 }); +console.log(Number.MAX_VALUE); +console.log(Number.MIN_VALUE); +console.log(899999999999999918767229449717619953810131273674690656206848); +console.log(0.299999999999999988896); +console.log(0.29999999999999993); console.log({ a: { b: { @@ -206,3 +211,47 @@ console.log({ "": "" }); ); console.log(proxy); } + +console.log("| %i | %i | %i | %i | %i | %i |", 0, 0.2, 132.51, -42.52, 41278885384321884328431, -0.0000000000085); +console.log("| %i | %i | %i | %i | %i | %i |", NaN, Infinity, -Infinity, -0, {}, Symbol.for("magic")); + +console.log("| %d | %d | %d | %d | %d | %d |", 0, 0.2, 132.51, -42.52, -41278885384321884328431, 0.0000000000085); +console.log("| %d | %d | %d | %d | %d | %d |", NaN, Infinity, -Infinity, -0, {}, Symbol.for("magic")); + +console.log("%d", { [Symbol.toPrimitive]: () => 0.000000000005 }); + +class Frac { + constructor(num, den) { + this.num = num; + this.den = den; + } + + [Symbol.toPrimitive]() { + return this.num / this.den; + } +} + +console.log("| %f | %f | %f | %f | %f | %f |", 0, 0.2, 132.51, -42.52, 41278885384321884328431, -0.0000000000085); +console.log("| %f | %f | %f | %f | %f | %f |", NaN, Infinity, -Infinity, -0, {}, Symbol.for("magic")); + +console.log("%f | %f", new Frac(1, 2), 0.005); + +console.log("%d", { [Symbol.toPrimitive]: () => 0 }); +console.log("%f", 504252); +console.log("%f", -491952); +console.log("%f", 8589934592); +console.log("%f", -8589934592); +console.log("%f", 0.0005); +console.log("%f", -0.0005); +console.log("%f %d", Number.MAX_VALUE, Number.MAX_VALUE); +console.log("%f %d", -Number.MAX_VALUE, Number.MAX_VALUE); +console.log("%f %d", Number.MIN_VALUE, Number.MIN_VALUE); +console.log("%f %d", -Number.MIN_VALUE, Number.MIN_VALUE); + +console.log("%f %d", Number.MIN_VALUE * 1.2, Number.MIN_VALUE * 1.5); +console.log("%f %d", -Number.MIN_VALUE * 1.2, Number.MIN_VALUE * 1.2); + +console.log("%f", 0.30000000000000004); + +console.log("Hello %cWorld", "color: red", 123); +console.log("Hello %vWorld", 123);