mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
53 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3bde22ca1 | ||
|
|
e4cd00dda2 | ||
|
|
83a78f3336 | ||
|
|
6bd0cf31c9 | ||
|
|
1f70115906 | ||
|
|
098bcfa318 | ||
|
|
9195e68e27 | ||
|
|
f2a6c7c233 | ||
|
|
75a7ee527e | ||
|
|
3466088fcc | ||
|
|
e7ef32e9ca | ||
|
|
c83254abc0 | ||
|
|
e8f40c2329 | ||
|
|
ef5b11c1e4 | ||
|
|
f758a5f838 | ||
|
|
21fb83eb37 | ||
|
|
8990dfc2ef | ||
|
|
d9b396a29d | ||
|
|
3b67f3f77e | ||
|
|
0869bd738d | ||
|
|
b848ac202d | ||
|
|
aec9e812f1 | ||
|
|
ebcfbd2531 | ||
|
|
c06ef30736 | ||
|
|
57efbd0be5 | ||
|
|
d66255705a | ||
|
|
d28affd937 | ||
|
|
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 |
@@ -6,8 +6,7 @@ To do that:
|
||||
- git fetch upstream
|
||||
- git merge upstream main
|
||||
- Fix the merge conflicts
|
||||
- cd ../../ (back to bun)
|
||||
- make jsc-build (this will take about 7 minutes)
|
||||
- bun build.ts debug
|
||||
- While it compiles, in another task review the JSC commits between the last version of Webkit and the new version. Write up a summary of the webkit changes in a file called "webkit-changes.md"
|
||||
- bun run build:local (build a build of Bun with the new Webkit, make sure it compiles)
|
||||
- After making sure it compiles, run some code to make sure things work. something like ./build/debug-local/bun-debug --print '42' should be all you need
|
||||
@@ -21,3 +20,7 @@ To do that:
|
||||
- commit + push (without adding the webkit-changes.md file)
|
||||
- create PR titled "Upgrade Webkit to the <commit-sha>", paste your webkit-changes.md into the PR description
|
||||
- delete the webkit-changes.md file
|
||||
|
||||
Things to check for a successful upgrade:
|
||||
- Did JSType in vendor/WebKit/Source/JavaScriptCore have any recent changes? Does the enum values align with whats present in src/bun.js/bindings/JSType.zig?
|
||||
- Were there any changes to the webcore code generator? If there are C++ compilation errors, check for differences in some of the generated code in like vendor/WebKit/source/WebCore/bindings/scripts/test/JS/
|
||||
|
||||
@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
|
||||
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
|
||||
|
||||
if(NOT WEBKIT_VERSION)
|
||||
set(WEBKIT_VERSION 1d0216219a3c52cb85195f48f19ba7d5db747ff7)
|
||||
set(WEBKIT_VERSION preview-pr-135-6ba5c161)
|
||||
endif()
|
||||
|
||||
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)
|
||||
@@ -33,8 +33,8 @@ if(WEBKIT_LOCAL)
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders
|
||||
${WEBKIT_PATH}/bmalloc/Headers
|
||||
${WEBKIT_PATH}/WTF/Headers
|
||||
${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector
|
||||
${WEBKIT_PATH}/JavaScriptCore/PrivateHeaders/JavaScriptCore
|
||||
${WEBKIT_PATH}/JavaScriptCore/DerivedSources/inspector
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
#include <JavaScriptCore/InspectorFrontendChannel.h>
|
||||
#include <JavaScriptCore/StopTheWorldCallback.h>
|
||||
#include <JavaScriptCore/VMManager.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectDebuggable.h>
|
||||
#include <JavaScriptCore/JSGlobalObjectDebugger.h>
|
||||
#include <JavaScriptCore/Debugger.h>
|
||||
@@ -659,3 +661,36 @@ extern "C" void Debugger__willDispatchAsyncCall(JSGlobalObject* globalObject, As
|
||||
agent->willDispatchAsyncCall(getCallType(callType), callbackId);
|
||||
}
|
||||
}
|
||||
|
||||
// StopTheWorld callback for SIGUSR1 debugger activation.
|
||||
// This runs on the main thread at a safe point when VMManager::requestStopAll(JSDebugger) is called.
|
||||
//
|
||||
// This handles the case where JS is actively executing (including infinite loops).
|
||||
// For idle VMs, RuntimeInspector::checkAndActivateInspector handles it via event loop.
|
||||
|
||||
extern "C" bool Bun__activateInspector();
|
||||
|
||||
JSC::StopTheWorldStatus Bun__jsDebuggerCallback(JSC::VM& vm, JSC::StopTheWorldEvent event)
|
||||
{
|
||||
using namespace JSC;
|
||||
|
||||
if (event != StopTheWorldEvent::VMStopped)
|
||||
return STW_CONTINUE();
|
||||
|
||||
if (Bun__activateInspector()) {
|
||||
vm.notifyNeedDebuggerBreak();
|
||||
}
|
||||
|
||||
return STW_RESUME_ALL();
|
||||
}
|
||||
|
||||
// Zig bindings for VMManager
|
||||
extern "C" void VMManager__requestStopAll(uint32_t reason)
|
||||
{
|
||||
JSC::VMManager::requestStopAll(static_cast<JSC::VMManager::StopReason>(reason));
|
||||
}
|
||||
|
||||
extern "C" void VMManager__requestResumeAll(uint32_t reason)
|
||||
{
|
||||
JSC::VMManager::requestResumeAll(static_cast<JSC::VMManager::StopReason>(reason));
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace WebCore {
|
||||
class JSHeapData;
|
||||
|
||||
class DOMGCOutputConstraint : public JSC::MarkingConstraint {
|
||||
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(DOMEGCOutputConstraint);
|
||||
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(DOMGCOutputConstraint);
|
||||
|
||||
public:
|
||||
DOMGCOutputConstraint(JSC::VM&, JSHeapData&);
|
||||
|
||||
@@ -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 briefly for the thread to complete because closing the handles
|
||||
// immediately could terminate the remote thread before it finishes
|
||||
// triggering the inspector in the target process.
|
||||
WaitForSingleObject(hThread, 1000);
|
||||
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
|
||||
|
||||
30
src/bun.js/bindings/VMManager.zig
Normal file
30
src/bun.js/bindings/VMManager.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
/// Zig bindings for JSC::VMManager
|
||||
///
|
||||
/// VMManager coordinates multiple VMs (workers) and provides the StopTheWorld
|
||||
/// mechanism for safely interrupting JavaScript execution at safe points.
|
||||
///
|
||||
/// Note: StopReason values are bitmasks (1 << bit_position), not sequential.
|
||||
/// This matches the C++ enum in VMManager.h which uses:
|
||||
/// enum class StopReason : StopRequestBits { None = 0, GC = 1, WasmDebugger = 2, MemoryDebugger = 4, JSDebugger = 8 }
|
||||
pub const StopReason = enum(u32) {
|
||||
None = 0,
|
||||
GC = 1 << 0, // 1
|
||||
WasmDebugger = 1 << 1, // 2
|
||||
MemoryDebugger = 1 << 2, // 4
|
||||
JSDebugger = 1 << 3, // 8
|
||||
};
|
||||
|
||||
extern fn VMManager__requestStopAll(reason: StopReason) void;
|
||||
extern fn VMManager__requestResumeAll(reason: StopReason) void;
|
||||
|
||||
/// Request all VMs to stop at their next safe point.
|
||||
/// The registered StopTheWorld callback for the given reason will be called
|
||||
/// on the main thread once all VMs have stopped.
|
||||
pub fn requestStopAll(reason: StopReason) void {
|
||||
VMManager__requestStopAll(reason);
|
||||
}
|
||||
|
||||
/// Clear the pending stop request and resume all VMs.
|
||||
pub fn requestResumeAll(reason: StopReason) void {
|
||||
VMManager__requestResumeAll(reason);
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "JavaScriptCore/StackFrame.h"
|
||||
#include "JavaScriptCore/StackVisitor.h"
|
||||
#include "JavaScriptCore/VM.h"
|
||||
#include "JavaScriptCore/VMManager.h"
|
||||
#include "AddEventListenerOptions.h"
|
||||
#include "AsyncContextFrame.h"
|
||||
#include "BunClientData.h"
|
||||
@@ -266,6 +267,10 @@ extern "C" unsigned getJSCBytecodeCacheVersion()
|
||||
extern "C" void Bun__REPRL__registerFuzzilliFunctions(Zig::GlobalObject*);
|
||||
#endif
|
||||
|
||||
// StopTheWorld callback for SIGUSR1 debugger activation (defined in BunDebugger.cpp).
|
||||
// Note: This is a C++ function - cannot use extern "C" because it returns std::pair.
|
||||
JSC::StopTheWorldStatus Bun__jsDebuggerCallback(JSC::VM&, JSC::StopTheWorldEvent);
|
||||
|
||||
extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(const char* ptr, size_t length), bool evalMode)
|
||||
{
|
||||
static std::once_flag jsc_init_flag;
|
||||
@@ -287,6 +292,11 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
|
||||
#endif
|
||||
|
||||
JSC::initialize();
|
||||
|
||||
// Register the StopTheWorld callback for SIGUSR1 debugger activation.
|
||||
// This allows us to interrupt infinite loops and activate the debugger.
|
||||
JSC::VMManager::setJSDebuggerCallback(Bun__jsDebuggerCallback);
|
||||
|
||||
{
|
||||
|
||||
JSC::Options::AllowUnfinalizedAccessScope scope;
|
||||
@@ -730,13 +740,18 @@ JSC::ScriptExecutionStatus Zig::GlobalObject::scriptExecutionStatus(JSC::JSGloba
|
||||
|
||||
void unsafeEvalNoop(JSGlobalObject*, const WTF::String&) {}
|
||||
|
||||
static void queueMicrotaskToEventLoop(JSGlobalObject& globalObject, QueuedTask&& task)
|
||||
{
|
||||
globalObject.vm().queueMicrotask(WTF::move(task));
|
||||
}
|
||||
|
||||
const JSC::GlobalObjectMethodTable& GlobalObject::globalObjectMethodTable()
|
||||
{
|
||||
static const JSC::GlobalObjectMethodTable table = {
|
||||
&supportsRichSourceInfo,
|
||||
&shouldInterruptScript,
|
||||
&javaScriptRuntimeFlags,
|
||||
nullptr, // &queueMicrotaskToEventLoop, // queueTaskToEventLoop
|
||||
&queueMicrotaskToEventLoop,
|
||||
nullptr, // &shouldInterruptScriptBeforeTimeout,
|
||||
&moduleLoaderImportModule, // moduleLoaderImportModule
|
||||
&moduleLoaderResolve, // moduleLoaderResolve
|
||||
@@ -765,8 +780,7 @@ const JSC::GlobalObjectMethodTable& EvalGlobalObject::globalObjectMethodTable()
|
||||
&supportsRichSourceInfo,
|
||||
&shouldInterruptScript,
|
||||
&javaScriptRuntimeFlags,
|
||||
// &queueMicrotaskToEventLoop, // queueTaskToEventLoop
|
||||
nullptr,
|
||||
&queueMicrotaskToEventLoop,
|
||||
nullptr, // &shouldInterruptScriptBeforeTimeout,
|
||||
&moduleLoaderImportModule, // moduleLoaderImportModule
|
||||
&moduleLoaderResolve, // moduleLoaderResolve
|
||||
@@ -1072,7 +1086,7 @@ JSC_DEFINE_HOST_FUNCTION(functionQueueMicrotask,
|
||||
// BunPerformMicrotaskJob accepts a variable number of arguments (up to: performMicrotask, job, asyncContext, arg0, arg1).
|
||||
// The runtime inspects argumentCount to determine which arguments are present, so callers may pass only the subset they need.
|
||||
// Here we pass: function, callback, asyncContext.
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, globalObject, function, callback, asyncContext };
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, function, callback, asyncContext };
|
||||
globalObject->vm().queueMicrotask(WTF::move(task));
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
@@ -3103,7 +3117,7 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskCallback(Zig::GlobalObject* g
|
||||
|
||||
// Do not use JSCell* here because the GC will try to visit it.
|
||||
// Use BunInvokeJobWithArguments to pass the two arguments (ptr and callback) to the trampoline function
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunInvokeJobWithArguments, globalObject, function, JSValue(std::bit_cast<double>(reinterpret_cast<uintptr_t>(ptr))), JSValue(std::bit_cast<double>(reinterpret_cast<uintptr_t>(callback))) };
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunInvokeJobWithArguments, 0, globalObject, function, JSValue(std::bit_cast<double>(reinterpret_cast<uintptr_t>(ptr))), JSValue(std::bit_cast<double>(reinterpret_cast<uintptr_t>(callback))) };
|
||||
globalObject->vm().queueMicrotask(WTF::move(task));
|
||||
}
|
||||
|
||||
|
||||
@@ -3540,7 +3540,7 @@ void JSC__JSPromise__rejectOnNextTickWithHandled(JSC::JSPromise* promise, JSC::J
|
||||
value = jsUndefined();
|
||||
}
|
||||
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, globalObject, microtaskFunction, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microtaskFunction, rejectPromiseFunction, globalObject->m_asyncContextData.get()->getInternalField(0), promise, value };
|
||||
globalObject->vm().queueMicrotask(WTF::move(task));
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
}
|
||||
@@ -5428,7 +5428,7 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC::JSGlobalObject* arg0
|
||||
|
||||
#endif
|
||||
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, globalObject, microTaskFunction, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
|
||||
JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, microTaskFunction, WTF::move(microtaskArgs[0]), WTF::move(microtaskArgs[1]), WTF::move(microtaskArgs[2]), WTF::move(microtaskArgs[3]) };
|
||||
globalObject->vm().queueMicrotask(WTF::move(task));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ JSC_DECLARE_HOST_FUNCTION(jsVerifyOneShot);
|
||||
static const unsigned int NoDsaSignature = static_cast<unsigned int>(-1);
|
||||
|
||||
struct SignJobCtx {
|
||||
WTF_MAKE_TZONE_ALLOCATED(name);
|
||||
WTF_MAKE_TZONE_ALLOCATED(SignJobCtx);
|
||||
|
||||
public:
|
||||
enum class Mode {
|
||||
|
||||
@@ -33,7 +33,7 @@ struct EventInit {
|
||||
bool composed { false };
|
||||
|
||||
template<class Encoder> void encode(Encoder&) const;
|
||||
template<class Decoder> WARN_UNUSED_RETURN static bool decode(Decoder&, EventInit&);
|
||||
template<class Decoder> [[nodiscard]] static bool decode(Decoder&, EventInit&);
|
||||
};
|
||||
|
||||
template<class Encoder>
|
||||
|
||||
@@ -261,7 +261,7 @@ public:
|
||||
}
|
||||
|
||||
template<class Encoder> void encode(Encoder &) const;
|
||||
template<class Decoder> WARN_UNUSED_RETURN static bool decode(Decoder &, HTTPHeaderMap &);
|
||||
template<class Decoder> [[nodiscard]] static bool decode(Decoder &, HTTPHeaderMap &);
|
||||
void setUncommonHeader(const String &name, const String &value);
|
||||
void setUncommonHeaderCloneName(const StringView name, const String &value);
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace WebCore {
|
||||
|
||||
using NavigationTimingFunction = unsigned long long (PerformanceTiming::*)() const;
|
||||
|
||||
static constexpr std::array<std::pair<ComparableASCIILiteral, NavigationTimingFunction>, 21> restrictedMarkMappings { {
|
||||
static constexpr SortedArrayMap restrictedMarkFunctions { std::to_array<std::pair<ComparableASCIILiteral, NavigationTimingFunction>>({
|
||||
{ "connectEnd"_s, &PerformanceTiming::connectEnd },
|
||||
{ "connectStart"_s, &PerformanceTiming::connectStart },
|
||||
{ "domComplete"_s, &PerformanceTiming::domComplete },
|
||||
@@ -65,8 +65,7 @@ static constexpr std::array<std::pair<ComparableASCIILiteral, NavigationTimingFu
|
||||
{ "secureConnectionStart"_s, &PerformanceTiming::secureConnectionStart },
|
||||
{ "unloadEventEnd"_s, &PerformanceTiming::unloadEventEnd },
|
||||
{ "unloadEventStart"_s, &PerformanceTiming::unloadEventStart },
|
||||
} };
|
||||
static constexpr SortedArrayMap restrictedMarkFunctions { restrictedMarkMappings };
|
||||
}) };
|
||||
|
||||
bool PerformanceUserTiming::isRestrictedMarkName(const String& markName)
|
||||
{
|
||||
|
||||
@@ -87,12 +87,11 @@ template<> JSString* convertEnumerationToJS(JSGlobalObject& lexicalGlobalObject,
|
||||
template<> std::optional<CryptoKey::Type> parseEnumeration<CryptoKey::Type>(JSGlobalObject& lexicalGlobalObject, JSValue value)
|
||||
{
|
||||
auto stringValue = value.toWTFString(&lexicalGlobalObject);
|
||||
static constexpr std::array<std::pair<ComparableASCIILiteral, CryptoKey::Type>, 3> mappings { {
|
||||
static constexpr SortedArrayMap enumerationMapping { std::to_array<std::pair<ComparableASCIILiteral, CryptoKey::Type>>({
|
||||
{ "private"_s, CryptoKey::Type::Private },
|
||||
{ "public"_s, CryptoKey::Type::Public },
|
||||
{ "secret"_s, CryptoKey::Type::Secret },
|
||||
} };
|
||||
static constexpr SortedArrayMap enumerationMapping { mappings };
|
||||
}) };
|
||||
if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]
|
||||
return *enumerationValue;
|
||||
return std::nullopt;
|
||||
|
||||
@@ -64,7 +64,7 @@ template<> JSString* convertEnumerationToJS(JSGlobalObject& lexicalGlobalObject,
|
||||
template<> std::optional<CryptoKeyUsage> parseEnumeration<CryptoKeyUsage>(JSGlobalObject& lexicalGlobalObject, JSValue value)
|
||||
{
|
||||
auto stringValue = value.toWTFString(&lexicalGlobalObject);
|
||||
static constexpr std::array<std::pair<ComparableASCIILiteral, CryptoKeyUsage>, 8> mappings { {
|
||||
static constexpr SortedArrayMap enumerationMapping { std::to_array<std::pair<ComparableASCIILiteral, CryptoKeyUsage>>({
|
||||
{ "decrypt"_s, CryptoKeyUsage::Decrypt },
|
||||
{ "deriveBits"_s, CryptoKeyUsage::DeriveBits },
|
||||
{ "deriveKey"_s, CryptoKeyUsage::DeriveKey },
|
||||
@@ -73,8 +73,7 @@ template<> std::optional<CryptoKeyUsage> parseEnumeration<CryptoKeyUsage>(JSGlob
|
||||
{ "unwrapKey"_s, CryptoKeyUsage::UnwrapKey },
|
||||
{ "verify"_s, CryptoKeyUsage::Verify },
|
||||
{ "wrapKey"_s, CryptoKeyUsage::WrapKey },
|
||||
} };
|
||||
static constexpr SortedArrayMap enumerationMapping { mappings };
|
||||
}) };
|
||||
if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]
|
||||
return *enumerationValue;
|
||||
return std::nullopt;
|
||||
|
||||
@@ -96,13 +96,12 @@ template<> JSString* convertEnumerationToJS(JSGlobalObject& lexicalGlobalObject,
|
||||
template<> std::optional<SubtleCrypto::KeyFormat> parseEnumeration<SubtleCrypto::KeyFormat>(JSGlobalObject& lexicalGlobalObject, JSValue value)
|
||||
{
|
||||
auto stringValue = value.toWTFString(&lexicalGlobalObject);
|
||||
static constexpr std::array<std::pair<ComparableASCIILiteral, SubtleCrypto::KeyFormat>, 4> mappings { {
|
||||
static constexpr SortedArrayMap enumerationMapping { std::to_array<std::pair<ComparableASCIILiteral, SubtleCrypto::KeyFormat>>({
|
||||
{ "jwk"_s, SubtleCrypto::KeyFormat::Jwk },
|
||||
{ "pkcs8"_s, SubtleCrypto::KeyFormat::Pkcs8 },
|
||||
{ "raw"_s, SubtleCrypto::KeyFormat::Raw },
|
||||
{ "spki"_s, SubtleCrypto::KeyFormat::Spki },
|
||||
} };
|
||||
static constexpr SortedArrayMap enumerationMapping { mappings };
|
||||
}) };
|
||||
if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]
|
||||
return *enumerationValue;
|
||||
return std::nullopt;
|
||||
|
||||
@@ -290,6 +290,10 @@ pub fn runImminentGCTimer(this: *EventLoop) void {
|
||||
pub fn tickConcurrentWithCount(this: *EventLoop) usize {
|
||||
this.updateCounts();
|
||||
|
||||
if (this.virtual_machine.is_main_thread) {
|
||||
RuntimeInspector.checkAndActivateInspector();
|
||||
}
|
||||
|
||||
if (comptime Environment.isPosix) {
|
||||
if (this.signal_handler) |signal_handler| {
|
||||
signal_handler.drain(this);
|
||||
@@ -687,6 +691,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;
|
||||
|
||||
380
src/bun.js/event_loop/RuntimeInspector.zig
Normal file
380
src/bun.js/event_loop/RuntimeInspector.zig
Normal file
@@ -0,0 +1,380 @@
|
||||
/// Runtime Inspector Activation Handler
|
||||
///
|
||||
/// Activates the inspector/debugger at runtime via `process._debugProcess(pid)`.
|
||||
///
|
||||
/// On POSIX (macOS/Linux):
|
||||
/// - A "SignalInspector" thread sleeps on a semaphore
|
||||
/// - SIGUSR1 handler runs on the main thread but in signal context (only
|
||||
/// async-signal-safe functions allowed), posts to the semaphore
|
||||
/// - SignalInspector thread wakes in normal context, calls VMManager::requestStopAll
|
||||
/// - JSC stops all VMs at safe points and calls our StopTheWorld callback
|
||||
/// - Callback runs on main thread, activates inspector, then resumes all VMs
|
||||
/// - 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
|
||||
/// - The remote thread is already in normal context, so can call JSC APIs directly
|
||||
/// - Usage: `process._debugProcess(pid)` from another Bun/Node process
|
||||
///
|
||||
/// Why StopTheWorld? Unlike notifyNeedDebuggerBreak() which only works if a debugger
|
||||
/// is already attached, StopTheWorld guarantees a callback runs on the main thread
|
||||
/// at a safe point - even during `while(true) {}` loops. This allows us to CREATE
|
||||
/// the debugger before pausing.
|
||||
///
|
||||
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);
|
||||
|
||||
/// Called from the dedicated SignalInspector thread (POSIX) or remote thread (Windows).
|
||||
/// This runs in normal thread context, so it's safe to call JSC APIs.
|
||||
fn requestInspectorActivation() void {
|
||||
inspector_activation_requested.store(true, .release);
|
||||
|
||||
// Two mechanisms work together to handle all cases:
|
||||
//
|
||||
// 1. StopTheWorld (for busy loops like `while(true){}`):
|
||||
// requestStopAll sets a trap that fires at the next JS safe point.
|
||||
// Our callback (Bun__jsDebuggerCallback) then activates the inspector.
|
||||
//
|
||||
// 2. Event loop wakeup (for idle VMs waiting on I/O):
|
||||
// The wakeup causes checkAndActivateInspector to run, which activates
|
||||
// the inspector and calls requestResumeAll to clear any pending trap.
|
||||
//
|
||||
// Both mechanisms check inspector_activation_requested and clear it atomically,
|
||||
// so only one will actually activate the inspector.
|
||||
|
||||
jsc.VMManager.requestStopAll(.JSDebugger);
|
||||
|
||||
if (VirtualMachine.getMainThreadVM()) |vm| {
|
||||
vm.eventLoop().wakeup();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from main thread during event loop tick.
|
||||
/// This handles the case where the VM is idle (waiting on I/O).
|
||||
/// For active JS execution (including infinite loops), the StopTheWorld callback handles it.
|
||||
pub fn checkAndActivateInspector() void {
|
||||
if (!inspector_activation_requested.swap(false, .acq_rel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
defer jsc.VMManager.requestResumeAll(.JSDebugger);
|
||||
_ = tryActivateInspector();
|
||||
}
|
||||
|
||||
/// Tries to activate the inspector. Returns true if activated, false otherwise.
|
||||
/// Caller must have already consumed the activation request flag.
|
||||
fn tryActivateInspector() bool {
|
||||
const vm = VirtualMachine.get();
|
||||
|
||||
if (vm.is_shutting_down) {
|
||||
log("VM is shutting down, ignoring inspector activation request", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vm.debugger != null) {
|
||||
log("Debugger already active, ignoring activation request", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
activateInspector(vm) catch |err| {
|
||||
Output.prettyErrorln("Failed to activate inspector: {s}\n", .{@errorName(err)});
|
||||
Output.flush();
|
||||
return false;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 {
|
||||
var semaphore: ?Semaphore = null;
|
||||
var thread: ?std.Thread = null;
|
||||
var shutting_down: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
|
||||
|
||||
fn signalHandler(_: c_int) callconv(.c) void {
|
||||
// Signal handlers can only call async-signal-safe functions.
|
||||
// Semaphore.post() is async-signal-safe (uses Mach semaphores on macOS,
|
||||
// POSIX semaphores on Linux).
|
||||
if (semaphore) |sem| _ = sem.post();
|
||||
}
|
||||
|
||||
/// Dedicated thread that waits on the semaphore.
|
||||
/// When woken, it calls requestInspectorActivation() in normal thread context.
|
||||
fn signalInspectorThread() void {
|
||||
Output.Source.configureNamedThread("SignalInspector");
|
||||
|
||||
while (true) {
|
||||
_ = semaphore.?.wait();
|
||||
if (shutting_down.load(.acquire)) {
|
||||
log("SignalInspector thread exiting", .{});
|
||||
return;
|
||||
}
|
||||
log("SignalInspector thread woke, activating inspector", .{});
|
||||
requestInspectorActivation();
|
||||
}
|
||||
}
|
||||
|
||||
fn install() bool {
|
||||
semaphore = Semaphore.init() orelse {
|
||||
log("semaphore init failed", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
// Spawn the SignalInspector thread
|
||||
thread = std.Thread.spawn(.{
|
||||
.stack_size = 512 * 1024,
|
||||
}, signalInspectorThread, .{}) catch |err| {
|
||||
log("thread spawn failed: {s}", .{@errorName(err)});
|
||||
return false;
|
||||
};
|
||||
|
||||
// Install SIGUSR1 handler
|
||||
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 {
|
||||
// Signal the thread to exit. We don't join because:
|
||||
// 1. This is called from JS context (process.on('SIGUSR1', ...))
|
||||
// 2. Blocking the JS thread is bad
|
||||
// 3. The thread will exit on its own after checking shutting_down
|
||||
// The thread and semaphore are "leaked" and anyway this happens once
|
||||
// per process lifetime when user installs their own SIGUSR1 handler
|
||||
shutting_down.store(true, .release);
|
||||
if (semaphore) |sem| _ = sem.post();
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
/// Called from C++ StopTheWorld callback.
|
||||
/// Returns true if inspector was activated, false if already active or not requested.
|
||||
export fn Bun__activateInspector() bool {
|
||||
if (!inspector_activation_requested.swap(false, .acq_rel)) {
|
||||
return false;
|
||||
}
|
||||
return tryActivateInspector();
|
||||
}
|
||||
|
||||
comptime {
|
||||
if (Environment.isPosix) {
|
||||
_ = Bun__Sigusr1Handler__uninstall;
|
||||
}
|
||||
_ = Bun__activateInspector;
|
||||
}
|
||||
|
||||
const Semaphore = @import("../../sync/Semaphore.zig");
|
||||
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;
|
||||
@@ -77,6 +77,7 @@ pub const SystemError = @import("./bindings/SystemError.zig").SystemError;
|
||||
pub const URL = @import("./bindings/URL.zig").URL;
|
||||
pub const URLSearchParams = @import("./bindings/URLSearchParams.zig").URLSearchParams;
|
||||
pub const VM = @import("./bindings/VM.zig").VM;
|
||||
pub const VMManager = @import("./bindings/VMManager.zig");
|
||||
pub const Weak = @import("./Weak.zig").Weak;
|
||||
pub const WeakRefType = @import("./Weak.zig").WeakRefType;
|
||||
pub const Exception = @import("./bindings/Exception.zig").Exception;
|
||||
|
||||
@@ -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,
|
||||
@@ -773,6 +774,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;
|
||||
|
||||
@@ -755,13 +755,12 @@ function emitConvertEnumFunction(w: CodeWriter, type: TypeImpl) {
|
||||
w.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String& stringValue)`);
|
||||
w.line(`{`);
|
||||
w.line(
|
||||
` static constexpr std::array<std::pair<ComparableASCIILiteral, ${name}>, ${type.data.length}> mappings { {`,
|
||||
` static constexpr SortedArrayMap enumerationMapping { std::to_array<std::pair<ComparableASCIILiteral, ${name}>>({`,
|
||||
);
|
||||
for (const value of type.data) {
|
||||
w.line(` { ${str(value)}_s, ${name}::${pascal(value)} },`);
|
||||
}
|
||||
w.line(` } };`);
|
||||
w.line(` static constexpr SortedArrayMap enumerationMapping { mappings };`);
|
||||
w.line(` }) };`);
|
||||
w.line(` if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]`);
|
||||
w.line(` return *enumerationValue;`);
|
||||
w.line(` return std::nullopt;`);
|
||||
|
||||
@@ -143,7 +143,7 @@ export function enumeration(
|
||||
template<> std::optional<${qualifiedName}>
|
||||
WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal)
|
||||
{
|
||||
static constexpr ::std::array<${pairType}, ${valueMap.size}> mappings {
|
||||
static constexpr ::WTF::SortedArrayMap enumerationMapping { ::std::to_array<${pairType}>({
|
||||
${joinIndented(
|
||||
12,
|
||||
Array.from(valueMap.entries())
|
||||
@@ -155,8 +155,7 @@ export function enumeration(
|
||||
},`;
|
||||
}),
|
||||
)}
|
||||
};
|
||||
static constexpr ::WTF::SortedArrayMap enumerationMapping { mappings };
|
||||
}) };
|
||||
if (auto* enumerationValue = enumerationMapping.tryGet(stringVal)) [[likely]] {
|
||||
return *enumerationValue;
|
||||
}
|
||||
|
||||
@@ -325,7 +325,9 @@ $$capture_start$$(${fn.async ? "async " : ""}${
|
||||
directives: fn.directives,
|
||||
source: finalReplacement,
|
||||
params: fn.params,
|
||||
visibility: fn.directives.visibility ?? (fn.directives.linkTimeConstant ? "Private" : "Public"),
|
||||
// Async functions automatically get Private visibility because the parser
|
||||
// upgrades them when they use await (see Parser.cpp parseFunctionBody)
|
||||
visibility: fn.directives.visibility ?? (fn.directives.linkTimeConstant || fn.async ? "Private" : "Public"),
|
||||
isGetter: !!fn.directives.getter,
|
||||
constructAbility: fn.directives.ConstructAbility ?? "CannotConstruct",
|
||||
constructKind: fn.directives.ConstructKind ?? "None",
|
||||
|
||||
@@ -2224,11 +2224,12 @@ export function readableStreamIntoArray(stream) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Use $promiseResolveWithThen to convert InternalPromise to regular Promise
|
||||
if (manyResult && $isPromise(manyResult)) {
|
||||
return manyResult.$then(processManyResult);
|
||||
return $promiseResolveWithThen(Promise, manyResult.$then(processManyResult));
|
||||
}
|
||||
|
||||
return processManyResult(manyResult);
|
||||
return $promiseResolveWithThen(Promise, processManyResult(manyResult));
|
||||
}
|
||||
|
||||
export function withoutUTF8BOM(result) {
|
||||
@@ -2245,10 +2246,13 @@ export function readableStreamIntoText(stream: ReadableStream) {
|
||||
const prom = $readStreamIntoSink(stream, textStream, false);
|
||||
|
||||
if (prom && $isPromise(prom)) {
|
||||
return Promise.$resolve(prom).$then(closer.promise).$then($withoutUTF8BOM);
|
||||
// Use $promiseResolveWithThen to convert InternalPromise to regular Promise
|
||||
// since $readStreamIntoSink is an async builtin function that returns InternalPromise
|
||||
return $promiseResolveWithThen(Promise, Promise.$resolve(prom).$then(closer.promise).$then($withoutUTF8BOM));
|
||||
}
|
||||
|
||||
return closer.promise.$then($withoutUTF8BOM);
|
||||
// Also wrap the non-promise path since closer.promise.$then() returns InternalPromise in builtin context
|
||||
return $promiseResolveWithThen(Promise, closer.promise.$then($withoutUTF8BOM));
|
||||
}
|
||||
|
||||
export function readableStreamToArrayBufferDirect(
|
||||
|
||||
39
src/sync/Semaphore.zig
Normal file
39
src/sync/Semaphore.zig
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Async-signal-safe semaphore.
|
||||
//!
|
||||
//! This is a thin wrapper around the C++ Bun::Semaphore class, which uses:
|
||||
//! - macOS: Mach semaphores (semaphore_signal is async-signal-safe)
|
||||
//! - Linux: POSIX semaphores (sem_post is async-signal-safe)
|
||||
//! - Windows: libuv semaphores
|
||||
//!
|
||||
//! Unlike std.Thread.Semaphore (which uses Mutex + Condition), this
|
||||
//! implementation's post/signal operation is safe to call from signal handlers.
|
||||
|
||||
const Semaphore = @This();
|
||||
|
||||
#ptr: *anyopaque,
|
||||
|
||||
pub fn init() ?Semaphore {
|
||||
const ptr = Bun__Semaphore__create(0) orelse return null;
|
||||
return .{ .#ptr = ptr };
|
||||
}
|
||||
|
||||
pub fn deinit(self: Semaphore) void {
|
||||
Bun__Semaphore__destroy(self.#ptr);
|
||||
}
|
||||
|
||||
/// Signal the semaphore, waking one waiting thread.
|
||||
/// This is async-signal-safe and can be called from signal handlers.
|
||||
pub fn post(self: Semaphore) bool {
|
||||
return Bun__Semaphore__signal(self.#ptr);
|
||||
}
|
||||
|
||||
/// Wait for the semaphore to be signaled.
|
||||
/// Blocks until another thread calls post().
|
||||
pub fn wait(self: Semaphore) bool {
|
||||
return Bun__Semaphore__wait(self.#ptr);
|
||||
}
|
||||
|
||||
extern fn Bun__Semaphore__create(value: c_uint) ?*anyopaque;
|
||||
extern fn Bun__Semaphore__destroy(sem: *anyopaque) void;
|
||||
extern fn Bun__Semaphore__signal(sem: *anyopaque) bool;
|
||||
extern fn Bun__Semaphore__wait(sem: *anyopaque) bool;
|
||||
@@ -49,3 +49,26 @@ bool Semaphore::wait()
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
extern "C" {
|
||||
|
||||
Bun::Semaphore* Bun__Semaphore__create(unsigned int value)
|
||||
{
|
||||
return new Bun::Semaphore(value);
|
||||
}
|
||||
|
||||
void Bun__Semaphore__destroy(Bun::Semaphore* sem)
|
||||
{
|
||||
delete sem;
|
||||
}
|
||||
|
||||
bool Bun__Semaphore__signal(Bun::Semaphore* sem)
|
||||
{
|
||||
return sem->signal();
|
||||
}
|
||||
|
||||
bool Bun__Semaphore__wait(Bun::Semaphore* sem)
|
||||
{
|
||||
return sem->wait();
|
||||
}
|
||||
}
|
||||
|
||||
425
test/js/bun/runtime-inspector/runtime-inspector-posix.test.ts
Normal file
425
test/js/bun/runtime-inspector/runtime-inspector-posix.test.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
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).toMatch(/ws:\/\/localhost:\d+\//);
|
||||
});
|
||||
|
||||
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("multiple SIGUSR1s work after user installs handler", async () => {
|
||||
// After user installs their own SIGUSR1 handler, multiple signals should all
|
||||
// be delivered to the user handler correctly.
|
||||
using dir = tempDir("sigusr1-uninstall-test", {
|
||||
"test.js": `
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
let count = 0;
|
||||
process.on("SIGUSR1", () => {
|
||||
count++;
|
||||
console.log("SIGNAL_" + count);
|
||||
if (count >= 3) {
|
||||
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);
|
||||
|
||||
// Send SIGUSR1s and wait for each handler to respond before sending the next
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
process.kill(pid, "SIGUSR1");
|
||||
// Wait for handler output before sending next signal
|
||||
while (!output.includes(`SIGNAL_${i}`)) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
output += decoder.decode(value, { stream: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Read remaining output until process exits
|
||||
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).toMatchInlineSnapshot(`
|
||||
"READY
|
||||
SIGNAL_1
|
||||
SIGNAL_2
|
||||
SIGNAL_3
|
||||
"
|
||||
`);
|
||||
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",
|
||||
});
|
||||
|
||||
const reader = proc.stderr.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
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",
|
||||
});
|
||||
|
||||
const reader = proc.stderr.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let stderr = "";
|
||||
|
||||
while ((stderr.match(/Bun Inspector/g) || []).length < 2) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
429
test/js/bun/runtime-inspector/runtime-inspector.test.ts
Normal file
429
test/js/bun/runtime-inspector/runtime-inspector.test.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
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).toMatch(/ws:\/\/localhost:\d+\//);
|
||||
});
|
||||
|
||||
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(stderr).toContain("Failed");
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
test("inspector does not activate twice", { timeout: 30_000 }, 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
|
||||
// Use longer timeout for ASAN builds which can be slower
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
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", { timeout: 30_000 }, 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
|
||||
// Use longer timeout for ASAN builds which can be slower
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
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(stderr).toContain("requires a pid argument");
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
test("can interrupt an infinite loop", async () => {
|
||||
using dir = tempDir("debug-infinite-loop-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));
|
||||
|
||||
// Infinite loop - the inspector should be able to interrupt this
|
||||
while (true) {}
|
||||
`,
|
||||
});
|
||||
|
||||
// Start target process with infinite loop
|
||||
await using targetProc = spawn({
|
||||
cmd: [bunExe(), "target.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Wait for PID file to be written
|
||||
const pidPath = join(String(dir), "pid");
|
||||
let pid: number | undefined;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
try {
|
||||
const pidText = await Bun.file(pidPath).text();
|
||||
pid = parseInt(pidText, 10);
|
||||
if (pid > 0) break;
|
||||
} catch {
|
||||
// File not ready yet
|
||||
}
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
expect(pid).toBeGreaterThan(0);
|
||||
|
||||
// Use _debugProcess to activate inspector - this should interrupt the infinite loop
|
||||
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 - this proves we interrupted the infinite loop
|
||||
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).toMatch(/ws:\/\/localhost:\d+\//);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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