mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
32 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f4b06550 | ||
|
|
c06ef30736 | ||
|
|
57efbd0be5 | ||
|
|
d66255705a | ||
|
|
d28affd937 | ||
|
|
ae2cad8ef8 | ||
|
|
f188b9352f | ||
|
|
856eda2f24 | ||
|
|
cc6704de2f | ||
|
|
0ae67c72bc | ||
|
|
d607391e53 | ||
|
|
93de1c3b2c | ||
|
|
1d2becb314 | ||
|
|
1a70d189e1 | ||
|
|
19fa3d303e | ||
|
|
60b7424a34 | ||
|
|
a028ee95df | ||
|
|
c25572ebfe | ||
|
|
1366c692e8 | ||
|
|
87524734b1 | ||
|
|
1130675215 | ||
|
|
6aeadbf7d1 | ||
|
|
d1924b8b14 | ||
|
|
997c7764c5 | ||
|
|
a859227a66 | ||
|
|
e474a1d148 | ||
|
|
0a40bb54f4 | ||
|
|
5e504db796 | ||
|
|
3e15ddc5e2 | ||
|
|
f0f5d171fc | ||
|
|
91bfb9f7a8 | ||
|
|
05ea1a2044 |
@@ -189,6 +189,7 @@ pub const Run = struct {
|
||||
.debugger = ctx.runtime_options.debugger,
|
||||
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
|
||||
.is_main_thread = true,
|
||||
.disable_sigusr1 = ctx.runtime_options.disable_sigusr1,
|
||||
},
|
||||
),
|
||||
.arena = arena,
|
||||
|
||||
@@ -1074,6 +1074,8 @@ pub fn initWithModuleGraph(
|
||||
vm.configureDebugger(opts.debugger);
|
||||
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));
|
||||
|
||||
configureSigusr1Handler(vm, opts);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
@@ -1100,8 +1102,26 @@ pub const Options = struct {
|
||||
/// Worker VMs are always destroyed on exit, regardless of this setting. Setting this to
|
||||
/// true may expose bugs that would otherwise only occur using Workers.
|
||||
destruct_main_thread_on_exit: bool = false,
|
||||
/// Disable SIGUSR1 handler for runtime debugger activation (matches Node.js).
|
||||
disable_sigusr1: bool = false,
|
||||
};
|
||||
|
||||
/// Configure SIGUSR1 handling for runtime debugger activation (main thread only).
|
||||
fn configureSigusr1Handler(vm: *const VirtualMachine, opts: Options) void {
|
||||
if (!opts.is_main_thread) return;
|
||||
|
||||
if (opts.disable_sigusr1) {
|
||||
// User requested --disable-sigusr1, set SIGUSR1 to default action (terminate)
|
||||
jsc.EventLoop.RuntimeInspector.setDefaultSigusr1Action();
|
||||
} else if (vm.debugger != null) {
|
||||
// Debugger already enabled via CLI flags, ignore SIGUSR1
|
||||
jsc.EventLoop.RuntimeInspector.ignoreSigusr1();
|
||||
} else {
|
||||
// Install RuntimeInspector signal handler for runtime activation
|
||||
jsc.EventLoop.RuntimeInspector.installIfNotAlready();
|
||||
}
|
||||
}
|
||||
|
||||
pub var is_smol_mode = false;
|
||||
|
||||
pub fn init(opts: Options) !*VirtualMachine {
|
||||
@@ -1201,6 +1221,8 @@ pub fn init(opts: Options) !*VirtualMachine {
|
||||
vm.configureDebugger(opts.debugger);
|
||||
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));
|
||||
|
||||
configureSigusr1Handler(vm, opts);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
@@ -1451,6 +1473,8 @@ pub fn initBake(opts: Options) anyerror!*VirtualMachine {
|
||||
vm.configureDebugger(opts.debugger);
|
||||
vm.body_value_hive_allocator = Body.Value.HiveAllocator.init(bun.typedAllocator(jsc.WebCore.Body.Value));
|
||||
|
||||
configureSigusr1Handler(vm, opts);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
|
||||
@@ -1346,6 +1346,9 @@ extern "C" bool Bun__shouldIgnoreOneDisconnectEventListener(JSC::JSGlobalObject*
|
||||
extern "C" void Bun__ensureSignalHandler();
|
||||
extern "C" bool Bun__isMainThreadVM();
|
||||
extern "C" void Bun__onPosixSignal(int signalNumber);
|
||||
#ifdef SIGUSR1
|
||||
extern "C" void Bun__Sigusr1Handler__uninstall();
|
||||
#endif
|
||||
|
||||
__attribute__((noinline)) static void forwardSignal(int signalNumber)
|
||||
{
|
||||
@@ -1504,6 +1507,14 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
|
||||
action.sa_flags = SA_RESTART;
|
||||
|
||||
sigaction(signalNumber, &action, nullptr);
|
||||
|
||||
#ifdef SIGUSR1
|
||||
// When user adds a SIGUSR1 listener, uninstall the automatic
|
||||
// inspector activation handler. User handlers take precedence.
|
||||
if (signalNumber == SIGUSR1) {
|
||||
Bun__Sigusr1Handler__uninstall();
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
signal_handle.handle = Bun__UVSignalHandle__init(
|
||||
eventEmitter.scriptExecutionContext()->jsGlobalObject(),
|
||||
@@ -3834,6 +3845,71 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, (JSC::JSGlobalObject * glob
|
||||
RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(result)));
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(Process_functionDebugProcess, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject));
|
||||
|
||||
if (callFrame->argumentCount() < 1) {
|
||||
throwVMError(globalObject, scope, "process._debugProcess requires a pid argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
int pid = callFrame->argument(0).toInt32(globalObject);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
// posix we can just send SIGUSR1, on windows we map a file to `bun-debug-handler-<pid>` and send to that
|
||||
#if !OS(WINDOWS)
|
||||
int result = kill(pid, SIGUSR1);
|
||||
if (result < 0) {
|
||||
throwVMError(globalObject, scope, makeString("Failed to send SIGUSR1 to process "_s, pid, ": process may not exist or permission denied"_s));
|
||||
return {};
|
||||
}
|
||||
#else
|
||||
wchar_t mappingName[64];
|
||||
swprintf(mappingName, 64, L"bun-debug-handler-%d", pid);
|
||||
|
||||
HANDLE hMapping = OpenFileMappingW(FILE_MAP_READ, FALSE, mappingName);
|
||||
if (!hMapping) {
|
||||
// Match Node.js error message for compatibility
|
||||
throwVMError(globalObject, scope, "The system cannot find the file specified."_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
void* pFunc = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, sizeof(void*));
|
||||
if (!pFunc) {
|
||||
CloseHandle(hMapping);
|
||||
throwVMError(globalObject, scope, makeString("Failed to map debug handler for process "_s, pid));
|
||||
return {};
|
||||
}
|
||||
|
||||
LPTHREAD_START_ROUTINE threadProc = *reinterpret_cast<LPTHREAD_START_ROUTINE*>(pFunc);
|
||||
UnmapViewOfFile(pFunc);
|
||||
CloseHandle(hMapping);
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
|
||||
if (!hProcess) {
|
||||
throwVMError(globalObject, scope, makeString("Failed to open process "_s, pid, ": access denied or process not found"_s));
|
||||
return {};
|
||||
}
|
||||
|
||||
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, threadProc, NULL, 0, NULL);
|
||||
if (!hThread) {
|
||||
CloseHandle(hProcess);
|
||||
throwVMError(globalObject, scope, makeString("Failed to create remote thread in process "_s, pid));
|
||||
return {};
|
||||
}
|
||||
|
||||
// Wait for the thread to complete. Closing the handles before the thread
|
||||
// finishes could terminate the remote thread before it triggers the
|
||||
// inspector in the target process. Use INFINITE to match Node.js behavior.
|
||||
WaitForSingleObject(hThread, INFINITE);
|
||||
CloseHandle(hThread);
|
||||
CloseHandle(hProcess);
|
||||
#endif
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject));
|
||||
@@ -3973,7 +4049,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
|
||||
/* Source for Process.lut.h
|
||||
@begin processObjectTable
|
||||
_debugEnd Process_stubEmptyFunction Function 0
|
||||
_debugProcess Process_stubEmptyFunction Function 0
|
||||
_debugProcess Process_functionDebugProcess Function 1
|
||||
_eval processGetEval CustomAccessor
|
||||
_fatalException Process_stubEmptyFunction Function 1
|
||||
_getActiveHandles Process_stubFunctionReturningArray Function 0
|
||||
|
||||
@@ -290,6 +290,8 @@ pub fn runImminentGCTimer(this: *EventLoop) void {
|
||||
pub fn tickConcurrentWithCount(this: *EventLoop) usize {
|
||||
this.updateCounts();
|
||||
|
||||
RuntimeInspector.checkAndActivateInspector(this.virtual_machine);
|
||||
|
||||
if (comptime Environment.isPosix) {
|
||||
if (this.signal_handler) |signal_handler| {
|
||||
signal_handler.drain(this);
|
||||
@@ -687,6 +689,7 @@ pub const DeferredTaskQueue = @import("./event_loop/DeferredTaskQueue.zig");
|
||||
pub const DeferredRepeatingTask = DeferredTaskQueue.DeferredRepeatingTask;
|
||||
pub const PosixSignalHandle = @import("./event_loop/PosixSignalHandle.zig");
|
||||
pub const PosixSignalTask = PosixSignalHandle.PosixSignalTask;
|
||||
pub const RuntimeInspector = @import("./event_loop/RuntimeInspector.zig");
|
||||
pub const MiniEventLoop = @import("./event_loop/MiniEventLoop.zig");
|
||||
pub const MiniVM = MiniEventLoop.MiniVM;
|
||||
pub const JsVM = MiniEventLoop.JsVM;
|
||||
|
||||
298
src/bun.js/event_loop/RuntimeInspector.zig
Normal file
298
src/bun.js/event_loop/RuntimeInspector.zig
Normal file
@@ -0,0 +1,298 @@
|
||||
/// Runtime Inspector Activation Handler
|
||||
///
|
||||
/// Activates the inspector/debugger at runtime via `process._debugProcess(pid)`.
|
||||
///
|
||||
/// On POSIX (macOS/Linux):
|
||||
/// - Dedicated thread waits for SIGUSR1 using sigwait()
|
||||
/// - When signal arrives, sets atomic flag and wakes event loop
|
||||
/// - Main thread checks flag on event loop tick and activates inspector
|
||||
/// - Usage: `kill -USR1 <pid>` to start debugger
|
||||
///
|
||||
/// On Windows:
|
||||
/// - Uses named file mapping mechanism (same as Node.js)
|
||||
/// - Creates "bun-debug-handler-<pid>" shared memory with function pointer
|
||||
/// - External tools use CreateRemoteThread() to call that function
|
||||
/// - Usage: `process._debugProcess(pid)` from another Bun/Node process
|
||||
///
|
||||
const RuntimeInspector = @This();
|
||||
|
||||
const log = Output.scoped(.RuntimeInspector, .hidden);
|
||||
|
||||
/// Default port for runtime-activated inspector (via SIGUSR1/process._debugProcess).
|
||||
/// Note: If this port is already in use, activation will fail with an error message.
|
||||
/// This matches Node.js behavior where SIGUSR1-activated inspectors also use a fixed
|
||||
/// port (9229). Users can pre-configure a different port using --inspect-port=<port>
|
||||
/// or --inspect=0 for automatic port selection when starting the process.
|
||||
const inspector_port = "6499";
|
||||
|
||||
var installed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
|
||||
var inspector_activation_requested: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
|
||||
|
||||
fn requestInspectorActivation() void {
|
||||
// Note: This function may be called from signal handler context on POSIX,
|
||||
// so we must only use async-signal-safe operations here.
|
||||
const vm = VirtualMachine.getMainThreadVM() orelse return;
|
||||
|
||||
inspector_activation_requested.store(true, .release);
|
||||
vm.eventLoop().wakeup();
|
||||
}
|
||||
|
||||
/// Called from main thread during event loop tick.
|
||||
pub fn checkAndActivateInspector(vm: *VirtualMachine) void {
|
||||
if (!inspector_activation_requested.swap(false, .acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log("Processing inspector activation request on main thread", .{});
|
||||
|
||||
if (vm.is_shutting_down) {
|
||||
log("VM is shutting down, ignoring inspector activation request", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if debugger is already active (prevents double activation via SIGUSR1)
|
||||
if (vm.debugger != null) {
|
||||
log("Debugger already active, ignoring SIGUSR1", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
activateInspector(vm) catch |err| {
|
||||
Output.prettyErrorln("Failed to activate inspector: {s}\n", .{@errorName(err)});
|
||||
Output.flush();
|
||||
};
|
||||
}
|
||||
|
||||
fn activateInspector(vm: *VirtualMachine) !void {
|
||||
log("Activating inspector", .{});
|
||||
|
||||
vm.debugger = .{
|
||||
.path_or_port = inspector_port,
|
||||
.from_environment_variable = "",
|
||||
.wait_for_connection = .off,
|
||||
.set_breakpoint_on_first_line = false,
|
||||
.mode = .listen,
|
||||
};
|
||||
|
||||
vm.transpiler.options.minify_identifiers = false;
|
||||
vm.transpiler.options.minify_syntax = false;
|
||||
vm.transpiler.options.minify_whitespace = false;
|
||||
vm.transpiler.options.debugger = true;
|
||||
|
||||
try Debugger.create(vm, vm.global);
|
||||
}
|
||||
|
||||
pub fn isInstalled() bool {
|
||||
return installed.load(.acquire);
|
||||
}
|
||||
|
||||
const posix = if (Environment.isPosix) struct {
|
||||
fn signalHandler(_: c_int) callconv(.c) void {
|
||||
// This handler runs in signal context, so we can only do async-signal-safe operations.
|
||||
// Set the atomic flag and wake the event loop.
|
||||
requestInspectorActivation();
|
||||
}
|
||||
|
||||
fn install() bool {
|
||||
// Install a signal handler for SIGUSR1. This approach works regardless of
|
||||
// which threads have SIGUSR1 blocked, because the handler runs in the
|
||||
// context of whichever thread receives the signal.
|
||||
var act: std.posix.Sigaction = .{
|
||||
.handler = .{ .handler = signalHandler },
|
||||
.mask = std.posix.sigemptyset(),
|
||||
.flags = std.posix.SA.RESTART,
|
||||
};
|
||||
|
||||
std.posix.sigaction(std.posix.SIG.USR1, &act, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn uninstall() void {
|
||||
// Note: We do NOT restore the previous signal handler here.
|
||||
// This function is called when a user adds their own SIGUSR1 handler,
|
||||
// and BunProcess.cpp has already set up the user's handler via sigaction().
|
||||
// Restoring the previous handler would overwrite the user's handler.
|
||||
}
|
||||
} else struct {};
|
||||
|
||||
const windows = if (Environment.isWindows) struct {
|
||||
const win32 = std.os.windows;
|
||||
const HANDLE = win32.HANDLE;
|
||||
const DWORD = win32.DWORD;
|
||||
const BOOL = win32.BOOL;
|
||||
const LPVOID = *anyopaque;
|
||||
const LPCWSTR = [*:0]const u16;
|
||||
const SIZE_T = usize;
|
||||
const INVALID_HANDLE_VALUE = win32.INVALID_HANDLE_VALUE;
|
||||
|
||||
const SECURITY_ATTRIBUTES = extern struct {
|
||||
nLength: DWORD,
|
||||
lpSecurityDescriptor: ?LPVOID,
|
||||
bInheritHandle: BOOL,
|
||||
};
|
||||
|
||||
const PAGE_READWRITE: DWORD = 0x04;
|
||||
const FILE_MAP_ALL_ACCESS: DWORD = 0xF001F;
|
||||
|
||||
const LPTHREAD_START_ROUTINE = *const fn (?LPVOID) callconv(.winapi) DWORD;
|
||||
|
||||
extern "kernel32" fn CreateFileMappingW(
|
||||
hFile: HANDLE,
|
||||
lpFileMappingAttributes: ?*SECURITY_ATTRIBUTES,
|
||||
flProtect: DWORD,
|
||||
dwMaximumSizeHigh: DWORD,
|
||||
dwMaximumSizeLow: DWORD,
|
||||
lpName: ?LPCWSTR,
|
||||
) callconv(.winapi) ?HANDLE;
|
||||
|
||||
extern "kernel32" fn MapViewOfFile(
|
||||
hFileMappingObject: HANDLE,
|
||||
dwDesiredAccess: DWORD,
|
||||
dwFileOffsetHigh: DWORD,
|
||||
dwFileOffsetLow: DWORD,
|
||||
dwNumberOfBytesToMap: SIZE_T,
|
||||
) callconv(.winapi) ?LPVOID;
|
||||
|
||||
extern "kernel32" fn UnmapViewOfFile(
|
||||
lpBaseAddress: LPVOID,
|
||||
) callconv(.winapi) BOOL;
|
||||
|
||||
extern "kernel32" fn GetCurrentProcessId() callconv(.winapi) DWORD;
|
||||
|
||||
var mapping_handle: ?HANDLE = null;
|
||||
|
||||
/// Called via CreateRemoteThread from another process.
|
||||
fn startDebugThreadProc(_: ?LPVOID) callconv(.winapi) DWORD {
|
||||
requestInspectorActivation();
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn install() bool {
|
||||
const pid = GetCurrentProcessId();
|
||||
|
||||
var mapping_name_buf: [64]u8 = undefined;
|
||||
const name_slice = std.fmt.bufPrint(&mapping_name_buf, "bun-debug-handler-{d}", .{pid}) catch return false;
|
||||
|
||||
var wide_name: [64]u16 = undefined;
|
||||
const wide_name_z = bun.strings.toWPath(&wide_name, name_slice);
|
||||
|
||||
mapping_handle = CreateFileMappingW(
|
||||
INVALID_HANDLE_VALUE,
|
||||
null,
|
||||
PAGE_READWRITE,
|
||||
0,
|
||||
@sizeOf(LPTHREAD_START_ROUTINE),
|
||||
wide_name_z.ptr,
|
||||
);
|
||||
|
||||
if (mapping_handle) |handle| {
|
||||
const handler_ptr = MapViewOfFile(
|
||||
handle,
|
||||
FILE_MAP_ALL_ACCESS,
|
||||
0,
|
||||
0,
|
||||
@sizeOf(LPTHREAD_START_ROUTINE),
|
||||
);
|
||||
|
||||
if (handler_ptr) |ptr| {
|
||||
// MapViewOfFile returns page-aligned memory, which satisfies
|
||||
// the alignment requirements for function pointers.
|
||||
const typed_ptr: *LPTHREAD_START_ROUTINE = @ptrCast(@alignCast(ptr));
|
||||
typed_ptr.* = &startDebugThreadProc;
|
||||
_ = UnmapViewOfFile(ptr);
|
||||
return true;
|
||||
} else {
|
||||
log("MapViewOfFile failed", .{});
|
||||
_ = bun.windows.CloseHandle(handle);
|
||||
mapping_handle = null;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log("CreateFileMappingW failed for bun-debug-handler-{d}", .{pid});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn uninstall() void {
|
||||
if (mapping_handle) |handle| {
|
||||
_ = bun.windows.CloseHandle(handle);
|
||||
mapping_handle = null;
|
||||
}
|
||||
}
|
||||
} else struct {};
|
||||
|
||||
/// Install the runtime inspector handler.
|
||||
/// Safe to call multiple times - subsequent calls are no-ops.
|
||||
pub fn installIfNotAlready() void {
|
||||
if (installed.swap(true, .acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const success = if (comptime Environment.isPosix)
|
||||
posix.install()
|
||||
else if (comptime Environment.isWindows)
|
||||
windows.install()
|
||||
else
|
||||
false;
|
||||
|
||||
if (!success) {
|
||||
installed.store(false, .release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Uninstall when a user SIGUSR1 listener takes over (POSIX only).
|
||||
pub fn uninstallForUserHandler() void {
|
||||
if (!installed.swap(false, .acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime Environment.isPosix) {
|
||||
posix.uninstall();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set SIGUSR1 to default action when --disable-sigusr1 is used.
|
||||
/// This allows SIGUSR1 to use its default behavior (terminate process).
|
||||
pub fn setDefaultSigusr1Action() void {
|
||||
if (comptime Environment.isPosix) {
|
||||
var act: std.posix.Sigaction = .{
|
||||
.handler = .{ .handler = std.posix.SIG.DFL },
|
||||
.mask = std.posix.sigemptyset(),
|
||||
.flags = 0,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.USR1, &act, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ignore SIGUSR1 when debugger is already enabled via CLI flags.
|
||||
/// This prevents SIGUSR1 from terminating the process when the user is already debugging.
|
||||
pub fn ignoreSigusr1() void {
|
||||
if (comptime Environment.isPosix) {
|
||||
var act: std.posix.Sigaction = .{
|
||||
.handler = .{ .handler = std.posix.SIG.IGN },
|
||||
.mask = std.posix.sigemptyset(),
|
||||
.flags = 0,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.USR1, &act, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from C++ when user adds a SIGUSR1 listener
|
||||
export fn Bun__Sigusr1Handler__uninstall() void {
|
||||
uninstallForUserHandler();
|
||||
}
|
||||
|
||||
comptime {
|
||||
if (Environment.isPosix) {
|
||||
_ = Bun__Sigusr1Handler__uninstall;
|
||||
}
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const Output = bun.Output;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const Debugger = jsc.Debugger;
|
||||
const VirtualMachine = jsc.VirtualMachine;
|
||||
@@ -393,6 +393,8 @@ pub const Command = struct {
|
||||
name: []const u8 = "",
|
||||
dir: []const u8 = "",
|
||||
} = .{},
|
||||
/// Disable SIGUSR1 handler for runtime debugger activation
|
||||
disable_sigusr1: bool = false,
|
||||
};
|
||||
|
||||
var global_cli_ctx: Context = undefined;
|
||||
|
||||
@@ -87,6 +87,7 @@ pub const runtime_params_ = [_]ParamType{
|
||||
clap.parseParam("--inspect <STR>? Activate Bun's debugger") catch unreachable,
|
||||
clap.parseParam("--inspect-wait <STR>? Activate Bun's debugger, wait for a connection before executing") catch unreachable,
|
||||
clap.parseParam("--inspect-brk <STR>? Activate Bun's debugger, set breakpoint on first line of code and wait") catch unreachable,
|
||||
clap.parseParam("--disable-sigusr1 Disable SIGUSR1 handler for runtime debugger activation") catch unreachable,
|
||||
clap.parseParam("--cpu-prof Start CPU profiler and write profile to disk on exit") catch unreachable,
|
||||
clap.parseParam("--cpu-prof-name <STR> Specify the name of the CPU profile file") catch unreachable,
|
||||
clap.parseParam("--cpu-prof-dir <STR> Specify the directory where the CPU profile will be saved") catch unreachable,
|
||||
@@ -771,6 +772,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
ctx.runtime_options.smol = args.flag("--smol");
|
||||
ctx.runtime_options.preconnect = args.options("--fetch-preconnect");
|
||||
ctx.runtime_options.expose_gc = args.flag("--expose-gc");
|
||||
ctx.runtime_options.disable_sigusr1 = args.flag("--disable-sigusr1");
|
||||
|
||||
if (args.option("--console-depth")) |depth_str| {
|
||||
const depth = std.fmt.parseInt(u16, depth_str, 10) catch {
|
||||
|
||||
@@ -1395,6 +1395,7 @@ pub const TestCommand = struct {
|
||||
.smol = ctx.runtime_options.smol,
|
||||
.debugger = ctx.runtime_options.debugger,
|
||||
.is_main_thread = true,
|
||||
.disable_sigusr1 = ctx.runtime_options.disable_sigusr1,
|
||||
},
|
||||
);
|
||||
vm.argv = ctx.passthrough;
|
||||
|
||||
351
test/js/bun/runtime-inspector/runtime-inspector-posix.test.ts
Normal file
351
test/js/bun/runtime-inspector/runtime-inspector-posix.test.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// POSIX-specific tests (SIGUSR1 mechanism) - macOS and Linux only
|
||||
describe.skipIf(isWindows)("Runtime inspector SIGUSR1 activation", () => {
|
||||
test("activates inspector when no user listener", async () => {
|
||||
using dir = tempDir("sigusr1-activate-test", {
|
||||
"test.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Write PID so parent can send signal
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep process alive
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
|
||||
// Send SIGUSR1
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
// Wait for inspector to activate by reading stderr until the full banner appears
|
||||
const stderrReader = proc.stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
// Wait for the full banner (header + content + footer)
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader.read();
|
||||
if (done) break;
|
||||
stderr += stderrDecoder.decode(value, { stream: true });
|
||||
}
|
||||
stderrReader.releaseLock();
|
||||
|
||||
// Kill process
|
||||
proc.kill();
|
||||
await proc.exited;
|
||||
|
||||
expect(stderr).toContain("Bun Inspector");
|
||||
expect(stderr).toContain("ws://localhost:6499/");
|
||||
});
|
||||
|
||||
test("user SIGUSR1 listener takes precedence over inspector activation", async () => {
|
||||
using dir = tempDir("sigusr1-user-test", {
|
||||
"test.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
process.on("SIGUSR1", () => {
|
||||
console.log("USER_HANDLER_CALLED");
|
||||
setTimeout(() => process.exit(0), 100);
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
output += decoder.decode();
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(output).toContain("USER_HANDLER_CALLED");
|
||||
expect(stderr).not.toContain("Bun Inspector");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("inspector does not activate twice via SIGUSR1", async () => {
|
||||
using dir = tempDir("sigusr1-twice-test", {
|
||||
"test.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep process alive, exit after a bit
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
// Send first SIGUSR1 and wait for inspector to activate
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
const stderrReader = proc.stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
// Wait for the full banner (header + content + footer) before sending second signal
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader.read();
|
||||
if (done) break;
|
||||
stderr += stderrDecoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
// Send second SIGUSR1 - inspector should not activate again
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
// Read any remaining stderr until process exits
|
||||
stderrReader.releaseLock();
|
||||
const [remainingStderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
stderr += remainingStderr;
|
||||
|
||||
// Should only see one "Bun Inspector" banner (two occurrences of the text, for header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("SIGUSR1 to self activates inspector", async () => {
|
||||
await using proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
setTimeout(() => process.exit(0), 300);
|
||||
// Small delay to ensure handler is installed
|
||||
setTimeout(() => {
|
||||
process.kill(process.pid, "SIGUSR1");
|
||||
}, 50);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).toContain("Bun Inspector");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("SIGUSR1 is ignored when started with --inspect", async () => {
|
||||
// When the process is started with --inspect, the debugger is already active.
|
||||
// The RuntimeInspector signal handler should NOT be installed, so SIGUSR1
|
||||
// should have no effect (default action is terminate, but signal may be ignored).
|
||||
using dir = tempDir("sigusr1-inspect-test", {
|
||||
"test.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "--inspect", "test.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = proc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
// Send SIGUSR1 - should be ignored since RuntimeInspector is not installed
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should only see one "Bun Inspector" banner (from --inspect flag, not from SIGUSR1)
|
||||
// The banner has two occurrences of "Bun Inspector" (header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("SIGUSR1 is ignored when started with --inspect-wait", async () => {
|
||||
// When the process is started with --inspect-wait, the debugger is already active.
|
||||
// Sending SIGUSR1 should NOT activate the inspector again.
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "--inspect-wait", "-e", "setTimeout(() => process.exit(0), 500)"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait for standard "Bun Inspector" message in stderr
|
||||
const reader = proc.stderr.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
while (!stderr.includes("Bun Inspector")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
stderr += decoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
// Send SIGUSR1 - should be ignored since debugger is already active
|
||||
process.kill(proc.pid, "SIGUSR1");
|
||||
|
||||
// Kill process since --inspect-wait would wait for connection
|
||||
// Signal processing is synchronous, so no sleep needed
|
||||
proc.kill();
|
||||
|
||||
// Read any remaining stderr
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
stderr += decoder.decode(value, { stream: true });
|
||||
}
|
||||
stderr += decoder.decode();
|
||||
reader.releaseLock();
|
||||
|
||||
await proc.exited;
|
||||
|
||||
// Should only see one "Bun Inspector" banner (from --inspect-wait flag, not from SIGUSR1)
|
||||
// The banner has two occurrences of "Bun Inspector" (header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
});
|
||||
|
||||
test("SIGUSR1 is ignored when started with --inspect-brk", async () => {
|
||||
// When the process is started with --inspect-brk, the debugger is already active.
|
||||
// Sending SIGUSR1 should NOT activate the inspector again.
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "--inspect-brk", "-e", "setTimeout(() => process.exit(0), 500)"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait for standard "Bun Inspector" message in stderr
|
||||
const reader = proc.stderr.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
while (!stderr.includes("Bun Inspector")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
stderr += decoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
// Send SIGUSR1 - should be ignored since debugger is already active
|
||||
process.kill(proc.pid, "SIGUSR1");
|
||||
|
||||
// Kill process since --inspect-brk would wait for connection
|
||||
// Signal processing is synchronous, so no sleep needed
|
||||
proc.kill();
|
||||
|
||||
// Read any remaining stderr
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
stderr += decoder.decode(value, { stream: true });
|
||||
}
|
||||
stderr += decoder.decode();
|
||||
reader.releaseLock();
|
||||
|
||||
await proc.exited;
|
||||
|
||||
// Should only see one "Bun Inspector" banner (from --inspect-brk flag, not from SIGUSR1)
|
||||
// The banner has two occurrences of "Bun Inspector" (header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
});
|
||||
});
|
||||
295
test/js/bun/runtime-inspector/runtime-inspector-windows.test.ts
Normal file
295
test/js/bun/runtime-inspector/runtime-inspector-windows.test.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
// Windows-specific tests (file mapping mechanism) - Windows only
|
||||
describe.skipIf(!isWindows)("Runtime inspector Windows file mapping", () => {
|
||||
test("inspector activates via file mapping mechanism", async () => {
|
||||
// This is the primary Windows test - verify the file mapping mechanism works
|
||||
using dir = tempDir("windows-file-mapping-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep process alive
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = targetProc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
|
||||
// Use _debugProcess which uses file mapping on Windows
|
||||
await using debugProc = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [debugStderr, debugExitCode] = await Promise.all([debugProc.stderr.text(), debugProc.exited]);
|
||||
|
||||
expect(debugStderr).toBe("");
|
||||
expect(debugExitCode).toBe(0);
|
||||
|
||||
// Wait for the debugger to start by reading stderr until the full banner appears
|
||||
const stderrReader = targetProc.stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
let targetStderr = "";
|
||||
// Wait for the full banner (header + content + footer)
|
||||
while ((targetStderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader.read();
|
||||
if (done) break;
|
||||
targetStderr += stderrDecoder.decode(value, { stream: true });
|
||||
}
|
||||
stderrReader.releaseLock();
|
||||
|
||||
targetProc.kill();
|
||||
await targetProc.exited;
|
||||
|
||||
// Verify inspector actually started
|
||||
expect(targetStderr).toContain("Bun Inspector");
|
||||
expect(targetStderr).toContain("ws://localhost:6499/");
|
||||
});
|
||||
|
||||
test("_debugProcess works with current process's own pid", async () => {
|
||||
// On Windows, calling _debugProcess with our own PID should work
|
||||
await using proc = spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
setTimeout(() => process.exit(0), 300);
|
||||
// Small delay to ensure handler is installed
|
||||
setTimeout(() => {
|
||||
process._debugProcess(process.pid);
|
||||
}, 50);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stderr).toContain("Bun Inspector");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("inspector does not activate twice via file mapping", async () => {
|
||||
using dir = tempDir("windows-twice-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = targetProc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
|
||||
// Set up stderr reader to wait for debugger to start
|
||||
const stderrReader = targetProc.stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
// Call _debugProcess twice
|
||||
await using debug1 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
await debug1.exited;
|
||||
|
||||
// Wait for the full banner (header + content + footer)
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader.read();
|
||||
if (done) break;
|
||||
stderr += stderrDecoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
await using debug2 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
await debug2.exited;
|
||||
|
||||
// Collect any remaining stderr and wait for process to exit
|
||||
stderrReader.releaseLock();
|
||||
const remainingStderr = await targetProc.stderr.text();
|
||||
stderr += remainingStderr;
|
||||
const exitCode = await targetProc.exited;
|
||||
|
||||
// Should only see one "Bun Inspector" banner (two occurrences of the text, for header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("multiple Windows processes can have inspectors sequentially", async () => {
|
||||
// Note: Runtime inspector uses hardcoded port 6499, so we must test
|
||||
// sequential activation (activate first, shut down, then activate second)
|
||||
// rather than concurrent activation.
|
||||
using dir = tempDir("windows-multi-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const id = process.argv[2];
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid-" + id), String(process.pid));
|
||||
console.log("READY-" + id);
|
||||
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// First process: activate inspector, verify, then shut down
|
||||
{
|
||||
await using target1 = spawn({
|
||||
cmd: [bunExe(), "target.js", "1"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader1 = target1.stdout.getReader();
|
||||
let output1 = "";
|
||||
while (!output1.includes("READY-1")) {
|
||||
const { value, done } = await reader1.read();
|
||||
if (done) break;
|
||||
output1 += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader1.releaseLock();
|
||||
|
||||
const pid1 = parseInt(await Bun.file(join(String(dir), "pid-1")).text(), 10);
|
||||
expect(pid1).toBeGreaterThan(0);
|
||||
|
||||
await using debug1 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid1})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await debug1.exited).toBe(0);
|
||||
|
||||
// Wait for the full banner
|
||||
const stderrReader1 = target1.stderr.getReader();
|
||||
const stderrDecoder1 = new TextDecoder();
|
||||
let stderr1 = "";
|
||||
while ((stderr1.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader1.read();
|
||||
if (done) break;
|
||||
stderr1 += stderrDecoder1.decode(value, { stream: true });
|
||||
}
|
||||
stderrReader1.releaseLock();
|
||||
|
||||
expect(stderr1).toContain("Bun Inspector");
|
||||
|
||||
target1.kill();
|
||||
await target1.exited;
|
||||
}
|
||||
|
||||
// Second process: now that first is shut down, port 6499 is free
|
||||
{
|
||||
await using target2 = spawn({
|
||||
cmd: [bunExe(), "target.js", "2"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader2 = target2.stdout.getReader();
|
||||
let output2 = "";
|
||||
while (!output2.includes("READY-2")) {
|
||||
const { value, done } = await reader2.read();
|
||||
if (done) break;
|
||||
output2 += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader2.releaseLock();
|
||||
|
||||
const pid2 = parseInt(await Bun.file(join(String(dir), "pid-2")).text(), 10);
|
||||
expect(pid2).toBeGreaterThan(0);
|
||||
|
||||
await using debug2 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid2})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await debug2.exited).toBe(0);
|
||||
|
||||
// Wait for the full banner
|
||||
const stderrReader2 = target2.stderr.getReader();
|
||||
const stderrDecoder2 = new TextDecoder();
|
||||
let stderr2 = "";
|
||||
while ((stderr2.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader2.read();
|
||||
if (done) break;
|
||||
stderr2 += stderrDecoder2.decode(value, { stream: true });
|
||||
}
|
||||
stderrReader2.releaseLock();
|
||||
|
||||
expect(stderr2).toContain("Bun Inspector");
|
||||
|
||||
target2.kill();
|
||||
await target2.exited;
|
||||
}
|
||||
});
|
||||
});
|
||||
364
test/js/bun/runtime-inspector/runtime-inspector.test.ts
Normal file
364
test/js/bun/runtime-inspector/runtime-inspector.test.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { spawn } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
/**
|
||||
* Reads from a stderr stream until the full Bun Inspector banner appears.
|
||||
* The banner has "Bun Inspector" in both header and footer lines.
|
||||
* Returns the accumulated stderr output.
|
||||
*/
|
||||
async function waitForDebuggerListening(
|
||||
stderrStream: ReadableStream<Uint8Array>,
|
||||
): Promise<{ stderr: string; reader: ReadableStreamDefaultReader<Uint8Array> }> {
|
||||
const reader = stderrStream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
// Wait for the full banner (header + content + footer)
|
||||
// The banner format is:
|
||||
// --------------------- Bun Inspector ---------------------
|
||||
// Listening:
|
||||
// ws://localhost:6499/...
|
||||
// Inspect in browser:
|
||||
// https://debug.bun.sh/#localhost:6499/...
|
||||
// --------------------- Bun Inspector ---------------------
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
stderr += decoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
return { stderr, reader };
|
||||
}
|
||||
|
||||
// Cross-platform tests - run on ALL platforms (Windows, macOS, Linux)
|
||||
// Windows uses file mapping mechanism, POSIX uses SIGUSR1
|
||||
describe("Runtime inspector activation", () => {
|
||||
describe("process._debugProcess", () => {
|
||||
test("activates inspector in target process", async () => {
|
||||
using dir = tempDir("debug-process-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Write PID so parent can find us
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep process alive
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
// Start target process
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait for target to be ready
|
||||
const reader = targetProc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
// Use _debugProcess to activate inspector
|
||||
await using debugProc = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [debugStderr, debugExitCode] = await Promise.all([debugProc.stderr.text(), debugProc.exited]);
|
||||
|
||||
expect(debugStderr).toBe("");
|
||||
expect(debugExitCode).toBe(0);
|
||||
|
||||
// Wait for inspector to activate by reading stderr until we see the message
|
||||
const { stderr: targetStderr, reader: stderrReader } = await waitForDebuggerListening(targetProc.stderr);
|
||||
stderrReader.releaseLock();
|
||||
|
||||
// Kill target
|
||||
targetProc.kill();
|
||||
await targetProc.exited;
|
||||
|
||||
expect(targetStderr).toContain("Bun Inspector");
|
||||
expect(targetStderr).toContain("ws://localhost:6499/");
|
||||
});
|
||||
|
||||
test.todoIf(isWindows)("throws error for non-existent process", async () => {
|
||||
// Use a PID that definitely doesn't exist
|
||||
const fakePid = 999999999;
|
||||
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${fakePid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Failed");
|
||||
});
|
||||
|
||||
test("inspector does not activate twice", async () => {
|
||||
using dir = tempDir("debug-process-twice-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep process alive long enough for both _debugProcess calls
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = targetProc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
// Start reading stderr before triggering debugger
|
||||
const stderrReader = targetProc.stderr.getReader();
|
||||
const stderrDecoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
// Call _debugProcess the first time
|
||||
await using debug1 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(await debug1.exited).toBe(0);
|
||||
|
||||
// Wait for the full debugger banner (header + content + footer)
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
const { value, done } = await stderrReader.read();
|
||||
if (done) break;
|
||||
stderr += stderrDecoder.decode(value, { stream: true });
|
||||
}
|
||||
|
||||
// Call _debugProcess again - inspector should not activate twice
|
||||
await using debug2 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(await debug2.exited).toBe(0);
|
||||
|
||||
// Release the reader and kill the target
|
||||
stderrReader.releaseLock();
|
||||
targetProc.kill();
|
||||
await targetProc.exited;
|
||||
|
||||
// Should only see one "Bun Inspector" banner (two occurrences of the text, for header and footer)
|
||||
const matches = stderr.match(/Bun Inspector/g);
|
||||
expect(matches?.length ?? 0).toBe(2);
|
||||
});
|
||||
|
||||
test("can activate inspector in multiple processes sequentially", async () => {
|
||||
// Note: Runtime inspector uses hardcoded port 6499, so we must test
|
||||
// sequential activation (activate first, shut down, then activate second)
|
||||
// rather than concurrent activation.
|
||||
using dir = tempDir("debug-process-multi-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const id = process.argv[2];
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid-" + id), String(process.pid));
|
||||
console.log("READY-" + id);
|
||||
|
||||
// Keep alive long enough for _debugProcess call
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
// First process: activate inspector, verify, then shut down
|
||||
{
|
||||
await using target1 = spawn({
|
||||
cmd: [bunExe(), "target.js", "1"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader1 = target1.stdout.getReader();
|
||||
let output1 = "";
|
||||
while (!output1.includes("READY-1")) {
|
||||
const { value, done } = await reader1.read();
|
||||
if (done) break;
|
||||
output1 += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader1.releaseLock();
|
||||
|
||||
const pid1 = parseInt(await Bun.file(join(String(dir), "pid-1")).text(), 10);
|
||||
expect(pid1).toBeGreaterThan(0);
|
||||
|
||||
await using debug1 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid1})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await debug1.exited).toBe(0);
|
||||
|
||||
const result1 = await waitForDebuggerListening(target1.stderr);
|
||||
result1.reader.releaseLock();
|
||||
|
||||
expect(result1.stderr).toContain("Bun Inspector");
|
||||
|
||||
target1.kill();
|
||||
await target1.exited;
|
||||
}
|
||||
|
||||
// Second process: now that first is shut down, port 6499 is free
|
||||
{
|
||||
await using target2 = spawn({
|
||||
cmd: [bunExe(), "target.js", "2"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader2 = target2.stdout.getReader();
|
||||
let output2 = "";
|
||||
while (!output2.includes("READY-2")) {
|
||||
const { value, done } = await reader2.read();
|
||||
if (done) break;
|
||||
output2 += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader2.releaseLock();
|
||||
|
||||
const pid2 = parseInt(await Bun.file(join(String(dir), "pid-2")).text(), 10);
|
||||
expect(pid2).toBeGreaterThan(0);
|
||||
|
||||
await using debug2 = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess(${pid2})`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(await debug2.exited).toBe(0);
|
||||
|
||||
const result2 = await waitForDebuggerListening(target2.stderr);
|
||||
result2.reader.releaseLock();
|
||||
|
||||
expect(result2.stderr).toContain("Bun Inspector");
|
||||
|
||||
target2.kill();
|
||||
await target2.exited;
|
||||
}
|
||||
});
|
||||
|
||||
test("throws when called with no arguments", async () => {
|
||||
await using proc = spawn({
|
||||
cmd: [bunExe(), "-e", `process._debugProcess()`],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("requires a pid argument");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// POSIX-only: --disable-sigusr1 test
|
||||
// On POSIX, when --disable-sigusr1 is set, no SIGUSR1 handler is installed,
|
||||
// so SIGUSR1 uses the default action (terminate process with exit code 128+30=158)
|
||||
// This test is skipped on Windows since there's no SIGUSR1 signal there.
|
||||
|
||||
describe.skipIf(isWindows)("--disable-sigusr1", () => {
|
||||
test("prevents inspector activation and uses default signal behavior", async () => {
|
||||
using dir = tempDir("disable-sigusr1-test", {
|
||||
"target.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "pid"), String(process.pid));
|
||||
console.log("READY");
|
||||
|
||||
// Keep alive long enough for signal to be sent
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
// Start with --disable-sigusr1
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "--disable-sigusr1", "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const reader = targetProc.stdout.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let output = "";
|
||||
while (!output.includes("READY")) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
reader.releaseLock();
|
||||
|
||||
const pid = parseInt(await Bun.file(join(String(dir), "pid")).text(), 10);
|
||||
|
||||
// Send SIGUSR1 directly - without handler, this will terminate the process
|
||||
process.kill(pid, "SIGUSR1");
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([targetProc.stderr.text(), targetProc.exited]);
|
||||
|
||||
// Should NOT see debugger listening message
|
||||
expect(stderr).not.toContain("Debugger listening");
|
||||
// Process should be terminated by SIGUSR1
|
||||
// Exit code = 128 + signal number (macOS: SIGUSR1=30 -> 158, Linux: SIGUSR1=10 -> 138)
|
||||
expect(exitCode).toBeOneOf([158, 138]);
|
||||
});
|
||||
});
|
||||
@@ -684,7 +684,6 @@ describe.concurrent(() => {
|
||||
|
||||
const undefinedStubs = [
|
||||
"_debugEnd",
|
||||
"_debugProcess",
|
||||
"_fatalException",
|
||||
"_linkedBinding",
|
||||
"_rawDebug",
|
||||
|
||||
Reference in New Issue
Block a user