From c99d7ed331a00f7ba6404c887986c63103d80c7b Mon Sep 17 00:00:00 2001 From: dave caruso Date: Wed, 17 Apr 2024 15:32:25 -0700 Subject: [PATCH] feat: overhaul the crash handler (#10203) * some work * linux things * linux things * feat: tracestrings on Windows * bwaa * more work on the crash handler * okay * adgadsgbcxcv * ya * dsafds * a * wuh * a * bru h * ok * yay * window * alright * oops * yeah * a * a * OOM handling * fix on window --- .github/ISSUE_TEMPLATE/6-crash-report.yml | 20 + CMakeLists.txt | 9 +- build.zig | 4 +- packages/bun-internal-test/src/banned.json | 13 +- src/{__global.zig => Global.zig} | 16 +- src/StandaloneModuleGraph.zig | 6 +- src/analytics.zig | 1 - src/analytics/analytics_thread.zig | 13 +- src/bun.js/api/BunObject.zig | 10 - src/bun.js/api/bun/process.zig | 2 +- src/bun.js/bindings/bindings.zig | 4 +- src/bun.js/bindings/exports.zig | 2 - src/bun.js/bindings/wtf-bindings.cpp | 32 - src/bun.js/javascript.zig | 8 +- src/bun.js/module_loader.zig | 7 - src/bun.js/node/types.zig | 6 +- src/bun.zig | 74 +- src/bundler.zig | 1 - src/bundler/bundle_v2.zig | 2 - src/bunfig.zig | 2 +- src/cli.zig | 50 +- src/cli/install.ps1 | 2 +- src/cli/install_completions_command.zig | 6 +- src/cli/run_command.zig | 47 +- src/cli/upgrade_command.zig | 8 +- src/crash_handler.zig | 1250 ++++++++++++++++++++ src/crash_reporter.zig | 65 - src/deps/backtrace.zig | 0 src/deps/crash_reporter_linux.zig | 12 - src/deps/zig-clap/clap/args.zig | 2 +- src/fmt.zig | 19 + src/http.zig | 7 +- src/install/install.zig | 26 +- src/install/lockfile.zig | 9 +- src/install/migration.zig | 6 +- src/js/internal-for-testing.ts | 8 + src/main.zig | 20 +- src/main_wasm.zig | 5 +- src/panic_handler.zig | 57 - src/report.zig | 655 ---------- src/sha.zig | 2 +- src/sourcemap/sourcemap.zig | 8 +- src/string_immutable.zig | 14 + src/windows.zig | 37 + src/zlib.zig | 2 +- test/cli/run/fixture-crash.js | 15 + test/cli/run/run-crash-handler.test.ts | 29 + test/harness.ts | 2 + 48 files changed, 1575 insertions(+), 1020 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/6-crash-report.yml rename src/{__global.zig => Global.zig} (93%) delete mode 100644 src/analytics.zig create mode 100644 src/crash_handler.zig delete mode 100644 src/crash_reporter.zig delete mode 100644 src/deps/backtrace.zig delete mode 100644 src/deps/crash_reporter_linux.zig delete mode 100644 src/panic_handler.zig delete mode 100644 src/report.zig create mode 100644 test/cli/run/fixture-crash.js create mode 100644 test/cli/run/run-crash-handler.test.ts diff --git a/.github/ISSUE_TEMPLATE/6-crash-report.yml b/.github/ISSUE_TEMPLATE/6-crash-report.yml new file mode 100644 index 0000000000..ccd53f3c3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/6-crash-report.yml @@ -0,0 +1,20 @@ +name: Crash +description: Crash report +labels: [bug, crash] +body: + - type: markdown + attributes: + value: | + Thank you for submitting a crash report. It helps make Bun better. + + - type: textarea + id: remapped_trace + attributes: + label: Stack Trace + validations: + required: true + + - type: textarea + attributes: + label: Any extra info? + description: If this is a reproducible bug, please provide a code snippet or list of steps that can reproduce it. diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c20793ea2..f5b9f61e84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -738,7 +738,7 @@ if(NOT NO_CODEGEN) OUTPUT ${BUN_IDENTIFIER_CACHE_OUT} MAIN_DEPENDENCY "${BUN_SRC}/js_lexer/identifier_data.zig" DEPENDS "${BUN_SRC}/js_lexer/identifier_cache.zig" - COMMAND ${ZIG_COMPILER} run "${BUN_SRC}/js_lexer/identifier_data.zig" + COMMAND ${ZIG_COMPILER} run "--zig-lib-dir" "${ZIG_LIB_DIR}" "${BUN_SRC}/js_lexer/identifier_data.zig" VERBATIM COMMENT "Building Identifier Cache" ) @@ -763,6 +763,7 @@ if(NOT NO_CODEGEN) "${BUN_SRC}/js/node/*.ts" "${BUN_SRC}/js/thirdparty/*.js" "${BUN_SRC}/js/thirdparty/*.ts" + "${BUN_SRC}/js/internal-for-testing.ts" ) file(GLOB CODEGEN_FILES ${CONFIGURE_DEPENDS} "${BUN_CODEGEN_SRC}/*.ts") @@ -1014,7 +1015,7 @@ endif() # --- clang and linker flags --- if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(NOT WIN32) - target_compile_options(${bun} PUBLIC -g3 -O0 -gdwarf-4 + target_compile_options(${bun} PUBLIC -O0 -g -g3 -ggdb -gdwarf-4 -Werror=return-type -Werror=return-stack-address -Werror=implicit-function-declaration @@ -1061,8 +1062,8 @@ elseif(CMAKE_BUILD_TYPE STREQUAL "Release") list(APPEND LTO_LINK_FLAG "/LTCG") endif() - target_compile_options(${bun} PUBLIC /O2 ${LTO_FLAG} /DEBUG /Z7) - target_link_options(${bun} PUBLIC ${LTO_LINK_FLAG} /DEBUG) + target_compile_options(${bun} PUBLIC /O2 ${LTO_FLAG} /DEBUG:FULL) + target_link_options(${bun} PUBLIC ${LTO_LINK_FLAG} /DEBUG:FULL) endif() endif() diff --git a/build.zig b/build.zig index 5a3eb7868a..17a80e983c 100644 --- a/build.zig +++ b/build.zig @@ -146,7 +146,7 @@ const fs = std.fs; pub fn build(b: *Build) !void { build_(b) catch |err| { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + (std.debug).dumpStackTrace(trace.*); } return err; @@ -394,7 +394,7 @@ pub fn build_(b: *Build) !void { obj.linkLibC(); obj.dll_export_fns = true; obj.strip = false; - obj.omit_frame_pointer = optimize != .Debug; + obj.omit_frame_pointer = false; obj.subsystem = .Console; // Disable stack probing on x86 so we don't need to include compiler_rt diff --git a/packages/bun-internal-test/src/banned.json b/packages/bun-internal-test/src/banned.json index 92f96ecfbd..5ec3a57e08 100644 --- a/packages/bun-internal-test/src/banned.json +++ b/packages/bun-internal-test/src/banned.json @@ -1,11 +1,12 @@ { - "std.debug.assert": "Use bun.assert instead", - "@import(\"root\").bun.": "Only import 'bun' once", - "std.mem.indexOfAny": "Use bun.strings.indexAny or bun.strings.indexAnyComptime", - "std.debug.print": "Don't let this be committed", - " == undefined": "This is by definition Undefined Behavior.", " != undefined": "This is by definition Undefined Behavior.", - "undefined == ": "This is by definition Undefined Behavior.", + " == undefined": "This is by definition Undefined Behavior.", + "@import(\"root\").bun.": "Only import 'bun' once", + "std.debug.assert": "Use bun.assert instead", + "std.debug.dumpStackTrace": "Use bun.handleErrorReturnTrace or bun.crash_handler.dumpStackTrace instead", + "std.debug.print": "Don't let this be committed", + "std.mem.indexOfAny": "Use bun.strings.indexAny or bun.strings.indexAnyComptime", "undefined != ": "This is by definition Undefined Behavior.", + "undefined == ": "This is by definition Undefined Behavior.", "": "" } diff --git a/src/__global.zig b/src/Global.zig similarity index 93% rename from src/__global.zig rename to src/Global.zig index 9ea8f20dd6..a6d7aff63c 100644 --- a/src/__global.zig +++ b/src/Global.zig @@ -64,13 +64,17 @@ pub inline fn getStartTime() i128 { return bun.start_time; } -pub fn setThreadName(name: StringTypes.stringZ) void { +extern "kernel32" fn SetThreadDescription(thread: std.os.windows.HANDLE, name: [*:0]const u16) callconv(std.os.windows.WINAPI) std.os.windows.HRESULT; + +pub fn setThreadName(name: [:0]const u8) void { if (Environment.isLinux) { - _ = std.os.prctl(.SET_NAME, .{@intFromPtr(name.ptr)}) catch 0; + _ = std.os.prctl(.SET_NAME, .{@intFromPtr(name.ptr)}) catch {}; } else if (Environment.isMac) { _ = std.c.pthread_setname_np(name); } else if (Environment.isWindows) { - // _ = std.os.SetThreadDescription(std.os.GetCurrentThread(), name); + var name_buf: [1024]u16 = undefined; + const name_wide = bun.strings.convertUTF8toUTF16InBufferZ(&name_buf, name); + _ = SetThreadDescription(std.os.windows.kernel32.GetCurrentThread(), name_wide); } } @@ -105,7 +109,7 @@ pub fn raiseIgnoringPanicHandler(sig: anytype) noreturn { } Output.flush(); - @import("./crash_reporter.zig").on_error = null; + if (!Environment.isWindows) { if (sig >= 1 and sig != std.os.SIG.STOP and sig != std.os.SIG.KILL) { const act = std.os.Sigaction{ @@ -119,9 +123,7 @@ pub fn raiseIgnoringPanicHandler(sig: anytype) noreturn { Output.Source.Stdio.restore(); - // TODO(@paperdave): report a bug that this intcast shouldnt be needed. signals are i32 not u32 - // after that is fixed we can make this function take i32 - _ = std.c.raise(@intCast(sig)); + _ = std.c.raise(sig); std.c.abort(); } diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index 7638c156c0..0fba80c9a3 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -694,7 +694,7 @@ pub const StandaloneModuleGraph = struct { fn openSelf() std.fs.OpenSelfExeError!bun.FileDescriptor { if (!Environment.isWindows) { - const argv = bun.argv(); + const argv = bun.argv; if (argv.len > 0) { if (isBuiltInExe(u8, argv[0])) { return error.FileNotFound; @@ -707,14 +707,14 @@ pub const StandaloneModuleGraph = struct { if (std.fs.openFileAbsoluteZ("/proc/self/exe", .{})) |easymode| { return bun.toFD(easymode.handle); } else |_| { - if (bun.argv().len > 0) { + if (bun.argv.len > 0) { // The user doesn't have /proc/ mounted, so now we just guess and hope for the best. var whichbuf: [bun.MAX_PATH_BYTES]u8 = undefined; if (bun.which( &whichbuf, bun.getenvZ("PATH") orelse return error.FileNotFound, "", - bun.argv()[0], + bun.argv[0], )) |path| { return bun.toFD((try std.fs.cwd().openFileZ(path, .{})).handle); } diff --git a/src/analytics.zig b/src/analytics.zig deleted file mode 100644 index 68b2e08f53..0000000000 --- a/src/analytics.zig +++ /dev/null @@ -1 +0,0 @@ -pub usingnamespace @import("./analytics/analytics_thread.zig"); diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index 95f0d19718..bfd9b1038b 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -72,6 +72,7 @@ pub const Features = struct { pub fn formatter() Formatter { return Formatter{}; } + pub const Formatter = struct { pub fn format(_: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { const fields = comptime brk: { @@ -91,9 +92,14 @@ pub const Features = struct { break :brk buffer[0..count]; }; + var is_first_feature = true; inline for (fields) |field| { const count = @field(Features, field); if (count > 0) { + if (is_first_feature) { + try writer.writeAll("Features: "); + is_first_feature = false; + } try writer.writeAll(field); if (count > 1) { try writer.print("({d}) ", .{count}); @@ -102,10 +108,13 @@ pub const Features = struct { } } } + if (!is_first_feature) { + try writer.writeAll("\n"); + } var builtins = builtin_modules.iterator(); if (builtins.next()) |first| { - try writer.writeAll("\nBuiltins: \""); + try writer.writeAll("Builtins: \""); try writer.writeAll(@tagName(first)); try writer.writeAll("\" "); @@ -115,8 +124,6 @@ pub const Features = struct { try writer.writeAll("\" "); } - try writer.writeAll("\n"); - } else { try writer.writeAll("\n"); } } diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 320d58fd45..c92e9ac3aa 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -3325,7 +3325,6 @@ const UnsafeObject = struct { const object = JSValue.createEmptyObject(globalThis, 3); const fields = comptime .{ .gcAggressionLevel = &gcAggressionLevel, - .segfault = &__debug__doSegfault, .arrayBufferToString = &arrayBufferToString, .mimallocDump = &dump_mimalloc, }; @@ -3357,15 +3356,6 @@ const UnsafeObject = struct { return ret; } - // For testing the segfault handler - pub fn __debug__doSegfault( - _: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - const Reporter = @import("../../report.zig"); - Reporter.globalError(error.SegfaultTest, null); - } - pub fn arrayBufferToString( globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame, diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index 3c6347a28d..c374126955 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -1065,7 +1065,7 @@ pub const PosixSpawnResult = struct { } fn pidfdFlagsForLinux() u32 { - const kernel = @import("../../../analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); + const kernel = bun.analytics.GenerateHeader.GeneratePlatform.kernelVersion(); // pidfd_nonblock only supported in 5.10+ return if (kernel.orderWithoutTag(.{ .major = 5, .minor = 10, .patch = 0 }).compare(.gte)) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 2793faff49..a00235d193 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1897,8 +1897,8 @@ pub fn NewGlobalObject(comptime Type: type) type { } Output.flush(); - const Reporter = @import("../../report.zig"); - Reporter.fatal(null, "A C++ exception occurred"); + + @panic("A C++ exception occurred"); } }; } diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index e7ba72d48a..4b40aff2f4 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -25,7 +25,6 @@ const Exception = JSC.Exception; const JSModuleLoader = JSC.JSModuleLoader; const Microtask = JSC.Microtask; -const Backtrace = @import("../../crash_reporter.zig"); const JSPrinter = bun.js_printer; const JSLexer = bun.js_lexer; const typeBaseName = @import("../../meta.zig").typeBaseName; @@ -49,7 +48,6 @@ pub const ZigGlobalObject = extern struct { worker_ptr: ?*anyopaque, ) *JSGlobalObject { const global = shim.cppFn("create", .{ console, context_id, mini_mode, eval_mode, worker_ptr }); - Backtrace.reloadHandlers() catch unreachable; return global; } diff --git a/src/bun.js/bindings/wtf-bindings.cpp b/src/bun.js/bindings/wtf-bindings.cpp index cf049c709f..712179eda7 100644 --- a/src/bun.js/bindings/wtf-bindings.cpp +++ b/src/bun.js/bindings/wtf-bindings.cpp @@ -178,38 +178,6 @@ extern "C" void WTF__copyLCharsFromUCharSource(LChar* destination, const UChar* WTF::StringImpl::copyCharacters(destination, source, length); } -extern "C" void Bun__crashReportWrite(void* ctx, const char* message, size_t length); -extern "C" void Bun__crashReportDumpStackTrace(void* ctx) -{ - static constexpr int framesToShow = 32; - static constexpr int framesToSkip = 2; - void* stack[framesToShow + framesToSkip]; - int frames = framesToShow + framesToSkip; - WTFGetBacktrace(stack, &frames); - int size = frames - framesToSkip; - bool isFirst = true; - for (int frameNumber = 0; frameNumber < size; ++frameNumber) { - auto demangled = WTF::StackTraceSymbolResolver::demangle(stack[frameNumber]); - - StringPrintStream out; - if (isFirst) { - isFirst = false; - if (demangled) - out.printf("\n%-3d %p %s", frameNumber, stack[frameNumber], demangled->demangledName() ? demangled->demangledName() : demangled->mangledName()); - else - out.printf("\n%-3d %p", frameNumber, stack[frameNumber]); - } else { - if (demangled) - out.printf("%-3d ??? %s", frameNumber, demangled->demangledName() ? demangled->demangledName() : demangled->mangledName()); - else - out.printf("%-3d ???", frameNumber); - } - - auto str = out.toCString(); - Bun__crashReportWrite(ctx, str.data(), str.length()); - } -} - // For whatever reason // Doing this in C++/C is 2x faster than doing it in Zig. // However, it's still slower than it should be. diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index c1b2ca7f5a..87db8f77ac 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -850,7 +850,11 @@ pub const VirtualMachine = struct { Output.debug("Reloading...", .{}); if (this.hot_reload == .watch) { Output.flush(); - bun.reloadProcess(bun.default_allocator, !strings.eqlComptime(this.bundler.env.get("BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD") orelse "0", "true")); + bun.reloadProcess( + bun.default_allocator, + !strings.eqlComptime(this.bundler.env.get("BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD") orelse "0", "true"), + false, + ); } if (!strings.eqlComptime(this.bundler.env.get("BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD") orelse "0", "true")) { @@ -3462,7 +3466,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime if (comptime Ctx == ImportWatcher) { this.reloader.ctx.rareData().closeAllListenSocketsForWatchMode(); } - bun.reloadProcess(bun.default_allocator, clear_screen); + bun.reloadProcess(bun.default_allocator, clear_screen, false); unreachable; } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 9f87dcb193..e069947191 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2354,13 +2354,6 @@ pub const ModuleLoader = struct { if (!Environment.isDebug) { if (!is_allowed_to_use_internal_testing_apis) return null; - const is_outside_our_ci = brk: { - const repo = jsc_vm.bundler.env.get("GITHUB_REPOSITORY") orelse break :brk true; - break :brk !strings.endsWithComptime(repo, "/bun"); - }; - if (is_outside_our_ci) { - return null; - } } return jsSyntheticModule(.InternalForTesting, specifier); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 894e99a5ff..5b20f045a9 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -4843,7 +4843,7 @@ pub const Path = struct { pub const Process = struct { pub fn getArgv0(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - return JSC.ZigString.fromUTF8(bun.argv()[0]).toValueGC(globalObject); + return JSC.ZigString.fromUTF8(bun.argv[0]).toValueGC(globalObject); } pub fn getExecPath(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { @@ -4874,13 +4874,13 @@ pub const Process = struct { JSC.ZigString, // argv omits "bun" because it could be "bun run" or "bun" and it's kind of ambiguous // argv also omits the script name - bun.argv().len -| 1, + bun.argv.len -| 1, ) catch bun.outOfMemory(); defer allocator.free(args); var used: usize = 0; const offset = 1; - for (bun.argv()[@min(bun.argv().len, offset)..]) |arg| { + for (bun.argv[@min(bun.argv.len, offset)..]) |arg| { if (arg.len == 0) continue; diff --git a/src/bun.zig b/src/bun.zig index 024fdfaaee..5448c0ff36 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -54,7 +54,7 @@ pub const shell = struct { }; pub const Output = @import("./output.zig"); -pub const Global = @import("./__global.zig"); +pub const Global = @import("./Global.zig"); // make this non-pub after https://github.com/ziglang/zig/issues/18462 is resolved pub const FileDescriptorInt = if (Environment.isBrowser) @@ -697,7 +697,8 @@ pub const uws = @import("./deps/uws.zig"); pub const BoringSSL = @import("./boringssl.zig"); pub const LOLHTML = @import("./deps/lol-html.zig"); pub const clap = @import("./deps/zig-clap/clap.zig"); -pub const analytics = @import("./analytics.zig"); +pub const analytics = @import("./analytics/analytics_thread.zig"); +pub const zlib = @import("./zlib.zig"); pub var start_time: i128 = 0; @@ -1534,7 +1535,7 @@ pub const failing_allocator = std.mem.Allocator{ .ptr = undefined, .vtable = &.{ var __reload_in_progress__ = std.atomic.Value(bool).init(false); threadlocal var __reload_in_progress__on_current_thread = false; -fn isProcessReloadInProgressOnAnotherThread() bool { +pub fn isProcessReloadInProgressOnAnotherThread() bool { @fence(.Acquire); return __reload_in_progress__.load(.Monotonic) and !__reload_in_progress__on_current_thread; } @@ -1562,18 +1563,26 @@ pub noinline fn maybeHandlePanicDuringProcessReload() void { } } -/// Reload Bun's process +/// Reload Bun's process. This clones envp, argv, and gets the current +/// executable path. /// -/// This clones envp, argv, and gets the current executable path +/// On posix, this overwrites the current process with the new process using +/// `execve`. On Windows, we dont have this API, instead relying on a dummy +/// parent process that we can signal via a special exit code. /// -/// Overwrites the current process with the new process -/// -/// Must be able to allocate memory. malloc is not signal safe, but it's +/// Must be able to allocate memory. `malloc` is not signal safe, but it's /// best-effort. Not much we can do if it fails. +/// +/// Note that this function is called during the crash handler, in which it is +/// passed true to `may_return`. If failure occurs, one line of standard error +/// is printed and then this returns void. If `may_return == false`, then a +/// panic will occur on failure. The crash handler will not schedule two reloads +/// at once. pub fn reloadProcess( allocator: std.mem.Allocator, clear_terminal: bool, -) noreturn { + comptime may_return: bool, +) if (may_return) void else noreturn { __reload_in_progress__.store(true, .Monotonic); __reload_in_progress__on_current_thread = true; @@ -1582,23 +1591,33 @@ pub fn reloadProcess( Output.disableBuffering(); Output.resetTerminalAll(); } + Output.Source.Stdio.restore(); const bun = @This(); if (comptime Environment.isWindows) { // on windows we assume that we have a parent process that is monitoring us and will restart us if we exit with a magic exit code // see becomeWatcherManager - const rc = bun.windows.TerminateProcess(@ptrFromInt(std.math.maxInt(usize)), win32.watcher_reload_exit); + const rc = bun.windows.TerminateProcess(bun.windows.GetCurrentProcess(), win32.watcher_reload_exit); if (rc == 0) { const err = bun.windows.GetLastError(); + if (may_return) { + Output.errGeneric("Failed to reload process: {s}", .{@tagName(err)}); + return; + } Output.panic("Error while reloading process: {s}", .{@tagName(err)}); } else { + if (may_return) { + Output.errGeneric("Failed to reload process", .{}); + return; + } Output.panic("Unexpected error while reloading process\n", .{}); } } + const PosixSpawn = posix.spawn; - const dupe_argv = allocator.allocSentinel(?[*:0]const u8, bun.argv().len, null) catch unreachable; - for (bun.argv(), dupe_argv) |src, *dest| { + const dupe_argv = allocator.allocSentinel(?[*:0]const u8, bun.argv.len, null) catch unreachable; + for (bun.argv, dupe_argv) |src, *dest| { dest.* = (allocator.dupeZ(u8, src) catch unreachable).ptr; } @@ -1642,9 +1661,17 @@ pub fn reloadProcess( ) catch unreachable; switch (PosixSpawn.spawnZ(exec_path, actions, attrs, @as([*:null]?[*:0]const u8, @ptrCast(newargv)), @as([*:null]?[*:0]const u8, @ptrCast(envp)))) { .err => |err| { + if (may_return) { + Output.errGeneric("Failed to reload process: {s}", .{@tagName(err.getErrno())}); + return; + } Output.panic("Unexpected error while reloading: {d} {s}", .{ err.errno, @tagName(err.getErrno()) }); }, .result => |_| { + if (may_return) { + Output.errGeneric("Failed to reload process", .{}); + return; + } Output.panic("Unexpected error while reloading: posix_spawn returned a result", .{}); }, } @@ -1659,6 +1686,10 @@ pub fn reloadProcess( newargv, envp, ); + if (may_return) { + Output.errGeneric("Failed to reload process: {s}", .{@errorName(err)}); + return; + } Output.panic("Unexpected error while reloading: {s}", .{@errorName(err)}); } else { @compileError("unsupported platform for reloadProcess"); @@ -2059,14 +2090,10 @@ const WindowsStat = extern struct { pub const Stat = if (Environment.isWindows) windows.libuv.uv_stat_t else std.os.Stat; -var _argv: [][:0]const u8 = &[_][:0]const u8{}; - -pub inline fn argv() [][:0]const u8 { - return _argv; -} +pub var argv: [][:0]const u8 = &[_][:0]const u8{}; pub fn initArgv(allocator: std.mem.Allocator) !void { - _argv = try std.process.argsAlloc(allocator); + argv = try std.process.argsAlloc(allocator); } pub const posix = struct { @@ -2702,9 +2729,7 @@ pub const Dirname = struct { pub noinline fn outOfMemory() noreturn { @setCold(true); - - // TODO: In the future, we should print jsc + mimalloc heap statistics - @panic("Bun ran out of memory!"); + crash_handler.crashHandler(.out_of_memory, null, @returnAddress()); } pub const is_heap_breakdown_enabled = Environment.allow_assert and Environment.isMac; @@ -3014,7 +3039,7 @@ pub inline fn markPosixOnly() if (Environment.isPosix) void else noreturn { pub fn linuxKernelVersion() Semver.Version { if (comptime !Environment.isLinux) @compileError("linuxKernelVersion() is only available on Linux"); - return @import("./analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); + return analytics.GenerateHeader.GeneratePlatform.kernelVersion(); } pub fn selfExePath() ![:0]u8 { @@ -3071,13 +3096,16 @@ pub fn SliceIterator(comptime T: type) type { pub const Futex = @import("./futex.zig"); +pub const crash_handler = @import("crash_handler.zig"); +pub const handleErrorReturnTrace = crash_handler.handleErrorReturnTrace; + noinline fn assertionFailure() noreturn { if (@inComptime()) { @compileError("assertion failure"); } @setCold(true); - Output.panic("Internal assertion failure. This is a bug in Bun.", .{}); + Output.panic("Internal assertion failure", .{}); } pub inline fn debugAssert(cheap_value_only_plz: bool) void { diff --git a/src/bundler.zig b/src/bundler.zig index 8383c92d7f..6dff52b000 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -46,7 +46,6 @@ const NodeFallbackModules = @import("./node_fallbacks.zig"); const CacheEntry = @import("./cache.zig").FsCacheEntry; const Analytics = @import("./analytics/analytics_thread.zig"); const URL = @import("./url.zig").URL; -const Report = @import("./report.zig"); const Linker = linker.Linker; const Resolver = _resolver.Resolver; const TOML = @import("./toml/toml_parser.zig").TOML; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 44b394bfcc..e3fb47ef3b 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -73,7 +73,6 @@ const DebugOptions = @import("../cli.zig").Command.DebugOptions; const ThreadPoolLib = @import("../thread_pool.zig"); const ThreadlocalArena = @import("../mimalloc_arena.zig").Arena; const BabyList = @import("../baby_list.zig").BabyList; -const panicky = @import("../panic_handler.zig"); const Fs = @import("../fs.zig"); const schema = @import("../api/schema.zig"); const Api = schema.Api; @@ -97,7 +96,6 @@ const NodeFallbackModules = @import("../node_fallbacks.zig"); const CacheEntry = @import("../cache.zig").Fs.Entry; const Analytics = @import("../analytics/analytics_thread.zig"); const URL = @import("../url.zig").URL; -const Report = @import("../report.zig"); const Linker = linker.Linker; const Resolver = _resolver.Resolver; const TOML = @import("../toml/toml_parser.zig").TOML; diff --git a/src/bunfig.zig b/src/bunfig.zig index c38b202ea8..62c12a6eaa 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -25,7 +25,7 @@ pub const MacroImportReplacementMap = bun.StringArrayHashMap(string); pub const MacroMap = bun.StringArrayHashMapUnmanaged(MacroImportReplacementMap); pub const BundlePackageOverride = bun.StringArrayHashMapUnmanaged(options.BundleOverride); const LoaderMap = bun.StringArrayHashMapUnmanaged(options.Loader); -const Analytics = @import("./analytics.zig"); +const Analytics = bun.Analytics; const JSONParser = bun.JSON; const Command = @import("cli.zig").Command; const TOML = @import("./toml/toml_parser.zig").TOML; diff --git a/src/cli.zig b/src/cli.zig index ef4986a911..da192d9737 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -20,6 +20,7 @@ const js_printer = bun.js_printer; const js_ast = bun.JSAst; const linker = @import("linker.zig"); const RegularExpression = bun.RegularExpression; +const builtin = @import("builtin"); const debug = Output.scoped(.CLI, true); @@ -41,7 +42,6 @@ const Router = @import("./router.zig"); const MacroMap = @import("./resolver/package_json.zig").MacroMap; const TestCommand = @import("./cli/test_command.zig").TestCommand; -const Reporter = @import("./report.zig"); pub var start_time: i128 = undefined; const Bunfig = @import("./bunfig.zig").Bunfig; @@ -49,26 +49,24 @@ pub const Cli = struct { var wait_group: sync.WaitGroup = undefined; pub var log_: logger.Log = undefined; pub fn startTransform(_: std.mem.Allocator, _: Api.TransformOptions, _: *logger.Log) anyerror!void {} - pub fn start(allocator: std.mem.Allocator, comptime MainPanicHandler: type) void { + pub fn start(allocator: std.mem.Allocator) void { + is_main_thread = true; start_time = std.time.nanoTimestamp(); log_ = logger.Log.init(allocator); var log = &log_; - var panicker = MainPanicHandler.init(log); - MainPanicHandler.Singleton = &panicker; + // var panicker = MainPanicHandler.init(log); + // MainPanicHandler.Singleton = &panicker; Command.start(allocator, log) catch |err| { log.printForLogLevel(Output.errorWriter()) catch {}; - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - - Reporter.globalError(err, null); + bun.crash_handler.handleRootError(err, @errorReturnTrace()); }; } pub var cmd: ?Command.Tag = null; + pub threadlocal var is_main_thread: bool = false; }; const LoaderMatcher = strings.ExactSizeMatcher(4); @@ -151,7 +149,10 @@ pub const Arguments = struct { clap.parseParam("-c, --config ? Specify path to Bun config file. Default $cwd/bunfig.toml") catch unreachable, clap.parseParam("-h, --help Display this menu and exit") catch unreachable, clap.parseParam("...") catch unreachable, - }; + } ++ if (builtin.have_error_return_tracing) [_]ParamType{ + // This will print more error return traces, as a debug aid + clap.parseParam("--verbose-error-trace") catch unreachable, + } else [_]ParamType{}; const transpiler_params_ = [_]ParamType{ clap.parseParam("--main-fields ... Main fields to lookup in package.json. Defaults to --target dependent") catch unreachable, @@ -409,6 +410,12 @@ pub const Arguments = struct { } } + if (builtin.have_error_return_tracing) { + if (args.flag("--verbose-error-trace")) { + bun.crash_handler.verbose_error_trace = true; + } + } + var cwd: []u8 = undefined; if (args.option("--cwd")) |cwd_| { cwd = brk: { @@ -1048,7 +1055,7 @@ pub const HelpCommand = struct { pub const ReservedCommand = struct { pub fn exec(_: std.mem.Allocator) !void { @setCold(true); - const command_name = bun.argv()[1]; + const command_name = bun.argv[1]; Output.prettyError( \\Uh-oh. bun {s} is a subcommand reserved for future use by Bun. \\ @@ -1241,7 +1248,7 @@ pub const Command = struct { } pub fn which() Tag { - var args_iter = ArgsIterator{ .buf = bun.argv() }; + var args_iter = ArgsIterator{ .buf = bun.argv }; const argv0 = args_iter.next() orelse return .HelpCommand; @@ -1386,8 +1393,8 @@ pub const Command = struct { var ctx = global_cli_ctx; ctx.args.target = Api.Target.bun; - if (bun.argv().len > 1) { - ctx.passthrough = bun.argv()[1..]; + if (bun.argv.len > 1) { + ctx.passthrough = bun.argv[1..]; } else { ctx.passthrough = &[_]string{}; } @@ -1400,7 +1407,7 @@ pub const Command = struct { return; } - debug("argv: [{s}]", .{bun.fmt.fmtSlice(bun.argv(), ", ")}); + debug("argv: [{s}]", .{bun.fmt.fmtSlice(bun.argv, ", ")}); const tag = which(); @@ -1408,7 +1415,7 @@ pub const Command = struct { .DiscordCommand => return try DiscordCommand.exec(allocator), .HelpCommand => return try HelpCommand.exec(allocator), .ReservedCommand => return try ReservedCommand.exec(allocator), - .InitCommand => return try InitCommand.exec(allocator, bun.argv()), + .InitCommand => return try InitCommand.exec(allocator, bun.argv), .BuildCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BuildCommand) unreachable; const ctx = try Command.init(allocator, log, .BuildCommand); @@ -1444,7 +1451,7 @@ pub const Command = struct { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; const ctx = try Command.init(allocator, log, .BunxCommand); - try BunxCommand.exec(ctx, bun.argv()[if (is_bunx_exe) 0 else 1..]); + try BunxCommand.exec(ctx, bun.argv[if (is_bunx_exe) 0 else 1..]); return; }, .ReplCommand => { @@ -1452,7 +1459,7 @@ pub const Command = struct { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; var ctx = try Command.init(allocator, log, .BunxCommand); ctx.debug.run_in_bun = true; // force the same version of bun used. fixes bun-debug for example - var args = bun.argv()[0..]; + var args = bun.argv[0..]; args[1] = "bun-repl"; try BunxCommand.exec(ctx, args); return; @@ -1773,7 +1780,7 @@ pub const Command = struct { // KEYWORDS: open file argv argv0 if (ctx.args.entry_points.len == 1) { if (strings.eqlComptime(extension, ".lockb")) { - for (bun.argv()) |arg| { + for (bun.argv) |arg| { if (strings.eqlComptime(arg, "--hash")) { try PackageManagerCommand.printHash(ctx, ctx.args.entry_points[0]); return; @@ -1969,6 +1976,8 @@ pub const Command = struct { ctx, absolute_script_path.?, ) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + if (Output.enable_ansi_colors) { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; } else { @@ -1979,9 +1988,6 @@ pub const Command = struct { std.fs.path.basename(file_path), @errorName(err), }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } Global.exit(1); }; return true; diff --git a/src/cli/install.ps1 b/src/cli/install.ps1 index 10530be324..44a06dcd9e 100644 --- a/src/cli/install.ps1 +++ b/src/cli/install.ps1 @@ -277,7 +277,7 @@ function Install-Bun { New-ItemProperty -Path $RegistryKey -Name "DisplayName" -Value "Bun" -PropertyType String -Force | Out-Null New-ItemProperty -Path $RegistryKey -Name "InstallLocation" -Value "${BunRoot}" -PropertyType String -Force | Out-Null New-ItemProperty -Path $RegistryKey -Name "DisplayIcon" -Value $BunBin\bun.exe -PropertyType String -Force | Out-Null - New-ItemProperty -Path $RegistryKey -Name "UninstallString" -Value "powershell -c `"& `'$BunRoot\uninstall.ps1`' -PauseOnError`"" -PropertyType String -Force | Out-Null + New-ItemProperty -Path $RegistryKey -Name "UninstallString" -Value "powershell -c `"& `'$BunRoot\uninstall.ps1`' -PauseOnError`" -ExecutionPolicy Bypass" -PropertyType String -Force | Out-Null } catch { if ($rootKey -ne $null) { Remove-Item -Path $RegistryKey -Force diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index 1322193236..51b8192424 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -221,10 +221,10 @@ pub const InstallCompletionsCommand = struct { var completions_dir: string = ""; var output_dir: std.fs.Dir = found: { - for (bun.argv(), 0..) |arg, i| { + for (bun.argv, 0..) |arg, i| { if (strings.eqlComptime(arg, "completions")) { - if (bun.argv().len > i + 1) { - const input = bun.argv()[i + 1]; + if (bun.argv.len > i + 1) { + const input = bun.argv[i + 1]; if (!std.fs.path.isAbsolute(input)) { completions_dir = resolve_path.joinAbs( diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 9adb0ccd28..4ac070822c 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -487,9 +487,6 @@ pub const RunCommand = struct { fn runBinaryGenericError(executable: []const u8, silent: bool, err: bun.sys.Error) noreturn { if (!silent) { Output.prettyErrorln("error: Failed to run \"{s}\" due to:\n{}", .{ basenameOrBun(executable), err.withPath(executable) }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } } Global.exit(1); @@ -533,6 +530,8 @@ pub const RunCommand = struct { .loop = JSC.EventLoopHandle.init(JSC.MiniEventLoop.initGlobal(env)), } else {}, }) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + // an error occurred before the process was spawned print_error: { if (!silent) { @@ -557,9 +556,6 @@ pub const RunCommand = struct { } Output.prettyErrorln("error: Failed to run \"{s}\" due to {s}", .{ basenameOrBun(executable), @errorName(err) }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } } } Global.exit(1); @@ -583,9 +579,6 @@ pub const RunCommand = struct { basenameOrBun(executable), signal.name() orelse "unknown", }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } } Output.flush(); @@ -600,9 +593,6 @@ pub const RunCommand = struct { basenameOrBun(executable), exit_code.signal.name() orelse "unknown", }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } } Output.flush(); @@ -640,10 +630,6 @@ pub const RunCommand = struct { code, }); } - - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } } } @@ -716,9 +702,9 @@ pub const RunCommand = struct { // if we are already an absolute path, use that // if the user started the application via a shebang, it's likely that the path is absolute already - if (bun.argv()[0][0] == '/') { - optional_bun_path.* = bun.argv()[0]; - argv0 = bun.argv()[0]; + if (bun.argv[0][0] == '/') { + optional_bun_path.* = bun.argv[0]; + argv0 = bun.argv[0]; } else if (optional_bun_path.len == 0) { // otherwise, ask the OS for the absolute path const self = try bun.selfExePath(); @@ -729,7 +715,7 @@ pub const RunCommand = struct { } if (optional_bun_path.len == 0) { - argv0 = bun.argv()[0]; + argv0 = bun.argv[0]; } if (Environment.isDebug) { @@ -1319,15 +1305,14 @@ pub const RunCommand = struct { (script_name_to_search.len == 2 and @as(u16, @bitCast(script_name_to_search[0..2].*)) == @as(u16, @bitCast([_]u8{ '.', '/' })))) { Run.boot(ctx, ".") catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + ctx.log.printForLogLevel(Output.errorWriter()) catch {}; Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ script_name_to_search, @errorName(err), }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } Global.exit(1); }; return true; @@ -1403,7 +1388,7 @@ pub const RunCommand = struct { shebang = std.mem.trim(u8, shebang, " \r\n\t"); if (strings.hasPrefixComptime(shebang, "#!")) { - const first_arg: string = if (bun.argv().len > 0) bun.argv()[0] else ""; + const first_arg: string = if (bun.argv.len > 0) bun.argv[0] else ""; const filename = std.fs.path.basename(first_arg); // are we attempting to run the script with bun? if (!strings.contains(shebang, filename)) { @@ -1415,15 +1400,14 @@ pub const RunCommand = struct { Global.configureAllocator(.{ .long_running = true }); const out_path = ctx.allocator.dupe(u8, file_path) catch unreachable; Run.boot(ctx, out_path) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + ctx.log.printForLogLevel(Output.errorWriter()) catch {}; Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(file_path), @errorName(err), }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } Global.exit(1); }; @@ -1517,15 +1501,14 @@ pub const RunCommand = struct { (script_name_to_search.len > 2 and script_name_to_search[0] == '.' and script_name_to_search[1] == '/')) { Run.boot(ctx, ctx.allocator.dupe(u8, script_name_to_search) catch unreachable) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + ctx.log.printForLogLevel(Output.errorWriter()) catch {}; Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(script_name_to_search), @errorName(err), }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } Global.exit(1); }; } @@ -1552,9 +1535,7 @@ pub const RunCommand = struct { std.fs.path.basename(script_name_to_search), @errorName(err), }); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); Global.exit(1); }; return true; diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index dcc90d7d60..15ea1f6a99 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -410,7 +410,7 @@ pub const UpgradeCommand = struct { pub fn exec(ctx: Command.Context) !void { @setCold(true); - const args = bun.argv(); + const args = bun.argv; if (args.len > 2) { for (args[2..]) |arg| { if (!strings.contains(arg, "--")) { @@ -455,14 +455,14 @@ pub const UpgradeCommand = struct { const use_canary = brk: { const default_use_canary = Environment.is_canary; - if (default_use_canary and strings.containsAny(bun.argv(), "--stable")) + if (default_use_canary and strings.containsAny(bun.argv, "--stable")) break :brk false; break :brk strings.eqlComptime(env_loader.map.get("BUN_CANARY") orelse "0", "1") or - strings.containsAny(bun.argv(), "--canary") or default_use_canary; + strings.containsAny(bun.argv, "--canary") or default_use_canary; }; - const use_profile = strings.containsAny(bun.argv(), "--profile"); + const use_profile = strings.containsAny(bun.argv, "--profile"); const version: Version = if (!use_canary) v: { var refresher = std.Progress{}; diff --git a/src/crash_handler.zig b/src/crash_handler.zig new file mode 100644 index 0000000000..8f126e032c --- /dev/null +++ b/src/crash_handler.zig @@ -0,0 +1,1250 @@ +//! This file contains Bun's crash handler. In debug builds, we are able to +//! print backtraces that are mapped to source code. In release builds, we do +//! not have debug symbols in the binary. Bun's solution to this is called +//! a "trace string", a url with compressed encoding of the captured +//! backtrace. Version 1 trace strings contain the following information: +//! +//! - What version and commit of Bun captured the backtrace. +//! - The platform the backtrace was captured on. +//! - The list of addresses with ASLR removed, ready to be remapped. +//! - If panicking, the message that was panicked with. +//! +//! These can be demangled using Bun's remapping API, which has cached +//! versions of all debug symbols for all versions of Bun. Hosting this keeps +//! users from having to download symbols, which can be very large. +//! +//! The remapper is open source: https://github.com/oven-sh/bun.report +//! +//! A lot of this handler is based on the Zig Standard Library implementation +//! for std.debug.panicImpl and their code for gathering backtraces. +const std = @import("std"); +const bun = @import("root").bun; +const builtin = @import("builtin"); +const mimalloc = @import("allocators/mimalloc.zig"); +const SourceMap = @import("./sourcemap/sourcemap.zig"); +const windows = std.os.windows; +const Output = bun.Output; +const Global = bun.Global; +const Features = bun.Analytics.Features; +const debug = std.debug; + +/// Set this to false if you want to disable all uses of this panic handler. +/// This is useful for testing as a crash in here will not 'panicked during a panic'. +pub const enable = true; + +const report_base_url = "https://bun.report/"; + +/// Only print the `Bun has crashed` message once. Once this is true, control +/// flow is not returned to the main application. +var has_printed_message = false; + +/// Non-zero whenever the program triggered a panic. +/// The counter is incremented/decremented atomically. +var panicking = std.atomic.Value(u8).init(0); + +// Locked to avoid interleaving panic messages from multiple threads. +var panic_mutex = std.Thread.Mutex{}; + +/// Counts how many times the panic handler is invoked by this thread. +/// This is used to catch and handle panics triggered by the panic handler. +threadlocal var panic_stage: usize = 0; + +/// This structure and formatter must be kept in sync with `bun.report`'s decoder implementation. +pub const CrashReason = union(enum) { + /// From @panic() + panic: []const u8, + + /// "reached unreachable code" + @"unreachable", + + segmentation_fault: usize, + illegal_instruction: usize, + + /// Posix-only + bus_error: usize, + /// Posix-only + floating_point_error: usize, + /// Windows-only + datatype_misalignment, + /// Windows-only + stack_overflow, + + /// Either `main` returned an error, or somewhere else in the code a trace string is printed. + zig_error: anyerror, + + out_of_memory, + + pub fn format(self: CrashReason, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self) { + .panic => try writer.print("{s}", .{self.panic}), + .@"unreachable" => try writer.writeAll("reached unreachable code"), + .segmentation_fault => |addr| try writer.print("Segmentation fault at address 0x{X}", .{addr}), + .illegal_instruction => |addr| try writer.print("Illegal instruction at address 0x{X}", .{addr}), + .bus_error => |addr| try writer.print("Bus error at address 0x{X}", .{addr}), + .floating_point_error => |addr| try writer.print("Floating point error at address 0x{X}", .{addr}), + .datatype_misalignment => try writer.writeAll("Unaligned memory access"), + .stack_overflow => try writer.writeAll("Stack overflow"), + .zig_error => |err| try writer.print("error.{s}", .{@errorName(err)}), + .out_of_memory => try writer.writeAll("Bun ran out of memory"), + } + } +}; + +/// This function is invoked when a crash happpens. A crash is classified in `CrashReason`. +pub fn crashHandler( + reason: CrashReason, + // TODO: if both of these are specified, what is supposed to happen? + error_return_trace: ?*std.builtin.StackTrace, + begin_addr: ?usize, +) noreturn { + @setCold(true); + + // If a segfault happens while panicking, we want it to actually segfault, not trigger + // the handler. + resetSegfaultHandler(); + + nosuspend switch (panic_stage) { + 0 => { + bun.maybeHandlePanicDuringProcessReload(); + + panic_stage = 1; + _ = panicking.fetchAdd(1, .SeqCst); + + { + panic_mutex.lock(); + defer panic_mutex.unlock(); + + const writer = Output.errorWriter(); + + // The format of the panic trace is slightly different in debug + // builds Mainly, we demangle the backtrace immediately instead + // of using a trace string. + // + // To make the release-mode behavior easier to demo, debug mode + // checks for this CLI flag. + const debug_trace = bun.Environment.isDebug and check_flag: { + for (bun.argv) |arg| { + if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { + break :check_flag false; + } + } + break :check_flag true; + }; + + if (!has_printed_message) { + Output.flush(); + Output.Source.Stdio.restore(); + + writer.writeAll("=" ** 60 ++ "\n") catch std.os.abort(); + printMetadata(writer) catch std.os.abort(); + + Output.flush(); + } else { + Output.err("oh no", "multiple threads are crashing", .{}); + } + + if (reason != .out_of_memory or debug_trace) { + if (Output.enable_ansi_colors) { + writer.writeAll(Output.prettyFmt("", true)) catch std.os.abort(); + } + + writer.writeAll("panic") catch std.os.abort(); + + if (Output.enable_ansi_colors) { + writer.writeAll(Output.prettyFmt("", true)) catch std.os.abort(); + } + + if (bun.CLI.Cli.is_main_thread) { + writer.writeAll("(main thread)") catch std.os.abort(); + } else switch (bun.Environment.os) { + .windows => { + var name: std.os.windows.PWSTR = undefined; + const result = bun.windows.GetThreadDescription(std.os.windows.kernel32.GetCurrentThread(), &name); + if (std.os.windows.HRESULT_CODE(result) == .SUCCESS and name[0] != 0) { + writer.print("({})", .{bun.fmt.utf16(bun.span(name))}) catch std.os.abort(); + } else { + writer.print("(thread {d})", .{std.os.windows.kernel32.GetCurrentThreadId()}) catch std.os.abort(); + } + }, + .mac, .linux => {}, + else => @compileError("TODO"), + } + + Output.prettyErrorln(": {}", .{reason}); + } + + Output.flush(); + + var addr_buf: [10]usize = undefined; + var trace_buf: std.builtin.StackTrace = undefined; + + // If a trace was not provided, compute one now + const trace = error_return_trace orelse get_backtrace: { + trace_buf = std.builtin.StackTrace{ + .index = 0, + .instruction_addresses = &addr_buf, + }; + std.debug.captureStackTrace(begin_addr orelse @returnAddress(), &trace_buf); + break :get_backtrace &trace_buf; + }; + + if (debug_trace) { + dumpStackTrace(trace.*); + } else { + if (!has_printed_message) { + has_printed_message = true; + if (reason == .out_of_memory) { + Output.err("oh no", + \\Bun has ran out of memory. + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ + , .{}); + } else { + Output.err("oh no", + \\Bun has crashed. This indicates a bug in Bun, not your code. + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ + , .{}); + } + Output.flush(); + } + + if (Output.enable_ansi_colors) { + writer.print(Output.prettyFmt("", true), .{}) catch std.os.abort(); + } + + writer.writeAll(" ") catch std.os.abort(); + + encodeTraceString( + .{ + .trace = trace, + .reason = reason, + .action = .open_issue, + }, + writer, + ) catch std.os.abort(); + + writer.writeAll("\n") catch std.os.abort(); + } + + if (Output.enable_ansi_colors) { + writer.writeAll(Output.prettyFmt("\n", true)) catch std.os.abort(); + } else { + writer.writeAll("\n") catch std.os.abort(); + } + + Output.flush(); + } + + // Be aware that this function only lets one thread return from it. + // This is important so that we do not try to run the following reload logic twice. + waitForOtherThreadToFinishPanicking(); + + if (bun.auto_reload_on_crash and + // Do not reload if the panic arised FROM the reload function. + !bun.isProcessReloadInProgressOnAnotherThread()) + { + // attempt to prevent a double panic + bun.auto_reload_on_crash = false; + + Output.prettyErrorln("--- Bun is auto-restarting due to crash [time: {d}] ---", .{ + @max(std.time.milliTimestamp(), 0), + }); + Output.flush(); + + comptime std.debug.assert(void == @TypeOf(bun.reloadProcess(bun.default_allocator, false, true))); + bun.reloadProcess(bun.default_allocator, false, true); + } + }, + inline 1, 2 => |t| { + if (t == 1) { + panic_stage = 2; + Output.flush(); + } + panic_stage = 3; + + // A panic happened while trying to print a previous panic message, + // we're still holding the mutex but that's fine as we're going to + // call abort() + const stderr = std.io.getStdErr().writer(); + stderr.print("\npanic: {s}\n", .{reason}) catch std.os.abort(); + stderr.print("panicked during a panic. Aborting.\n", .{}) catch std.os.abort(); + }, + 3 => { + // Panicked while printing "Panicked during a panic." + }, + else => { + // Panicked or otherwise looped into the panic handler while trying to exit. + std.os.abort(); + }, + }; + + crash(); +} + +/// This is called when `main` returns a Zig error. +/// We don't want to treat it as a crash under certain error codes. +pub fn handleRootError(err: anyerror, error_return_trace: ?*std.builtin.StackTrace) noreturn { + var show_trace = bun.Environment.isDebug; + + switch (err) { + error.OutOfMemory => bun.outOfMemory(), + + error.InvalidArgument, + error.@"Invalid Bunfig", + => if (!show_trace) Global.exit(1), + + error.SyntaxError => { + Output.err("SyntaxError", "An error occurred while parsing code", .{}); + }, + + error.CurrentWorkingDirectoryUnlinked => { + Output.errGeneric( + "The current working directory was deleted, so that command didn't work. Please cd into a different directory and try again.", + .{}, + ); + }, + + error.SystemFdQuotaExceeded => { + if (comptime bun.Environment.isPosix) { + const limit = if (std.os.getrlimit(.NOFILE)) |limit| limit.cur else |_| null; + if (comptime bun.Environment.isMac) { + Output.prettyError( + \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) + \\ + \\Current limit: {d} + \\ + \\To fix this, try running: + \\ + \\ sudo launchctl limit maxfiles 2147483646 + \\ ulimit -n 2147483646 + \\ + \\That will only work until you reboot. + \\ + , + .{ + bun.fmt.nullableFallback(limit, ""), + }, + ); + } else { + Output.prettyError( + \\ + \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) + \\ + \\Current limit: {d} + \\ + \\To fix this, try running: + \\ + \\ sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf + \\ sudo sysctl -p + \\ ulimit -n 2147483646 + \\ + , + .{ + bun.fmt.nullableFallback(limit, ""), + }, + ); + + if (bun.getenvZ("USER")) |user| { + if (user.len > 0) { + Output.prettyError( + \\ + \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: + \\ + \\ {s} soft nofile 2147483646 + \\ {s} hard nofile 2147483646 + \\ + , + .{ user, user }, + ); + } + } + } + } else { + Output.prettyError( + \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) + , + .{}, + ); + } + }, + + error.ProcessFdQuotaExceeded => { + if (comptime bun.Environment.isPosix) { + const limit = if (std.os.getrlimit(.NOFILE)) |limit| limit.cur else |_| null; + if (comptime bun.Environment.isMac) { + Output.prettyError( + \\ + \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) + \\ + \\Current limit: {d} + \\ + \\To fix this, try running: + \\ + \\ ulimit -n 2147483646 + \\ + \\You may also need to run: + \\ + \\ sudo launchctl limit maxfiles 2147483646 + \\ + , + .{ + bun.fmt.nullableFallback(limit, ""), + }, + ); + } else { + Output.prettyError( + \\ + \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) + \\ + \\Current limit: {d} + \\ + \\To fix this, try running: + \\ + \\ ulimit -n 2147483646 + \\ + \\That will only work for the current shell. To fix this for the entire system, run: + \\ + \\ sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf + \\ sudo sysctl -p + \\ + , + .{ + bun.fmt.nullableFallback(limit, ""), + }, + ); + + if (bun.getenvZ("USER")) |user| { + if (user.len > 0) { + Output.prettyError( + \\ + \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: + \\ + \\ {s} soft nofile 2147483646 + \\ {s} hard nofile 2147483646 + \\ + , + .{ user, user }, + ); + } + } + } + } else { + Output.prettyErrorln( + \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) + , + .{}, + ); + } + }, + + // The usage of `unreachable` in Zig's std.os may cause the file descriptor problem to show up as other errors + error.NotOpenForReading, error.Unexpected => { + if (comptime bun.Environment.isPosix) { + const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit); + + if (limit.cur > 0 and limit.cur < (8192 * 2)) { + Output.prettyError( + \\ + \\error: An unknown error ocurred, possibly due to low max file descriptors (Unexpected) + \\ + \\Current limit: {d} + \\ + \\To fix this, try running: + \\ + \\ ulimit -n 2147483646 + \\ + , + .{ + limit.cur, + }, + ); + + if (bun.Environment.isLinux) { + if (bun.getenvZ("USER")) |user| { + if (user.len > 0) { + Output.prettyError( + \\ + \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: + \\ + \\ {s} soft nofile 2147483646 + \\ {s} hard nofile 2147483646 + \\ + , + .{ + user, + user, + }, + ); + } + } + } else if (bun.Environment.isMac) { + Output.prettyError( + \\ + \\If that still doesn't work, you may need to run: + \\ + \\ sudo launchctl limit maxfiles 2147483646 + \\ + , + .{}, + ); + } + } else { + Output.errGeneric( + "An unknown error ocurred ({s})", + .{@errorName(err)}, + ); + show_trace = true; + } + } else { + Output.errGeneric( + \\An unknown error ocurred ({s}) + , + .{@errorName(err)}, + ); + show_trace = true; + } + }, + + error.ENOENT, error.FileNotFound => { + Output.err( + "ENOENT", + "Bun could not find a file, and the code that produces this error is missing a better error.", + .{}, + ); + }, + + error.MissingPackageJSON => { + Output.errGeneric( + "Bun could not find a package.json file to install from", + .{}, + ); + Output.note("Run \"bun init\" to initialize a project", .{}); + }, + + else => { + Output.errGeneric( + if (bun.Environment.isDebug) + "'main' returned error.{s}" + else + "An internal error ocurred ({s})", + .{@errorName(err)}, + ); + show_trace = true; + }, + } + + if (show_trace) { + handleErrorReturnTraceExtra(err, error_return_trace, true); + } + + Global.exit(1); +} + +pub fn panicImpl(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, begin_addr: ?usize) noreturn { + @setCold(true); + crashHandler( + if (bun.strings.eqlComptime(msg, "reached unreachable code")) + .{ .@"unreachable" = {} } + else + .{ .panic = msg }, + error_return_trace, + begin_addr orelse @returnAddress(), + ); +} + +fn panicBuiltin(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, begin_addr: ?usize) noreturn { + std.debug.panicImpl(error_return_trace, begin_addr, msg); +} + +pub const panic = if (enable) panicImpl else panicBuiltin; + +const arch_display_string = if (bun.Environment.isAarch64) + if (bun.Environment.isMac) "Silicon" else "arm64" +else + "x64"; + +const metadata_version_line = std.fmt.comptimePrint( + "Bun v{s} {s} {s}{s}\n", + .{ + Global.package_json_version_with_sha, + bun.Environment.os.displayString(), + arch_display_string, + if (bun.Environment.baseline) " (baseline)" else "", + }, +); + +fn handleSegfaultPosix(sig: i32, info: *const std.os.siginfo_t, _: ?*const anyopaque) callconv(.C) noreturn { + const addr = switch (bun.Environment.os) { + .linux => @intFromPtr(info.fields.sigfault.addr), + .mac => @intFromPtr(info.addr), + else => unreachable, + }; + + crashHandler( + switch (sig) { + std.os.SIG.SEGV => .{ .segmentation_fault = addr }, + std.os.SIG.ILL => .{ .illegal_instruction = addr }, + std.os.SIG.BUS => .{ .bus_error = addr }, + std.os.SIG.FPE => .{ .floating_point_error = addr }, + + // we do not register this handler for other signals + else => unreachable, + }, + null, + @returnAddress(), + ); +} + +pub fn updatePosixSegfaultHandler(act: ?*const std.os.Sigaction) !void { + try std.os.sigaction(std.os.SIG.SEGV, act, null); + try std.os.sigaction(std.os.SIG.ILL, act, null); + try std.os.sigaction(std.os.SIG.BUS, act, null); + try std.os.sigaction(std.os.SIG.FPE, act, null); +} + +var windows_segfault_handle: ?windows.HANDLE = null; + +pub fn init() void { + if (!enable) return; + switch (bun.Environment.os) { + .windows => { + windows_segfault_handle = windows.kernel32.AddVectoredExceptionHandler(0, handleSegfaultWindows); + }, + .mac, .linux => { + const act = std.os.Sigaction{ + .handler = .{ .sigaction = handleSegfaultPosix }, + .mask = std.os.empty_sigset, + .flags = (std.os.SA.SIGINFO | std.os.SA.RESTART | std.os.SA.RESETHAND), + }; + updatePosixSegfaultHandler(&act) catch {}; + }, + else => @compileError("TODO"), + } +} + +pub fn resetSegfaultHandler() void { + if (bun.Environment.os == .windows) { + if (windows_segfault_handle) |handle| { + const rc = windows.kernel32.RemoveVectoredExceptionHandler(handle); + windows_segfault_handle = null; + bun.assert(rc != 0); + } + return; + } + + const act = std.os.Sigaction{ + .handler = .{ .handler = std.os.SIG.DFL }, + .mask = std.os.empty_sigset, + .flags = 0, + }; + // To avoid a double-panic, do nothing if an error happens here. + updatePosixSegfaultHandler(&act) catch {}; +} + +pub fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) callconv(windows.WINAPI) c_long { + crashHandler( + switch (info.ExceptionRecord.ExceptionCode) { + windows.EXCEPTION_DATATYPE_MISALIGNMENT => .{ .datatype_misalignment = {} }, + windows.EXCEPTION_ACCESS_VIOLATION => .{ .segmentation_fault = info.ExceptionRecord.ExceptionInformation[1] }, + windows.EXCEPTION_ILLEGAL_INSTRUCTION => .{ .illegal_instruction = info.ContextRecord.getRegs().ip }, + windows.EXCEPTION_STACK_OVERFLOW => .{ .stack_overflow = {} }, + else => return windows.EXCEPTION_CONTINUE_SEARCH, + }, + null, + @intFromPtr(info.ExceptionRecord.ExceptionAddress), + ); +} + +pub fn printMetadata(writer: anytype) !void { + if (Output.enable_ansi_colors) { + try writer.writeAll(Output.prettyFmt("", true)); + } + + try writer.writeAll(metadata_version_line); + { + try writer.print("Args: ", .{}); + var arg_chars_left: usize = 196; + for (bun.argv, 0..) |arg, i| { + if (i != 0) try writer.writeAll(", "); + try bun.fmt.quotedWriter(writer, arg[0..@min(arg.len, arg_chars_left)]); + arg_chars_left -|= arg.len; + if (arg_chars_left == 0) { + try writer.writeAll("..."); + break; + } + } + } + try writer.print("\n{}", .{bun.Analytics.Features.formatter()}); + + if (bun.use_mimalloc) { + var elapsed_msecs: usize = 0; + var user_msecs: usize = 0; + var system_msecs: usize = 0; + var current_rss: usize = 0; + var peak_rss: usize = 0; + var current_commit: usize = 0; + var peak_commit: usize = 0; + var page_faults: usize = 0; + mimalloc.mi_process_info( + &elapsed_msecs, + &user_msecs, + &system_msecs, + ¤t_rss, + &peak_rss, + ¤t_commit, + &peak_commit, + &page_faults, + ); + try writer.print("Elapsed: {d}ms | User: {d}ms | Sys: {d}ms\nRSS: {:<3.2} | Peak: {:<3.2} | Commit: {:<3.2} | Faults: {d}\n", .{ + elapsed_msecs, + user_msecs, + system_msecs, + std.fmt.fmtIntSizeDec(current_rss), + std.fmt.fmtIntSizeDec(peak_rss), + std.fmt.fmtIntSizeDec(current_commit), + page_faults, + }); + } + + if (Output.enable_ansi_colors) { + try writer.writeAll(Output.prettyFmt("", true)); + } + try writer.writeAll("\n"); +} + +fn waitForOtherThreadToFinishPanicking() void { + if (panicking.fetchSub(1, .SeqCst) != 1) { + // Another thread is panicking, wait for the last one to finish + // and call abort() + if (builtin.single_threaded) unreachable; + + // Sleep forever without hammering the CPU + var futex = std.atomic.Value(u32).init(0); + while (true) std.Thread.Futex.wait(&futex, 0); + comptime unreachable; + } +} + +/// Each platform is encoded is a single character. It is placed right after the +/// slash after the version, so someone just reading the trace string can tell +/// what platform it came from. L, M, and W are for Linux, macOS, and Windows, +/// with capital letters indicating aarch64, lowercase indicating x86_64. +/// +/// eg: 'https://bun.report/1.1.3/we04c... +// ^ this tells you it is windows x86_64 +/// +/// Baseline gets a weirder encoding of a mix of b and e. +const Platform = enum(u8) { + linux_x86_64 = 'l', + linux_x86_64_baseline = 'B', + linux_aarch64 = 'L', + + mac_x86_64_baseline = 'b', + mac_x86_64 = 'm', + mac_aarch64 = 'M', + + windows_x86_64 = 'w', + windows_x86_64_baseline = 'e', + + const current = @field(Platform, @tagName(bun.Environment.os) ++ + "_" ++ @tagName(builtin.target.cpu.arch) ++ + (if (bun.Environment.baseline) "_baseline" else "")); +}; + +const tracestr_version: u8 = '1'; + +const tracestr_header = std.fmt.comptimePrint( + "{s}/{c}{s}{c}", + .{ + bun.Environment.version_string, + @intFromEnum(Platform.current), + if (bun.Environment.git_sha.len > 0) bun.Environment.git_sha[0..7] else "unknown", + tracestr_version, + }, +); + +const StackLine = union(enum) { + unknown, + known: struct { + address: i32, + // null -> from bun.exe + object: ?[]const u8, + }, + javascript, + + pub fn fromAddress(addr: usize, name_bytes: []u8) StackLine { + return switch (bun.Environment.os) { + .windows => { + const module = bun.windows.getModuleHandleFromAddress(addr) orelse { + // TODO: try to figure out of this is a JS stack frame + return .{ .unknown = {} }; + }; + + const base_address = @intFromPtr(module); + + var temp: [512]u16 = undefined; + const name = bun.windows.getModuleNameW(module, &temp) orelse + return .{ .unknown = {} }; + + const image_path = bun.windows.exePathW(); + + return .{ + .known = .{ + // To remap this, `pdb-addr2line --exe bun.pdb 0x123456` + .address = @intCast(addr - base_address), + + .object = if (!std.mem.eql(u16, name, image_path)) name: { + const basename = name[std.mem.lastIndexOfAny(u16, name, &[_]u16{ '\\', '/' }) orelse 0 ..]; + break :name bun.strings.convertUTF16toUTF8InBuffer(name_bytes, basename) catch null; + } else null, + }, + }; + }, + .mac => { + // This code is slightly modified from std.debug.DebugInfo.lookupModuleNameDyld + // https://github.com/ziglang/zig/blob/215de3ee67f75e2405c177b262cb5c1cd8c8e343/lib/std/debug.zig#L1783 + const address = if (addr == 0) 0 else addr - 1; + + const image_count = std.c._dyld_image_count(); + + var i: u32 = 0; + while (i < image_count) : (i += 1) { + const header = std.c._dyld_get_image_header(i) orelse continue; + const base_address = @intFromPtr(header); + if (address < base_address) continue; + // This 'slide' is the ASLR offset. Subtract from `address` to get a stable address + const vmaddr_slide = std.c._dyld_get_image_vmaddr_slide(i); + + var it = std.macho.LoadCommandIterator{ + .ncmds = header.ncmds, + .buffer = @alignCast(@as( + [*]u8, + @ptrFromInt(@intFromPtr(header) + @sizeOf(std.macho.mach_header_64)), + )[0..header.sizeofcmds]), + }; + + while (it.next()) |cmd| switch (cmd.cmd()) { + .SEGMENT_64 => { + const segment_cmd = cmd.cast(std.macho.segment_command_64).?; + if (!bun.strings.eqlComptime(segment_cmd.segName(), "__TEXT")) continue; + + const original_address = address - vmaddr_slide; + const seg_start = segment_cmd.vmaddr; + const seg_end = seg_start + segment_cmd.vmsize; + if (original_address >= seg_start and original_address < seg_end) { + // Subtract ASLR value for stable address + const stable_address: usize = @intCast(address - vmaddr_slide); + + if (i == 0) { + const image_relative_address = stable_address - seg_start; + if (image_relative_address > std.math.maxInt(i32)) { + return .{ .unknown = {} }; + } + + // To remap this, you have to add the offset (which is going to be 0x100000000), + // and then you can run it through `llvm-symbolizer --obj bun-with-symbols 0x123456` + // The reason we are subtracting this known offset is mostly just so that we can + // fit it within a signed 32-bit integer. The VLQs will be shorter too. + return .{ .known = .{ + .object = null, + .address = @intCast(image_relative_address), + } }; + } else { + // these libraries are not interesting, mark as unknown + return .{ .unknown = {} }; + } + + return .{ .unknown = {} }; + } + }, + else => {}, + }; + } + + return .{ .unknown = {} }; + }, + else => { + // This code is slightly modified from std.debug.DebugInfo.lookupModuleDl + // https://github.com/ziglang/zig/blob/215de3ee67f75e2405c177b262cb5c1cd8c8e343/lib/std/debug.zig#L2024 + var ctx: struct { + // Input + address: usize, + i: usize = 0, + // Output + result: StackLine = .{ .unknown = {} }, + } = .{ .address = addr -| 1 }; + const CtxTy = @TypeOf(ctx); + + std.os.dl_iterate_phdr(&ctx, error{Found}, struct { + fn callback(info: *std.os.dl_phdr_info, _: usize, context: *CtxTy) !void { + defer context.i += 1; + + if (context.address < info.dlpi_addr) return; + const phdrs = info.dlpi_phdr[0..info.dlpi_phnum]; + for (phdrs) |*phdr| { + if (phdr.p_type != std.elf.PT_LOAD) continue; + + // Overflowing addition is used to handle the case of VSDOs + // having a p_vaddr = 0xffffffffff700000 + const seg_start = info.dlpi_addr +% phdr.p_vaddr; + const seg_end = seg_start + phdr.p_memsz; + if (context.address >= seg_start and context.address < seg_end) { + // const name = bun.sliceTo(info.dlpi_name, 0) orelse ""; + context.result = .{ + .known = .{ + .address = @intCast(context.address - info.dlpi_addr), + .object = null, + }, + }; + return error.Found; + } + } + } + }.callback) catch {}; + + return ctx.result; + }, + }; + } + + pub fn writeEncoded(self: StackLine, writer: anytype) !void { + switch (self) { + .unknown => try writer.writeAll("_"), + .known => |known| { + if (known.object) |object| { + try SourceMap.encodeVLQ(1).writeTo(writer); + try SourceMap.encodeVLQ(@intCast(object.len)).writeTo(writer); + try writer.writeAll(object); + } + try SourceMap.encodeVLQ(known.address).writeTo(writer); + }, + .javascript => { + try writer.writeAll("="); + }, + } + } + + pub fn format(self: StackLine, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (self) { + .unknown => try writer.print("???", .{}), + .known => |known| try writer.print("0x{x}{s}{s}", .{ + if (bun.Environment.isMac) @as(u64, known.address) + 0x100000000 else known.address, + if (known.object != null) " @ " else "", + known.object orelse "", + }), + .javascript => try writer.print("javascript address", .{}), + } + } +}; + +const TraceString = struct { + trace: *std.builtin.StackTrace, + reason: CrashReason, + action: Action, + + const Action = enum { + /// Open a pre-filled GitHub issue with the expanded trace + open_issue, + /// View the trace with nothing else + view_trace, + }; + + pub fn format(self: TraceString, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try encodeTraceString(self, writer); + } +}; + +fn encodeTraceString(opts: TraceString, writer: anytype) !void { + try writer.writeAll(report_base_url ++ tracestr_header); + + var name_bytes: [1024]u8 = undefined; + + for (opts.trace.instruction_addresses[0..opts.trace.index]) |addr| { + const line = StackLine.fromAddress(addr, &name_bytes); + try line.writeEncoded(writer); + } + + try writer.writeAll(comptime zero_vlq: { + const vlq = SourceMap.encodeVLQ(0); + break :zero_vlq vlq.bytes[0..vlq.len]; + }); + + // The following switch must be kept in sync with `bun.report`'s decoder implementation. + switch (opts.reason) { + .panic => |message| { + try writer.writeByte('0'); + + var compressed_bytes: [2048]u8 = undefined; + var len: usize = compressed_bytes.len; + const ret: bun.zlib.ReturnCode = @enumFromInt(bun.zlib.compress2(&compressed_bytes, &len, message.ptr, message.len, 9)); + const compressed = switch (ret) { + .Ok => compressed_bytes[0..len], + // Insufficient memory. + .MemError => return error.OutOfMemory, + // The buffer dest was not large enough to hold the compressed data. + .BufError => return error.NoSpaceLeft, + + // The level was not Z_DEFAULT_LEVEL, or was not between 0 and 9. + // This is technically possible but impossible because we pass 9. + .StreamError => return error.Unexpected, + else => return error.Unexpected, + }; + + var b64_bytes: [2048]u8 = undefined; + if (bun.base64.encodeLen(compressed) > b64_bytes.len) { + return error.NoSpaceLeft; + } + const b64_len = bun.base64.encode(&b64_bytes, compressed); + + try writer.writeAll(std.mem.trimRight(u8, b64_bytes[0..b64_len], "=")); + }, + + .@"unreachable" => try writer.writeByte('1'), + + .segmentation_fault => |addr| { + try writer.writeByte('2'); + try writeU64AsTwoVLQs(writer, addr); + }, + .illegal_instruction => |addr| { + try writer.writeByte('3'); + try writeU64AsTwoVLQs(writer, addr); + }, + .bus_error => |addr| { + try writer.writeByte('4'); + try writeU64AsTwoVLQs(writer, addr); + }, + .floating_point_error => |addr| { + try writer.writeByte('5'); + try writeU64AsTwoVLQs(writer, addr); + }, + + .datatype_misalignment => try writer.writeByte('6'), + .stack_overflow => try writer.writeByte('7'), + + .zig_error => |err| { + try writer.writeByte('8'); + try writer.writeAll(@errorName(err)); + }, + + .out_of_memory => try writer.writeByte('9'), + } + + if (opts.action == .view_trace) { + try writer.writeAll("/view"); + } +} + +fn writeU64AsTwoVLQs(writer: anytype, addr: usize) !void { + const first = SourceMap.encodeVLQ(@intCast((addr & 0xFFFFFFFF00000000) >> 32)); + const second = SourceMap.encodeVLQ(@bitCast(@as(u32, @intCast(addr & 0xFFFFFFFF)))); + try first.writeTo(writer); + try second.writeTo(writer); +} + +/// Crash. Make sure segfault handlers are off so that this doesnt trigger the crash handler. +/// This causes a segfault on posix systems to try to get a core dump. +fn crash() noreturn { + switch (bun.Environment.os) { + .windows => { + std.os.abort(); + }, + else => { + // Install default handler so that the tkill below will terminate. + const sigact = std.os.Sigaction{ .handler = .{ .handler = std.os.SIG.DFL }, .mask = std.os.empty_sigset, .flags = 0 }; + inline for (.{ + std.os.SIG.SEGV, + std.os.SIG.ILL, + std.os.SIG.BUS, + std.os.SIG.ABRT, + std.os.SIG.FPE, + std.os.SIG.HUP, + std.os.SIG.TERM, + }) |sig| { + std.os.sigaction(sig, &sigact, null) catch {}; + } + + @trap(); + }, + } +} + +pub var verbose_error_trace = false; + +fn handleErrorReturnTraceExtra(err: anyerror, maybe_trace: ?*std.builtin.StackTrace, comptime is_root: bool) void { + if (!builtin.have_error_return_tracing) return; + if (!verbose_error_trace and !is_root) return; + + if (maybe_trace) |trace| { + // The format of the panic trace is slightly different in debug + // builds Mainly, we demangle the backtrace immediately instead + // of using a trace string. + // + // To make the release-mode behavior easier to demo, debug mode + // checks for this CLI flag. + const is_debug = bun.Environment.isDebug and check_flag: { + for (bun.argv) |arg| { + if (bun.strings.eqlComptime(arg, "--debug-crash-handler-use-trace-string")) { + break :check_flag false; + } + } + break :check_flag true; + }; + + if (is_debug) { + if (is_root) { + Output.note( + "'main' returned error.{s}.{s}", + .{ + @errorName(err), + if (verbose_error_trace) + "" + else + " (release build will not have this trace by default)", + }, + ); + } else { + Output.note( + "caught error.{s}:", + .{@errorName(err)}, + ); + } + Output.flush(); + dumpStackTrace(trace.*); + } else { + const ts = TraceString{ + .trace = trace, + .reason = .{ .zig_error = err }, + .action = .view_trace, + }; + if (is_root) { + Output.prettyErrorln( + \\ + \\To send a redacted crash report to Bun's team, + \\please file a GitHub issue using the link below: + \\ + \\ {} + \\ + , + .{ts}, + ); + } else { + Output.prettyErrorln( + "trace: error.{s}: {}", + .{ @errorName(err), ts }, + ); + } + } + } +} + +/// In many places we catch errors, the trace for them is absorbed and only a +/// single line (the error name) is printed. When this is set, we will print +/// trace strings for those errors (or full stacks in debug builds). +/// +/// This can be enabled by passing `--verbose-error-trace` to the CLI. +/// In release builds with error return tracing enabled, this is also exposed. +/// You can test if this feature is available by checking `bun --help` for the flag. +pub inline fn handleErrorReturnTrace(err: anyerror, maybe_trace: ?*std.builtin.StackTrace) void { + handleErrorReturnTraceExtra(err, maybe_trace, false); +} + +const stdDumpStackTrace = debug.dumpStackTrace; + +/// Version of the standard library dumpStackTrace that has some fallbacks for +/// cases where such logic fails to run. +pub fn dumpStackTrace(trace: std.builtin.StackTrace) void { + const stderr = std.io.getStdErr().writer(); + switch (bun.Environment.os) { + .windows => attempt_dump: { + // Windows has issues with opening the PDB file sometimes. + const debug_info = debug.getSelfDebugInfo() catch |err| { + stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + break :attempt_dump; + }; + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + debug.writeStackTrace(trace, stderr, arena.allocator(), debug_info, std.io.tty.detectConfig(std.io.getStdErr())) catch |err| { + stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + break :attempt_dump; + }; + return; + }, + .linux => { + // Linux doesnt seem to be able to decode it's own debug info. + // TODO(@paperdave): see if zig 0.12 fixes this + }, + else => { + stdDumpStackTrace(trace); + return; + }, + } + // TODO: It would be reasonable, but hacky, to spawn LLVM-symbolizer here in + // order to get the demangled stack traces. + var name_bytes: [1024]u8 = undefined; + for (trace.instruction_addresses[0..trace.index]) |addr| { + const line = StackLine.fromAddress(addr, &name_bytes); + stderr.print("- {}\n", .{line}) catch break; + } + const program = switch (bun.Environment.os) { + .windows => "pdb-addr2line", + else => "llvm-symbolizer", + }; + stderr.print("note: Use " ++ program ++ " to demangle the above trace.\n", .{}) catch return; +} + +pub const js_bindings = struct { + const JSC = bun.JSC; + const JSValue = JSC.JSValue; + + pub fn generate(global: *JSC.JSGlobalObject) JSC.JSValue { + const obj = JSC.JSValue.createEmptyObject(global, 3); + inline for (.{ + .{ "getMachOImageZeroOffset", jsGetMachOImageZeroOffset }, + .{ "segfault", jsSegfault }, + .{ "panic", jsPanic }, + .{ "rootError", jsRootError }, + .{ "outOfMemory", jsOutOfMemory }, + }) |tuple| { + const name = JSC.ZigString.static(tuple[0]); + obj.put(global, name, JSC.createCallback(global, name, 1, tuple[1])); + } + return obj; + } + + pub fn jsGetMachOImageZeroOffset(_: *bun.JSC.JSGlobalObject, _: *bun.JSC.CallFrame) callconv(.C) bun.JSC.JSValue { + if (!bun.Environment.isMac) return .undefined; + + const header = std.c._dyld_get_image_header(0) orelse return .undefined; + const base_address = @intFromPtr(header); + const vmaddr_slide = std.c._dyld_get_image_vmaddr_slide(0); + + return bun.JSC.JSValue.jsNumber(base_address - vmaddr_slide); + } + + pub fn jsSegfault(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + @setRuntimeSafety(false); + const ptr: [*]align(1) u64 = @ptrFromInt(0xDEADBEEF); + ptr[0] = 0xDEADBEEF; + std.mem.doNotOptimizeAway(&ptr); + return .undefined; + } + + pub fn jsPanic(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + bun.crash_handler.panicImpl("invoked crashByPanic() handler", null, null); + } + + pub fn jsRootError(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + bun.crash_handler.handleRootError(error.Test, null); + } + + pub fn jsOutOfMemory(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + bun.outOfMemory(); + } +}; diff --git a/src/crash_reporter.zig b/src/crash_reporter.zig deleted file mode 100644 index 735ec74bdb..0000000000 --- a/src/crash_reporter.zig +++ /dev/null @@ -1,65 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; - -fn setup_sigactions(act: ?*const os.Sigaction) !void { - try os.sigaction(os.SIG.ABRT, act, null); - try os.sigaction(os.SIG.BUS, act, null); - try os.sigaction(os.SIG.FPE, act, null); - try os.sigaction(os.SIG.ILL, act, null); - try os.sigaction(os.SIG.SEGV, act, null); - try os.sigaction(os.SIG.TRAP, act, null); -} - -const builtin = @import("builtin"); -const ErrorCallback = *const fn (sig: i32, addr: usize) void; -pub var on_error: ?ErrorCallback = null; -noinline fn sigaction_handler(sig: i32, info: *const std.os.siginfo_t, _: ?*const anyopaque) callconv(.C) void { - // Prevent recursive calls - setup_sigactions(null) catch unreachable; - - const addr = switch (builtin.target.os.tag) { - .linux => @intFromPtr(info.fields.sigfault.addr), - .macos, .freebsd => @intFromPtr(info.addr), - .netbsd => @intFromPtr(info.info.reason.fault.addr), - .openbsd => @intFromPtr(info.data.fault.addr), - .solaris => @intFromPtr(info.reason.fault.addr), - else => @compileError("unreachable"), - }; - if (on_error) |handle| handle(sig, addr); -} - -noinline fn sigpipe_handler(_: i32, _: *const std.os.siginfo_t, _: ?*const anyopaque) callconv(.C) void { - bun.Output.debug("SIGPIPE received\n", .{}); -} - -pub fn reloadHandlers() !void { - if (comptime bun.Environment.isWindows) { - return bun.todo(@src(), {}); - } - try os.sigaction(os.SIG.PIPE, null, null); - try setup_sigactions(null); - - var act = os.Sigaction{ - .handler = .{ .sigaction = sigaction_handler }, - .mask = os.empty_sigset, - .flags = (os.SA.SIGINFO | os.SA.RESTART | os.SA.RESETHAND), - }; - - try setup_sigactions(&act); - bun.spawn.WaiterThread.reloadHandlers(); - bun_ignore_sigpipe(); -} -const os = std.os; -pub fn start() !void { - var act = os.Sigaction{ - .handler = .{ .sigaction = sigaction_handler }, - .mask = os.empty_sigset, - .flags = (os.SA.SIGINFO | os.SA.RESTART | os.SA.RESETHAND), - }; - - try setup_sigactions(&act); - bun_ignore_sigpipe(); - bun.spawn.WaiterThread.reloadHandlers(); -} - -extern fn bun_ignore_sigpipe() void; diff --git a/src/deps/backtrace.zig b/src/deps/backtrace.zig deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/deps/crash_reporter_linux.zig b/src/deps/crash_reporter_linux.zig deleted file mode 100644 index 62dd31616d..0000000000 --- a/src/deps/crash_reporter_linux.zig +++ /dev/null @@ -1,12 +0,0 @@ -const std = @import("std"); - -pub fn start(_: anytype) bool { - std.debug.attachSegfaultHandler(); - return true; -} - -pub fn generate() void {} - -pub fn crashReportPath(_: *[1024]u8) []const u8 { - return ""; -} diff --git a/src/deps/zig-clap/clap/args.zig b/src/deps/zig-clap/clap/args.zig index d96b91ec42..aba747eb5e 100644 --- a/src/deps/zig-clap/clap/args.zig +++ b/src/deps/zig-clap/clap/args.zig @@ -61,7 +61,7 @@ pub const OsIterator = struct { var res = OsIterator{ .arena = bun.ArenaAllocator.init(allocator), .exe_arg = undefined, - .remain = bun.argv(), + .remain = bun.argv, }; res.exe_arg = res.next(); return res; diff --git a/src/fmt.zig b/src/fmt.zig index 8d6dae069f..54482fb913 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1303,3 +1303,22 @@ pub const FormatDouble = struct { try writer.writeAll(slice); } }; + +pub fn nullableFallback(value: anytype, null_fallback: []const u8) NullableFallback(@TypeOf(value)) { + return .{ .value = value, .null_fallback = null_fallback }; +} + +pub fn NullableFallback(comptime T: type) type { + return struct { + value: T, + null_fallback: []const u8, + + pub fn format(self: @This(), comptime template: []const u8, opts: fmt.FormatOptions, writer: anytype) !void { + if (self.value) |value| { + try std.fmt.formatType(value, template, opts, writer, 4); + } else { + try writer.writeAll(self.null_fallback); + } + } + }; +} diff --git a/src/http.zig b/src/http.zig index b8723a6fe0..bc490d03c5 100644 --- a/src/http.zig +++ b/src/http.zig @@ -2258,11 +2258,8 @@ fn start_(this: *HTTPClient, comptime is_ssl: bool) void { } var socket = http_thread.connect(this, is_ssl) catch |err| { - if (Environment.isDebug) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + this.fail(err); return; }; diff --git a/src/install/install.zig b/src/install/install.zig index 90da7aae7b..23bf985a70 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -673,11 +673,8 @@ const Task = struct { manifest.network.callback.package_manifest.loaded_manifest, manager, ) catch |err| { - if (comptime Environment.isDebug) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + this.err = err; this.status = Status.fail; this.data = .{ .package_manifest = .{} }; @@ -710,11 +707,7 @@ const Task = struct { const result = this.request.extract.tarball.run( bytes, ) catch |err| { - if (comptime Environment.isDebug) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); this.err = err; this.status = Status.fail; @@ -790,11 +783,7 @@ const Task = struct { manager.allocator, &this.request.local_tarball.tarball, ) catch |err| { - if (comptime Environment.isDebug) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); this.err = err; this.status = Status.fail; @@ -1737,11 +1726,8 @@ pub const PackageInstall = struct { state.to_copy_buf2, if (Environment.isWindows) &state.buf2 else void{}, ) catch |err| { - if (comptime Environment.isDebug) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + if (comptime Environment.isWindows) { if (err == error.FailedToCopyFile) { return Result.fail(err, .copying_files); diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index e03c400b43..f487e03892 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1671,15 +1671,12 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { } file.closeAndMoveTo(tmpname, filename) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + // note: file is already closed here. _ = bun.sys.unlink(tmpname); - if (comptime Environment.allow_assert) { - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } - Output.err(err, "failed to replace old lockfile with new lockfile on disk", .{}); + Output.err(err, "Failed to replace old lockfile with new lockfile on disk", .{}); Global.crash(); }; } diff --git a/src/install/migration.zig b/src/install/migration.zig index ca206ad121..163084e880 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -60,12 +60,10 @@ pub fn detectAndLoadOtherLockfile(this: *Lockfile, allocator: Allocator, log: *l Global.exit(1); } if (Environment.allow_assert) { - const maybe_trace = @errorReturnTrace(); + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Output.prettyErrorln("Error: {s}", .{@errorName(err)}); log.printForLogLevel(Output.errorWriter()) catch {}; - if (maybe_trace) |trace| { - std.debug.dumpStackTrace(trace.*); - } Output.prettyErrorln("Invalid NPM package-lock.json\nIn a release build, this would ignore and do a fresh install.\nAborting", .{}); Global.exit(1); } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index ffbf7e7b43..bce65a3191 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -21,3 +21,11 @@ export const shellInternals = { lex: $newZigFunction("shell.zig", "TestingAPIs.shellLex", 1), parse: $newZigFunction("shell.zig", "TestingAPIs.shellParse", 1), }; + +export const crash_handler = $zig("crash_handler.zig", "js_bindings.generate") as { + getMachOImageZeroOffset: () => number; + segfault: () => void; + panic: () => void; + rootError: () => void; + outOfMemory: () => void; +}; diff --git a/src/main.zig b/src/main.zig index bd4023ee20..5f06509a2d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,8 +6,10 @@ const bun = @import("root").bun; const Output = bun.Output; const Environment = bun.Environment; -const panic_handler = @import("./panic_handler.zig"); -const MainPanicHandler = panic_handler.NewPanicHandler(std.builtin.default_panic); +pub const panic = bun.crash_handler.panic; +pub const std_options = struct { + pub const enable_segfault_handler = !bun.crash_handler.enable; +}; pub const io_mode = .blocking; @@ -15,16 +17,13 @@ comptime { bun.assert(builtin.target.cpu.arch.endian() == .little); } -pub fn panic(msg: []const u8, trace: ?*std.builtin.StackTrace, addr: ?usize) noreturn { - MainPanicHandler.handle_panic(msg, trace, addr); -} - -const CrashReporter = @import("./crash_reporter.zig"); extern fn bun_warn_avx_missing(url: [*:0]const u8) void; pub extern "C" var _environ: ?*anyopaque; pub extern "C" var environ: ?*anyopaque; pub fn main() void { + bun.crash_handler.init(); + // This should appear before we make any calls at all to libuv. // So it's safest to put it very early in the main function. if (Environment.isWindows) { @@ -38,19 +37,16 @@ pub fn main() void { _environ = @ptrCast(std.os.environ.ptr); } + bun.start_time = std.time.nanoTimestamp(); bun.initArgv(bun.default_allocator) catch |err| { Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)}); }; - if (Environment.isRelease and Environment.isPosix) - CrashReporter.start() catch unreachable; - - bun.start_time = std.time.nanoTimestamp(); Output.Source.Stdio.init(); defer Output.flush(); if (Environment.isX64 and Environment.enableSIMD and Environment.isPosix) { bun_warn_avx_missing(@import("./cli/upgrade_command.zig").Version.Bun__githubBaselineURL.ptr); } - bun.CLI.Cli.start(bun.default_allocator, MainPanicHandler); + bun.CLI.Cli.start(bun.default_allocator); } diff --git a/src/main_wasm.zig b/src/main_wasm.zig index adf57dee43..a02da7755b 100644 --- a/src/main_wasm.zig +++ b/src/main_wasm.zig @@ -479,11 +479,10 @@ export fn getTests(opts_array: u64) u64 { parser.options.features.top_level_await = true; parser.analyze(&anaylzer, @ptrCast(&TestAnalyzer.visitParts)) catch |err| { + bun.handleErrorReturnTrace(err, @errorReturnTrace()); + Output.print("Error: {s}\n", .{@errorName(err)}); - if (@errorReturnTrace()) |trace| { - Output.print("{}\n", .{trace}); - } log_.printForLogLevel(Output.writer()) catch unreachable; return 0; }; diff --git a/src/panic_handler.zig b/src/panic_handler.zig deleted file mode 100644 index e5d0167fe9..0000000000 --- a/src/panic_handler.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); -const bun = @import("root").bun; -const string = bun.string; -const Output = bun.Output; -const Global = bun.Global; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const stringZ = bun.stringZ; -const default_allocator = bun.default_allocator; -const C = bun.C; -const CLI = @import("./cli.zig").Cli; -const Features = @import("./analytics/analytics_thread.zig").Features; -const HTTP = bun.http.AsyncHTTP; -const Report = @import("./report.zig"); - -pub fn NewPanicHandler(comptime panic_func: fn ([]const u8, ?*std.builtin.StackTrace, ?usize) noreturn) type { - return struct { - panic_count: usize = 0, - skip_next_panic: bool = false, - log: *bun.logger.Log, - - pub var Singleton: ?*Handler = null; - const Handler = @This(); - - pub fn init(log: *bun.logger.Log) Handler { - return Handler{ - .log = log, - }; - } - pub inline fn handle_panic(msg: []const u8, error_return_type: ?*std.builtin.StackTrace, addr: ?usize) noreturn { - - // This exists to ensure we flush all buffered output before panicking. - Output.flush(); - - bun.maybeHandlePanicDuringProcessReload(); - - Report.fatal(null, msg); - - Output.disableBuffering(); - - Output.Source.Stdio.restore(); - - if (bun.auto_reload_on_crash) { - // attempt to prevent a double panic - bun.auto_reload_on_crash = false; - - Output.prettyErrorln("--- Bun is auto-restarting due to crash [time: {d}] ---", .{@max(std.time.milliTimestamp(), 0)}); - Output.flush(); - bun.reloadProcess(bun.default_allocator, false); - } - - // We want to always inline the panic handler so it doesn't show up in the stacktrace. - @call(bun.callmod_inline, panic_func, .{ msg, error_return_type, addr }); - } - }; -} diff --git a/src/report.zig b/src/report.zig deleted file mode 100644 index 84d729653c..0000000000 --- a/src/report.zig +++ /dev/null @@ -1,655 +0,0 @@ -const std = @import("std"); -const logger = bun.logger; -const bun = @import("root").bun; -const string = bun.string; -const Output = bun.Output; -const Global = bun.Global; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const stringZ = bun.stringZ; -const default_allocator = bun.default_allocator; -const C = bun.C; -const CLI = @import("./cli.zig").Cli; -const Features = @import("./analytics/analytics_thread.zig").Features; -const Platform = @import("./analytics/analytics_thread.zig").GenerateHeader.GeneratePlatform; -const HTTP = bun.http.AsyncHTTP; -const CrashReporter = @import("./crash_reporter.zig"); - -const Report = @This(); - -var crash_report_writer: CrashReportWriter = CrashReportWriter{ .file = null }; -const CrashWritable = if (Environment.isWasm) std.io.fixedBufferStream([2048]u8) else std.fs.File.Writer; -var crash_reporter_path: [1024]u8 = undefined; -pub const CrashReportWriter = struct { - file: ?std.io.BufferedWriter(4096, CrashWritable) = null, - file_path: []const u8 = "", - - pub fn printFrame(_: ?*anyopaque, frame: CrashReporter.StackFrame) void { - const function_name = if (frame.function_name.len > 0) frame.function_name else "[function ?]"; - const filename = if (frame.filename.len > 0) frame.function_name else "[file ?]"; - const fmt = bun.fmt.hexIntUpper(frame.pc); - crash_report_writer.print("[0x{any}] - {s} {s}:{d}\n", .{ fmt, function_name, filename, frame.line_number }); - } - - pub fn dump() void { - if (comptime !Environment.isWasm) - CrashReporter.print(); - } - - pub fn done() void { - if (comptime !Environment.isWasm) - CrashReporter.print(); - } - - pub fn print(this: *CrashReportWriter, comptime fmt: string, args: anytype) void { - Output.prettyError(fmt, args); - if (this.file) |*file| { - var writer = file.writer(); - writer.print(comptime Output.prettyFmt(fmt, false), args) catch {}; - } - } - - pub fn flush(this: *CrashReportWriter) void { - if (this.file) |*file| { - file.flush() catch {}; - } - Output.flush(); - } - - pub fn generateFile(this: *CrashReportWriter) void { - if (this.file != null) return; - - var base_dir: []const u8 = "."; - if (bun.getenvZ("BUN_INSTALL")) |install_dir| { - base_dir = strings.withoutTrailingSlash(install_dir); - } else if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { - base_dir = strings.withoutTrailingSlash(home_dir); - } - const file_path = std.fmt.bufPrintZ( - &crash_reporter_path, - "{s}/.bun-crash/v{s}-{d}.crash", - .{ base_dir, Global.package_json_version, @as(u64, @intCast(@max(std.time.milliTimestamp(), 0))) }, - ) catch return; - - if (bun.path.nextDirname(file_path)) |dirname| { - _ = bun.sys.mkdirA(dirname, 0); - } - - const call = bun.sys.openA(file_path, std.os.O.CREAT | std.os.O.TRUNC, 0).unwrap() catch return; - var file = call.asFile(); - this.file = std.io.bufferedWriter( - file.writer(), - ); - this.file_path = bun.asByteSlice(file_path); - } - - pub fn printPath(this: *CrashReportWriter) void { - var display_path = this.file_path; - - if (this.file_path.len > 0) { - var tilda = false; - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { - if (strings.hasPrefix(display_path, home_dir)) { - display_path = display_path[home_dir.len..]; - tilda = true; - } - } - - if (tilda) { - Output.prettyError("\nCrash report saved to:\n ~{s}\n", .{display_path}); - } else { - Output.prettyError("\nCrash report saved to:\n {s}\n", .{display_path}); - } - } - } -}; - -pub fn printMetadata() void { - @setCold(true); - - if (comptime !Environment.isWindows) { - // TODO(@paperdave): report files do not work on windows, and report files in general are buggy - crash_report_writer.generateFile(); - } - - const cmd_label: string = if (CLI.cmd) |tag| @tagName(tag) else "Unknown"; - - const platform = comptime Environment.os.displayString(); - const arch = comptime if (Environment.isAarch64) - if (Environment.isMac) "Silicon" else "arm64" - else - "x64"; - - const analytics_platform = Platform.forOS(); - - const maybe_baseline = if (Environment.baseline) " (baseline)" else ""; - - crash_report_writer.print( - \\ - \\----- bun meta ----- - ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ maybe_baseline ++ " {s}\n" ++ - \\{s}: {} - , .{ - analytics_platform.version, - cmd_label, - Features.formatter(), - }); - - const http_count = HTTP.active_requests_count.raw; - if (http_count > 0) - crash_report_writer.print( - \\HTTP: {d} - \\ - , .{http_count}); - - if (comptime bun.use_mimalloc) { - var elapsed_msecs: usize = 0; - var user_msecs: usize = 0; - var system_msecs: usize = 0; - var current_rss: usize = 0; - var peak_rss: usize = 0; - var current_commit: usize = 0; - var peak_commit: usize = 0; - var page_faults: usize = 0; - const mimalloc = @import("allocators/mimalloc.zig"); - mimalloc.mi_process_info( - &elapsed_msecs, - &user_msecs, - &system_msecs, - ¤t_rss, - &peak_rss, - ¤t_commit, - &peak_commit, - &page_faults, - ); - crash_report_writer.print("Elapsed: {d}ms | User: {d}ms | Sys: {d}ms\nRSS: {:<3.2} | Peak: {:<3.2} | Commit: {:<3.2} | Faults: {d}\n", .{ - elapsed_msecs, - user_msecs, - system_msecs, - std.fmt.fmtIntSizeDec(current_rss), - std.fmt.fmtIntSizeDec(peak_rss), - std.fmt.fmtIntSizeDec(current_commit), - page_faults, - }); - } - - crash_report_writer.print("----- bun meta -----\n", .{}); -} -var has_printed_fatal = false; -var has_printed_crash = false; -pub fn fatal(err_: ?anyerror, msg_: ?string) void { - const had_printed_fatal = has_printed_fatal; - if (!has_printed_fatal) { - has_printed_fatal = true; - if (comptime !Environment.isWindows) { - crash_report_writer.generateFile(); - } - - if (err_) |err| { - if (Output.isEmojiEnabled()) { - crash_report_writer.print( - "\nerror: {s}\n", - .{@errorName(err)}, - ); - } else { - crash_report_writer.print( - "\nerror: {s}\n\n", - .{@errorName(err)}, - ); - } - } - - if (msg_) |msg| { - const msg_ptr = @intFromPtr(msg.ptr); - if (msg_ptr > 0) { - const len = @max(@min(msg.len, 1024), 0); - - if (len > 0) { - if (Output.isEmojiEnabled()) { - crash_report_writer.print( - "\nuh-oh: {s}\n", - .{msg[0..len]}, - ); - } else { - crash_report_writer.print( - "\nPanic: {s}\n\n", - .{msg[0..len]}, - ); - } - } - } - } - - if (err_ == null) { - if (Output.isEmojiEnabled()) { - if (msg_ == null and err_ == null) { - crash_report_writer.print("", .{}); - } else { - crash_report_writer.print("", .{}); - } - crash_report_writer.print("bun will crash now 😭😭😭\n", .{}); - } else { - crash_report_writer.print("bun has crashed :'(\n", .{}); - } - } - crash_report_writer.flush(); - - printMetadata(); - - crash_report_writer.flush(); - - // It only is a real crash report if it's not coming from Zig - std.mem.doNotOptimizeAway(&Bun__crashReportWrite); - Bun__crashReportDumpStackTrace(&crash_report_writer); - - crash_report_writer.flush(); - - crash_report_writer.printPath(); - } - - if (!had_printed_fatal) { - if (Environment.isWindows) { - // TODO(@paperdave) change this to the original one once bun windows is stable - crash_report_writer.print("\nSearch GitHub issues https://bun.sh/issues or join in #windows channel in https://bun.sh/discord\n\n", .{}); - } else { - crash_report_writer.print("\nSearch GitHub issues https://bun.sh/issues or ask for #help in https://bun.sh/discord\n\n", .{}); - } - crash_report_writer.flush(); - } -} - -var globalError_ranOnce = false; -var error_return_trace: ?*std.builtin.StackTrace = null; - -export fn Bun__crashReportWrite(ctx: *CrashReportWriter, bytes_ptr: [*]const u8, len: usize) void { - if (!Environment.isWindows) { - if (error_return_trace) |trace| { - if (len > 0) { - ctx.print("{s}\n{}", .{ bytes_ptr[0..len], trace }); - } else { - ctx.print("{}\n", .{trace}); - } - return; - } - } - - if (len > 0) { - ctx.print("{s}\n", .{bytes_ptr[0..len]}); - } -} - -extern "C" fn Bun__crashReportDumpStackTrace(ctx: *anyopaque) void; - -pub noinline fn handleCrash(signal: i32, addr: usize) void { - const had_printed_fatal = has_printed_fatal; - if (has_printed_fatal) return; - has_printed_fatal = true; - - if (comptime !Environment.isWindows) { - // TODO(@paperdave): report files do not work on windows, and report files in general are buggy - crash_report_writer.generateFile(); - } - - const name = switch (signal) { - std.os.SIG.SEGV => error.SegmentationFault, - std.os.SIG.ILL => error.InstructionError, - std.os.SIG.BUS => error.BusError, - else => error.Crash, - }; - - crash_report_writer.print( - "\n{s} at 0x{any}\n\n", - .{ @errorName(name), bun.fmt.hexIntUpper(addr) }, - ); - printMetadata(); - if (comptime Environment.isDebug and !Environment.isWindows) { - error_return_trace = @errorReturnTrace(); - } - - if (!Environment.isWindows) { - if (comptime !bun.JSC.is_bindgen) { - std.mem.doNotOptimizeAway(&Bun__crashReportWrite); - Bun__crashReportDumpStackTrace(&crash_report_writer); - } - } - - if (!had_printed_fatal) { - crash_report_writer.print("\nAsk for #help in https://bun.sh/discord or go to https://bun.sh/issues\n\n", .{}); - } - crash_report_writer.flush(); - - if (crash_report_writer.file) |file| { - // don't handle return codes here - _ = std.os.system.close(file.unbuffered_writer.context.handle); - } - - crash_report_writer.file = null; - if (!Environment.isWindows) { - if (error_return_trace) |trace| { - std.debug.dumpStackTrace(trace.*); - } - } - Global.runExitCallbacks(); - std.c._exit(128 + @as(u8, @truncate(@as(u8, @intCast(@max(signal, 0)))))); -} - -pub noinline fn globalError(err: anyerror, trace_: @TypeOf(@errorReturnTrace())) noreturn { - @setCold(true); - - bun.maybeHandlePanicDuringProcessReload(); - - error_return_trace = trace_; - - if (@atomicRmw(bool, &globalError_ranOnce, .Xchg, true, .Monotonic)) { - Global.exit(1); - } - - switch (err) { - // error.BrokenPipe => { - // if (comptime Environment.isNative) { - // // if stdout/stderr was closed, we don't need to print anything - // std.c._exit(bun.JSC.getGlobalExitCodeForPipeFailure()); - // } - // }, - error.SyntaxError => { - Output.prettyError( - "\nSyntaxError: An error occurred while parsing code", - .{}, - ); - Global.exit(1); - }, - error.OutOfMemory => { - Output.prettyError( - "\nOutOfMemory: There might be an infinite loop somewhere", - .{}, - ); - printMetadata(); - Global.exit(1); - }, - error.CurrentWorkingDirectoryUnlinked => { - Output.prettyError( - "\nerror: The current working directory was deleted, so that command didn't work. Please cd into a different directory and try again.", - .{}, - ); - Global.exit(1); - }, - error.InvalidArgument => { - Global.exit(1); - }, - error.SystemFdQuotaExceeded => { - if (comptime Environment.isPosix) { - const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit); - if (comptime Environment.isMac) { - Output.prettyError( - \\ - \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) - \\ - \\Current limit: {d} - \\ - \\To fix this, try running: - \\ - \\ sudo launchctl limit maxfiles 2147483646 - \\ ulimit -n 2147483646 - \\ - \\That will only work until you reboot. - \\ - , - .{ - limit.cur, - }, - ); - } else { - Output.prettyError( - \\ - \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) - \\ - \\Current limit: {d} - \\ - \\To fix this, try running: - \\ - \\ sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf - \\ sudo sysctl -p - \\ ulimit -n 2147483646 - \\ - , - .{ - limit.cur, - }, - ); - - if (bun.getenvZ("USER")) |user| { - if (user.len > 0) { - Output.prettyError( - \\ - \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: - \\ - \\ {s} soft nofile 2147483646 - \\ {s} hard nofile 2147483646 - \\ - , - .{ user, user }, - ); - } - } - } - } else { - Output.prettyError( - \\error: Your computer ran out of file descriptors (SystemFdQuotaExceeded) - , - .{}, - ); - } - - Global.exit(1); - }, - error.@"Invalid Bunfig" => { - Global.exit(1); - }, - error.ProcessFdQuotaExceeded => { - if (comptime Environment.isPosix) { - const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit); - if (comptime Environment.isMac) { - Output.prettyError( - \\ - \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) - \\ - \\Current limit: {d} - \\ - \\To fix this, try running: - \\ - \\ ulimit -n 2147483646 - \\ - \\You may also need to run: - \\ - \\ sudo launchctl limit maxfiles 2147483646 - \\ - , - .{ - limit.cur, - }, - ); - } else { - Output.prettyError( - \\ - \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) - \\ - \\Current limit: {d} - \\ - \\To fix this, try running: - \\ - \\ ulimit -n 2147483646 - \\ - \\That will only work for the current shell. To fix this for the entire system, run: - \\ - \\ sudo echo -e "\nfs.file-max=2147483646\n" >> /etc/sysctl.conf - \\ sudo sysctl -p - \\ - , - .{ - limit.cur, - }, - ); - - if (bun.getenvZ("USER")) |user| { - if (user.len > 0) { - Output.prettyError( - \\ - \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: - \\ - \\ {s} soft nofile 2147483646 - \\ {s} hard nofile 2147483646 - \\ - , - .{ user, user }, - ); - } - } - } - } else { - Output.prettyErrorln( - \\error: bun ran out of file descriptors (ProcessFdQuotaExceeded) - , - .{}, - ); - } - - Global.exit(1); - }, - // The usage of `unreachable` in Zig's std.os may cause the file descriptor problem to show up as other errors - error.NotOpenForReading, error.Unexpected => { - if (!Environment.isWindows) { - if (trace_) |trace| { - print_stacktrace: { - const debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace; - Output.disableBuffering(); - std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.io.tty.detectConfig(std.io.getStdErr())) catch break :print_stacktrace; - } - } - } - - if (comptime Environment.isPosix) { - const limit = std.os.getrlimit(.NOFILE) catch std.mem.zeroes(std.os.rlimit); - - if (limit.cur > 0 and limit.cur < (8192 * 2)) { - Output.prettyError( - \\ - \\error: An unknown error ocurred, possibly due to low max file descriptors (Unexpected) - \\ - \\Current limit: {d} - \\ - \\To fix this, try running: - \\ - \\ ulimit -n 2147483646 - \\ - , - .{ - limit.cur, - }, - ); - - if (Environment.isLinux) { - if (bun.getenvZ("USER")) |user| { - if (user.len > 0) { - Output.prettyError( - \\ - \\If that still doesn't work, you may need to add these lines to /etc/security/limits.conf: - \\ - \\ {s} soft nofile 2147483646 - \\ {s} hard nofile 2147483646 - \\ - , - .{ - user, - user, - }, - ); - } - } - } else if (Environment.isMac) { - Output.prettyError( - \\ - \\If that still doesn't work, you may need to run: - \\ - \\ sudo launchctl limit maxfiles 2147483646 - \\ - , - .{}, - ); - } - } else { - Output.prettyError( - \\error: An unknown error ocurred (Unexpected) - , - .{}, - ); - } - - Global.exit(1); - } - }, - error.ENOENT, error.FileNotFound => { - Output.prettyError( - "\nerror: FileNotFound\nBun could not find a file, and the code that produces this error is missing a better error.\n", - .{}, - ); - Output.flush(); - - Report.printMetadata(); - - Output.flush(); - - if (!Environment.isWindows) { - if (trace_) |trace| { - print_stacktrace: { - const debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace; - Output.disableBuffering(); - std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.io.tty.detectConfig(std.io.getStdErr())) catch break :print_stacktrace; - } - } - } - - Global.exit(1); - }, - error.MissingPackageJSON => { - Output.prettyError( - "\nerror: MissingPackageJSON\nBun could not find a package.json file.\n", - .{}, - ); - if (!Environment.isWindows) { - if (trace_) |trace| { - print_stacktrace: { - const debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace; - Output.disableBuffering(); - std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.io.tty.detectConfig(std.io.getStdErr())) catch break :print_stacktrace; - } - } - } - - Global.exit(1); - }, - else => {}, - } - - Report.fatal(err, null); - if (!Environment.isWindows) { - if (trace_) |trace| { - print_stacktrace: { - const debug_info = std.debug.getSelfDebugInfo() catch break :print_stacktrace; - Output.disableBuffering(); - std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.io.tty.detectConfig(std.io.getStdErr())) catch break :print_stacktrace; - } - } - } - - if (bun.auto_reload_on_crash) { - // attempt to prevent a double panic - bun.auto_reload_on_crash = false; - - Output.prettyErrorln("--- Bun is auto-restarting due to crash [time: {d}] ---", .{@max(std.time.milliTimestamp(), 0)}); - Output.flush(); - bun.reloadProcess(bun.default_allocator, false); - } - - Global.exit(1); -} diff --git a/src/sha.zig b/src/sha.zig index d12bcfe86c..f719e9a45d 100644 --- a/src/sha.zig +++ b/src/sha.zig @@ -198,7 +198,7 @@ const labels = [_][]const u8{ "Blake3", }; pub fn main() anyerror!void { - var file = try std.fs.cwd().openFileZ(bun.argv()[bun.argv().len - 1], .{}); + var file = try std.fs.cwd().openFileZ(bun.argv[bun.argv.len - 1], .{}); const bytes = try file.readToEndAlloc(std.heap.c_allocator, std.math.maxInt(usize)); const engine = BoringSSL.ENGINE_new().?; diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index 85e8cf4a22..f0f77d6178 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -678,6 +678,10 @@ pub const VLQ = struct { // I believe the actual number is 7 bytes long, however we can add an extra byte to be more cautious bytes: [vlq_max_in_bytes]u8, len: u4 = 0, + + pub fn writeTo(self: VLQ, writer: anytype) !void { + try writer.writeAll(self.bytes[0..self.len]); + } }; pub fn encodeVLQWithLookupTable( @@ -736,9 +740,7 @@ test "decodeVLQ" { // V V // 101011 // -pub fn encodeVLQ( - value: i32, -) VLQ { +pub fn encodeVLQ(value: i32) VLQ { var len: u4 = 0; var bytes: [vlq_max_in_bytes]u8 = undefined; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index aee4513b46..9ff5d75985 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5402,6 +5402,20 @@ pub fn convertUTF8toUTF16InBuffer( return buf[0..result]; } +pub fn convertUTF8toUTF16InBufferZ( + buf: []u16, + input: []const u8, +) [:0]u16 { + // TODO: see convertUTF8toUTF16InBuffer + if (input.len == 0) { + buf[0] = 0; + return buf[0..0 :0]; + } + const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); + buf[result] = 0; + return buf[0..result :0]; +} + pub fn convertUTF16toUTF8InBuffer( buf: []u8, input: []const u16, diff --git a/src/windows.zig b/src/windows.zig index 14685ecaec..2a3e8a2b70 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -77,6 +77,7 @@ pub const PathBuffer = if (Environment.isWindows) bun.PathBuffer else void; pub const WPathBuffer = if (Environment.isWindows) bun.WPathBuffer else void; pub const HANDLE = win32.HANDLE; +pub const HMODULE = win32.HMODULE; /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle pub extern "kernel32" fn GetFileInformationByHandle( @@ -3367,6 +3368,42 @@ pub fn GetFinalPathNameByHandle( return std.os.windows.GetFinalPathNameByHandle(hFile, fmt, out_buffer); } +extern "kernel32" fn GetModuleHandleExW( + dwFlags: u32, // [in] + lpModuleName: ?*anyopaque, // [in, optional] + phModule: *HMODULE, // [out] +) BOOL; + +extern "kernel32" fn GetModuleFileNameW( + hModule: HMODULE, // [in] + lpFilename: LPWSTR, // [out] + nSize: DWORD, // [in] +) BOOL; + +const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 0x00000004; + +pub fn getModuleHandleFromAddress(addr: usize) ?HMODULE { + var module: HMODULE = undefined; + const rc = GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + @ptrFromInt(addr), + &module, + ); + // If the function succeeds, the return value is nonzero. + return if (rc != 0) module else null; +} + +pub fn getModuleNameW(module: HMODULE, buf: []u16) ?[]const u16 { + const rc = GetModuleFileNameW(module, @ptrCast(buf.ptr), @intCast(buf.len)); + if (rc == 0) return null; + return buf[0..@intCast(rc)]; +} + +pub extern "kernel32" fn GetThreadDescription( + thread: ?*anyopaque, // [in] + *PWSTR, // [out] +) std.os.windows.HRESULT; + pub const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200; pub const ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002; pub const ENABLE_PROCESSED_OUTPUT = 0x0001; diff --git a/src/zlib.zig b/src/zlib.zig index 1be5855759..4a871ab72a 100644 --- a/src/zlib.zig +++ b/src/zlib.zig @@ -93,7 +93,7 @@ const z_streamp = @import("zlib-internal").z_streamp; const DataType = @import("zlib-internal").DataType; const FlushValue = @import("zlib-internal").FlushValue; -const ReturnCode = @import("zlib-internal").ReturnCode; +pub const ReturnCode = @import("zlib-internal").ReturnCode; // ZEXTERN int ZEXPORT inflateInit OF((z_streamp strm)); diff --git a/test/cli/run/fixture-crash.js b/test/cli/run/fixture-crash.js new file mode 100644 index 0000000000..9a56452ac7 --- /dev/null +++ b/test/cli/run/fixture-crash.js @@ -0,0 +1,15 @@ +let crash_handler; +try { + crash_handler = require("bun:internal-for-testing").crash_handler; +} catch { + console.error("This version of bun does not have internal-for-testing exposed"); + console.error("BUN_GARBAGE_COLLECTOR_LEVEL=0 BUN_FEATURE_FLAG_INTERNALS_FOR_TESTING=1 bun"); + process.exit(1); +} + +const approach = process.argv[2]; +if (approach in crash_handler) { + crash_handler[approach](); +} else { + console.error("usage: bun fixture-crash.js "); +} diff --git a/test/cli/run/run-crash-handler.test.ts b/test/cli/run/run-crash-handler.test.ts new file mode 100644 index 0000000000..7800bf3a2f --- /dev/null +++ b/test/cli/run/run-crash-handler.test.ts @@ -0,0 +1,29 @@ +import { crash_handler } from "bun:internal-for-testing"; +import { test, expect } from "bun:test"; +import { bunExe, bunEnv } from "harness"; +import path from "path"; +const { getMachOImageZeroOffset } = crash_handler; + +test.if(process.platform === "darwin")("macOS has the assumed image offset", () => { + // If this fails, then https://bun.report will be incorrect and the stack + // trace remappings will stop working. + expect(getMachOImageZeroOffset()).toBe(0x100000000); +}); + +test("a panic dumps a trace string", async () => { + const result = Bun.spawnSync( + [bunExe(), path.join(import.meta.dir, "fixture-crash.js"), "panic", "--debug-crash-handler-use-trace-string"], + { + env: { + ...bunEnv, + }, + }, + ); + + try { + expect(result.stderr.toString("utf-8")).toInclude("https://bun.report/"); + } catch (e) { + console.log(result.stderr.toString("utf-8")); + throw e; + } +}); diff --git a/test/harness.ts b/test/harness.ts index 8acf652663..a9d6276509 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -20,6 +20,8 @@ export const bunEnv: NodeJS.ProcessEnv = { TZ: "Etc/UTC", CI: "1", BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", + BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: '1', + BUN_GARBAGE_COLLECTOR_LEVEL: process.env.BUN_GARBAGE_COLLECTOR_LEVEL || '0', }; if (isWindows) {