Compare commits

...

21 Commits

Author SHA1 Message Date
190n
2f2b530133 bun run zig-format 2025-03-31 22:57:14 +00:00
Ben Grant
62aade4493 Correct ref counting in UDPSocket 2025-03-31 14:10:09 -07:00
Ben Grant
dfcbdcba23 Give each Worker its own autoSelectFamilyAttemptTimeoutDefault 2025-03-31 14:05:09 -07:00
Ben Grant
488fc97b5e Make BUN_DESTRUCT_VM_ON_EXIT work for bun test 2025-03-31 14:02:04 -07:00
Ben Grant
806b59c7fe Test napi_add_finalizer in main thread instead of worker 2025-03-27 16:02:04 -07:00
Ben Grant
6382ae9b61 Skip destructOnExit while JS is running 2025-03-27 16:01:27 -07:00
Ben Grant
ea40a1dab9 Add BUN_DESTRUCT_VM_ON_EXIT 2025-03-27 14:46:07 -07:00
Ben Grant
31e778efa4 Merge branch 'main' into ben/workers 2025-03-27 12:21:42 -07:00
Ben Grant
a0e41dc4f7 Add test for #18139 2025-03-27 12:01:26 -07:00
Ben Grant
92a73de564 Move napiEnvs to ScriptExecutionContext and give napi_env a strong reference to GlobalObject 2025-03-27 11:57:51 -07:00
Ben Grant
db77d8c9f6 Clarify sus code 2025-03-25 16:58:20 -07:00
Ben Grant
c882d75071 Merge branch 'main' into ben/workers 2025-03-25 14:36:39 -07:00
Ben Grant
eacc092e2e Undo noreturn 2025-03-25 14:29:33 -07:00
Ben Grant
3724c34353 Clean up worker options 2025-03-24 18:53:00 -07:00
Ben Grant
b0628fea99 Revert "Emit Worker construction errors as error events instead of throwing synchronously"
This reverts commit 1448e6ad91.
2025-03-24 14:37:14 -07:00
Ben Grant
1448e6ad91 Emit Worker construction errors as error events instead of throwing synchronously 2025-03-24 13:13:42 -07:00
Ben Grant
985bd5cc4e add TODO for [[noreturn]] issue 2025-03-21 16:05:35 -07:00
Ben Grant
5ea190b12f Merge branch 'main' into ben/workers 2025-03-21 12:45:48 -07:00
Ben Grant
c434e07b0a Sync declarations and definitions of Bun__Process__exit 2025-03-17 14:18:01 -07:00
Ben Grant
35e2a6cbfa Merge branch 'main' into ben/workers 2025-03-17 14:11:12 -07:00
Ben Grant
e222f8ceb3 Sync already-added tests with Node 2025-03-14 13:41:17 -07:00
29 changed files with 377 additions and 176 deletions

View File

@@ -25,24 +25,25 @@ extern fn inet_pton(af: c_int, src: [*c]const u8, dst: ?*anyopaque) c_int;
fn onClose(socket: *uws.udp.Socket) callconv(.C) void {
JSC.markBinding(@src());
const this: *UDPSocket = bun.cast(*UDPSocket, socket.user().?);
const this: *UDPSocket = @alignCast(@ptrCast(socket.user().?));
this.closed = true;
this.poll_ref.disable();
_ = this.js_refcount.fetchSub(1, .monotonic);
// Free the reference held by UWS
this.deref();
}
fn onDrain(socket: *uws.udp.Socket) callconv(.C) void {
JSC.markBinding(@src());
const this: *UDPSocket = bun.cast(*UDPSocket, socket.user().?);
const callback = this.config.on_drain;
if (callback == .zero) return;
const this: *UDPSocket = @alignCast(@ptrCast(socket.user().?));
const thisValue = this.strong_this.get().?;
const callback = UDPSocket.onDrainGetCached(thisValue) orelse return;
const vm = JSC.VirtualMachine.get();
const event_loop = vm.eventLoop();
event_loop.enter();
defer event_loop.exit();
_ = callback.call(this.globalThis, this.thisValue, &.{this.thisValue}) catch |err| {
_ = callback.call(this.globalThis, thisValue, &.{thisValue}) catch |err| {
_ = this.callErrorHandler(.zero, &.{this.globalThis.takeException(err)});
};
}
@@ -50,9 +51,9 @@ fn onDrain(socket: *uws.udp.Socket) callconv(.C) void {
fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) callconv(.C) void {
JSC.markBinding(@src());
const udpSocket: *UDPSocket = bun.cast(*UDPSocket, socket.user().?);
const callback = udpSocket.config.on_data;
if (callback == .zero) return;
const udpSocket: *UDPSocket = @alignCast(@ptrCast(socket.user().?));
const thisValue = udpSocket.strong_this.get().?;
const callback = UDPSocket.onDataGetCached(thisValue) orelse return;
const globalThis = udpSocket.globalThis;
@@ -90,8 +91,8 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c
const loop = udpSocket.vm.eventLoop();
loop.enter();
defer loop.exit();
_ = udpSocket.js_refcount.fetchAdd(1, .monotonic);
defer _ = udpSocket.js_refcount.fetchSub(1, .monotonic);
udpSocket.ref();
defer udpSocket.deref();
const span = std.mem.span(hostname.?);
var hostname_string = if (scope_id) |id| blk: {
@@ -105,8 +106,8 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c
break :blk bun.String.createFormat("{s}%{d}", .{ span, id }) catch bun.outOfMemory();
} else bun.String.init(span);
_ = callback.call(globalThis, udpSocket.thisValue, &.{
udpSocket.thisValue,
_ = callback.call(globalThis, thisValue, &.{
thisValue,
udpSocket.config.binary_type.toJS(slice, globalThis),
JSC.jsNumber(port),
hostname_string.transferToJS(globalThis),
@@ -128,17 +129,19 @@ pub const UDPSocketConfig = struct {
port: u16,
address: [:0]u8,
};
const Callbacks = struct {
on_data: JSValue = .zero,
on_drain: JSValue = .zero,
on_error: JSValue = .zero,
};
hostname: [:0]u8,
connect: ?ConnectConfig = null,
port: u16,
flags: i32,
binary_type: JSC.BinaryType = .Buffer,
on_data: JSValue = .zero,
on_drain: JSValue = .zero,
on_error: JSValue = .zero,
pub fn fromJS(globalThis: *JSGlobalObject, options: JSValue) bun.JSError!This {
pub fn fromJS(globalThis: *JSGlobalObject, options: JSValue) bun.JSError!struct { This, Callbacks } {
if (options.isEmptyOrUndefinedOrNull() or !options.isObject()) {
return globalThis.throwInvalidArguments("Expected an object", .{});
}
@@ -174,11 +177,12 @@ pub const UDPSocketConfig = struct {
else
0;
var config = This{
var config: This = .{
.hostname = hostname,
.port = port,
.flags = flags,
};
var callbacks: Callbacks = .{};
if (try options.getTruthy(globalThis, "socket")) |socket| {
if (!socket.isObject()) {
@@ -196,11 +200,12 @@ pub const UDPSocketConfig = struct {
}
inline for (handlers) |handler| {
if (try socket.getTruthyComptime(globalThis, handler.@"0")) |value| {
const js_name, const zig_name = handler;
if (try socket.getTruthyComptime(globalThis, js_name)) |value| {
if (!value.isCell() or !value.isCallable()) {
return globalThis.throwInvalidArguments("Expected \"socket.{s}\" to be a function", .{handler.@"0"});
}
@field(config, handler.@"1") = value;
@field(callbacks, zig_name) = value;
}
}
}
@@ -241,25 +246,10 @@ pub const UDPSocketConfig = struct {
};
}
config.protect();
return config;
}
pub fn protect(this: This) void {
inline for (handlers) |handler| {
@field(this, handler.@"1").protect();
}
}
pub fn unprotect(this: This) void {
inline for (handlers) |handler| {
@field(this, handler.@"1").unprotect();
}
return .{ config, callbacks };
}
pub fn deinit(this: This) void {
this.unprotect();
default_allocator.free(this.hostname);
if (this.connect) |val| {
default_allocator.free(val.address);
@@ -276,15 +266,15 @@ pub const UDPSocket = struct {
loop: *uws.Loop,
globalThis: *JSGlobalObject,
thisValue: JSValue = .zero,
strong_this: JSC.Strong = .empty,
jsc_ref: JSC.Ref = JSC.Ref.init(),
poll_ref: Async.KeepAlive = Async.KeepAlive.init(),
jsc_ref: JSC.Ref = .init(),
poll_ref: Async.KeepAlive = .init(),
// if marked as closed the socket pointer may be stale
closed: bool = false,
connect_info: ?ConnectInfo = null,
vm: *JSC.VirtualMachine,
js_refcount: std.atomic.Value(usize) = std.atomic.Value(usize).init(1),
ref_count: std.atomic.Value(usize) = .init(1),
const ConnectInfo = struct {
port: u16,
@@ -293,15 +283,15 @@ pub const UDPSocket = struct {
pub usingnamespace JSC.Codegen.JSUDPSocket;
pub fn hasPendingActivity(this: *This) callconv(.C) bool {
return this.js_refcount.load(.monotonic) > 0;
return this.ref_count.load(.seq_cst) > 0;
}
pub usingnamespace bun.New(@This());
pub usingnamespace bun.NewThreadSafeRefCounted(@This(), deinit, "UDPSocket");
pub fn udpSocket(globalThis: *JSGlobalObject, options: JSValue) bun.JSError!JSValue {
log("udpSocket", .{});
const config = try UDPSocketConfig.fromJS(globalThis, options);
const config, const callbacks = try UDPSocketConfig.fromJS(globalThis, options);
const vm = globalThis.bunVM();
var this = This.new(.{
@@ -311,6 +301,12 @@ pub const UDPSocket = struct {
.loop = uws.Loop.get(),
.vm = vm,
});
const thisValue = this.toJS(globalThis);
defer thisValue.ensureStillAlive();
this.strong_this.set(globalThis, thisValue);
UDPSocket.onDataSetCached(thisValue, globalThis, callbacks.on_data);
UDPSocket.onDrainSetCached(thisValue, globalThis, callbacks.on_drain);
UDPSocket.onErrorSetCached(thisValue, globalThis, callbacks.on_error);
var err: i32 = 0;
@@ -326,9 +322,10 @@ pub const UDPSocket = struct {
this,
)) |socket| {
this.socket = socket;
// second ref held by UWS
this.ref();
} else {
this.closed = true;
defer this.deinit();
if (err != 0) {
const code = @tagName(bun.C.SystemErrno.init(@as(c_int, @intCast(err))).?);
const sys_err = JSC.SystemError{
@@ -345,7 +342,7 @@ pub const UDPSocket = struct {
errdefer {
this.socket.close();
this.deinit();
// second deref will be called by JS finalizer
}
if (config.connect) |connect| {
@@ -363,9 +360,6 @@ pub const UDPSocket = struct {
}
this.poll_ref.ref(vm);
const thisValue = this.toJS(globalThis);
thisValue.ensureStillAlive();
this.thisValue = thisValue;
return JSC.JSPromise.resolvedPromiseValue(globalThis, thisValue);
}
@@ -374,19 +368,18 @@ pub const UDPSocket = struct {
thisValue: JSValue,
err: []const JSValue,
) bool {
const callback = this.config.on_error;
const maybe_callback = UDPSocket.onErrorGetCached(thisValue);
const globalThis = this.globalThis;
const vm = globalThis.bunVM();
if (callback == .zero) {
if (maybe_callback) |callback| {
_ = callback.call(globalThis, thisValue, err) catch |e| globalThis.reportActiveExceptionAsUnhandled(e);
} else {
if (err.len > 0)
_ = vm.uncaughtException(globalThis, err[0], false);
return false;
}
_ = callback.call(globalThis, thisValue, err) catch |e| globalThis.reportActiveExceptionAsUnhandled(e);
return true;
}
@@ -795,7 +788,7 @@ pub const UDPSocket = struct {
address: JSValue,
};
pub fn ref(this: *This, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
pub fn jsRef(this: *This, globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
if (!this.closed) {
this.poll_ref.ref(globalThis.bunVM());
}
@@ -827,12 +820,12 @@ pub const UDPSocket = struct {
}
const options = args.ptr[0];
const config = try UDPSocketConfig.fromJS(globalThis, options);
config.protect();
var previous_config = this.config;
previous_config.unprotect();
const config, const callbacks = try UDPSocketConfig.fromJS(globalThis, options);
const thisValue = this.strong_this.get().?;
this.config = config;
UDPSocket.onDataSetCached(thisValue, globalThis, callbacks.on_data);
UDPSocket.onDrainSetCached(thisValue, globalThis, callbacks.on_drain);
UDPSocket.onErrorSetCached(thisValue, globalThis, callbacks.on_error);
return .undefined;
}
@@ -892,12 +885,17 @@ pub const UDPSocket = struct {
pub fn finalize(this: *This) void {
log("Finalize {*}", .{this});
this.deinit();
if (!this.closed) {
this.socket.close();
}
this.strong_this.deinit();
// If the socket is open, UWS holds the other reference and will deref when it's closed.
this.deref();
}
pub fn deinit(this: *This) void {
// finalize is only called when js_refcount reaches 0
// js_refcount can only reach 0 when the socket is closed
// finalize is only called when reference count reaches 0
// reference count can only reach 0 when the socket is closed
bun.assert(this.closed);
this.poll_ref.disable();
this.config.deinit();

View File

@@ -301,7 +301,7 @@ export default [
length: 1,
},
ref: {
fn: "ref",
fn: "jsRef",
length: 0,
},
unref: {
@@ -368,6 +368,7 @@ export default [
length: 3,
},
},
values: ["onData", "onDrain", "onError"],
klass: {},
}),
define({

View File

@@ -9,6 +9,7 @@
#include "EventLoopTask.h"
#include "BunBroadcastChannelRegistry.h"
#include <wtf/LazyRef.h>
#include "napi.h"
extern "C" void Bun__startLoop(us_loop_t* loop);
namespace WebCore {

View File

@@ -22,6 +22,7 @@ struct WebSocketContext;
struct us_socket_t;
struct us_socket_context_t;
struct us_loop_t;
struct napi_env__;
namespace WebCore {
@@ -157,6 +158,8 @@ public:
static ScriptExecutionContext* getMainThreadScriptExecutionContext();
Vector<std::unique_ptr<napi_env__>>& napiEnvs() { return m_napiEnvs; }
private:
JSC::VM* m_vm = nullptr;
JSC::JSGlobalObject* m_globalObject = nullptr;
@@ -181,6 +184,8 @@ private:
us_socket_context_t* m_connected_ssl_client_websockets_ctx = nullptr;
us_socket_context_t* m_connected_client_websockets_ctx = nullptr;
Vector<std::unique_ptr<napi_env__>> m_napiEnvs;
public:
template<bool isSSL, bool isServer>
us_socket_context_t* connectedWebSocketContext()

View File

@@ -986,26 +986,25 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(void* console_client,
const auto initializeWorker = [&](WebCore::Worker& worker) -> void {
auto& options = worker.options();
if (options.bun.env) {
auto map = WTFMove(options.bun.env);
auto size = map->size();
if (options.env.has_value()) {
HashMap<String, String> map = WTFMove(*std::exchange(options.env, std::nullopt));
auto size = map.size();
// In theory, a GC could happen before we finish putting all the properties on the object.
// So we use a MarkedArgumentBuffer to ensure that the strings are not collected and we immediately put them on the object.
MarkedArgumentBuffer strings;
strings.ensureCapacity(map->size());
for (const auto& value : map->values()) {
strings.ensureCapacity(size);
for (const auto& value : map.values()) {
strings.append(jsString(vm, value));
}
auto env = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), size >= JSFinalObject::maxInlineCapacity ? JSFinalObject::maxInlineCapacity : size);
size_t i = 0;
for (auto k : *map) {
for (auto k : map) {
// They can have environment variables with numbers as keys.
// So we must use putDirectMayBeIndex to handle that.
env->putDirectMayBeIndex(globalObject, JSC::Identifier::fromString(vm, WTFMove(k.key)), strings.at(i++));
}
map->clear();
globalObject->m_processEnvObject.set(vm, globalObject, env);
}
@@ -4643,8 +4642,9 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h
napi_env GlobalObject::makeNapiEnv(const napi_module& mod)
{
m_napiEnvs.append(std::make_unique<napi_env__>(this, mod));
return m_napiEnvs.last().get();
auto& envs = m_scriptExecutionContext->napiEnvs();
envs.append(std::make_unique<napi_env__>(this, mod));
return envs.last().get();
}
napi_env GlobalObject::makeNapiEnvForFFI()
@@ -4663,7 +4663,7 @@ napi_env GlobalObject::makeNapiEnvForFFI()
bool GlobalObject::hasNapiFinalizers() const
{
for (const auto& env : m_napiEnvs) {
for (const auto& env : m_scriptExecutionContext->napiEnvs()) {
if (env->hasFinalizers()) {
return true;
}
@@ -4672,6 +4672,21 @@ bool GlobalObject::hasNapiFinalizers() const
return false;
}
extern "C" void Zig__GlobalObject__destructOnExit(Zig::GlobalObject* globalObject)
{
if (JSC::getVM(globalObject).entryScope) {
// Exiting while running JavaScript code (e.g. `process.exit()`), so we can't destroy it
// just now. Perhaps later in this case we can defer destruction to run later.
return;
}
auto& vm = JSC::getVM(globalObject);
gcUnprotect(globalObject);
globalObject = nullptr;
vm.heap.collectNow(JSC::Sync, JSC::CollectionScope::Full);
vm.derefSuppressingSaferCPPChecking();
vm.derefSuppressingSaferCPPChecking();
}
#include "ZigGeneratedClasses+lazyStructureImpl.h"
#include "ZigGlobalObject.lut.h"

View File

@@ -636,7 +636,6 @@ public:
// De-optimization once `require("module").runMain` is written to
bool hasOverriddenModuleRunMain = false;
WTF::Vector<std::unique_ptr<napi_env__>> m_napiEnvs;
napi_env makeNapiEnv(const napi_module&);
napi_env makeNapiEnvForFFI();
bool hasNapiFinalizers() const;

View File

@@ -741,7 +741,7 @@ ZIG_DECL size_t Bun__WebSocketClientTLS__memoryCost(WebSocketClientTLS* arg0);
#ifdef __cplusplus
ZIG_DECL void Bun__Process__exit(JSC__JSGlobalObject* arg0, unsigned char arg1);
ZIG_DECL /*[[noreturn]]*/ void Bun__Process__exit(JSC__JSGlobalObject* arg0, uint8_t arg1); // TODO(@190n) figure out why with a real [[noreturn]] annotation this trips ASan before calling the function
ZIG_DECL JSC__JSValue Bun__Process__getArgv(JSC__JSGlobalObject* arg0);
ZIG_DECL JSC__JSValue Bun__Process__getArgv0(JSC__JSGlobalObject* arg0);
ZIG_DECL JSC__JSValue Bun__Process__getCwd(JSC__JSGlobalObject* arg0);

View File

@@ -64,7 +64,7 @@ struct napi_async_cleanup_hook_handle__ {
struct napi_env__ {
public:
napi_env__(Zig::GlobalObject* globalObject, const napi_module& napiModule)
: m_globalObject(globalObject)
: m_globalObject(JSC::getVM(globalObject), globalObject)
, m_napiModule(napiModule)
{
napi_internal_register_cleanup_zig(this);
@@ -94,7 +94,7 @@ public:
m_isFinishingFinalizers = true;
for (const BoundFinalizer& boundFinalizer : m_finalizers) {
Bun::NapiHandleScope handle_scope(m_globalObject);
Bun::NapiHandleScope handle_scope(globalObject());
boundFinalizer.call(this);
}
m_finalizers.clear();
@@ -174,7 +174,7 @@ public:
bool inGC() const
{
JSC::VM& vm = JSC::getVM(m_globalObject);
JSC::VM& vm = JSC::getVM(globalObject());
return vm.isCollectorBusyOnCurrentThread();
}
@@ -189,7 +189,7 @@ public:
bool isVMTerminating() const
{
return JSC::getVM(m_globalObject).hasTerminationRequest();
return JSC::getVM(globalObject()).hasTerminationRequest();
}
void doFinalizer(napi_finalize finalize_cb, void* data, void* finalize_hint)
@@ -197,6 +197,10 @@ public:
if (!finalize_cb) {
return;
}
if (!globalObject()) {
NAPI_LOG("not running finalizer as global object is destroyed");
return;
}
if (mustDeferFinalizers() && inGC()) {
napi_internal_enqueue_finalizer(this, finalize_cb, data, finalize_hint);
@@ -205,7 +209,7 @@ public:
}
}
inline Zig::GlobalObject* globalObject() const { return m_globalObject; }
inline Zig::GlobalObject* globalObject() const { return m_globalObject.get(); }
inline const napi_module& napiModule() const { return m_napiModule; }
// Returns true if finalizers from this module need to be scheduled for the next tick after garbage collection, instead of running during garbage collection
@@ -291,7 +295,7 @@ public:
};
private:
Zig::GlobalObject* m_globalObject = nullptr;
JSC::Strong<Zig::GlobalObject> m_globalObject;
napi_module m_napiModule;
// TODO(@heimskr): Use WTF::HashSet
std::unordered_set<BoundFinalizer, BoundFinalizer::Hash> m_finalizers;

View File

@@ -0,0 +1,32 @@
#include "root.h"
#include "BunClientData.h"
#include "JSDOMWrapper.h"
const WTF::RefCountedBase* Bun__refToInspect = nullptr;
extern "C" void Bun__inspectRef()
{
fprintf(stderr, "\x1b[1;34mref %p %u -> %u\x1b[0m\n", Bun__refToInspect, Bun__refToInspect->refCount(), Bun__refToInspect->refCount() + 1);
if (Bun__refToInspect->refCount() == 2) {
fprintf(stderr, "breakpoint\n");
}
WTF::StackTrace::captureStackTrace(30, 2)->dump(WTF::dataFile());
}
extern "C" void Bun__inspectDeref()
{
fprintf(stderr, "\x1b[1;34mderef %p %u -> %u\x1b[0m\n", Bun__refToInspect, Bun__refToInspect->refCount(), Bun__refToInspect->refCount() - 1);
if (Bun__refToInspect->refCount() == 3) {
fprintf(stderr, "breakpoint\n");
}
WTF::StackTrace::captureStackTrace(30, 2)->dump(WTF::dataFile());
}
extern "C" void Bun__testVMOnExit(JSC::VM* vm)
{
// auto clientData = WebCore::clientData(*vm);
// WTF::RefCountedBase* base = &clientData->normalWorld();
// fprintf(stderr, "vm in refToInspect: %p\n", &static_cast<const WebCore::DOMWrapperWorld*>(Bun__refToInspect)->vm());
// fprintf(stderr, "vm in testVMOnExit: %p\n", vm);
// fprintf(stderr, "normalWorld %p %p refcount for vm %p = %u %u\n", Bun__refToInspect, base, vm, clientData->normalWorld().refCount(), Bun__refToInspect->refCount());
}

View File

@@ -41,6 +41,7 @@
#include "JSDOMOperation.h"
#include "JSDOMWrapperCache.h"
#include "JSEventListener.h"
#include "NodeValidator.h"
#include "StructuredSerializeOptions.h"
#include "JSWorkerOptions.h"
#include "ScriptExecutionContext.h"
@@ -128,7 +129,7 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
EnsureStillAliveScope argument1 = callFrame->argument(1);
auto options = WorkerOptions {};
options.bun.unref = false;
options.unref = false;
if (JSObject* optionsObject = JSC::jsDynamicCast<JSC::JSObject*>(argument1.value())) {
if (auto nameValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->name)) {
@@ -139,12 +140,14 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
}
if (auto miniModeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s))) {
options.bun.mini = miniModeValue.toBoolean(lexicalGlobalObject);
options.mini = miniModeValue.toBoolean(lexicalGlobalObject);
}
RETURN_IF_EXCEPTION(throwScope, {});
if (auto ref = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "ref"_s))) {
options.bun.unref = !ref.toBoolean(lexicalGlobalObject);
options.unref = !ref.toBoolean(lexicalGlobalObject);
}
RETURN_IF_EXCEPTION(throwScope, {});
if (auto preloadModulesValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "preload"_s))) {
if (!preloadModulesValue.isUndefinedOrNull()) {
@@ -152,14 +155,14 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
auto str = preloadModulesValue.toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (!str.isEmpty()) {
options.bun.preloadModules.append(str);
options.preloadModules.append(str);
}
} else if (auto* array = jsDynamicCast<JSC::JSArray*>(preloadModulesValue)) {
std::optional<Vector<String>> seq = convert<IDLSequence<IDLDOMString>>(*lexicalGlobalObject, array);
RETURN_IF_EXCEPTION(throwScope, {});
if (seq) {
options.bun.preloadModules = WTFMove(*seq);
options.bun.preloadModules.removeAllMatching([](const String& str) {
options.preloadModules = WTFMove(*seq);
options.preloadModules.removeAllMatching([](const String& str) {
return str.isEmpty();
});
}
@@ -211,8 +214,8 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
transferredPorts = disentangleResult.releaseReturnValue();
}
options.bun.data = serialized.releaseReturnValue();
options.bun.dataMessagePorts = WTFMove(transferredPorts);
options.data = serialized.releaseReturnValue();
options.dataMessagePorts = WTFMove(transferredPorts);
}
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
@@ -246,35 +249,35 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor::
env.add(key.impl()->isolatedCopy(), str);
}
options.bun.env = std::make_unique<HashMap<String, String>>(WTFMove(env));
options.env.emplace(WTFMove(env));
}
JSValue argvValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "argv"_s));
RETURN_IF_EXCEPTION(throwScope, {});
if (argvValue && argvValue.isCell() && argvValue.asCell()->type() == JSC::JSType::ArrayType) {
Vector<String> argv;
forEachInIterable(lexicalGlobalObject, argvValue, [&argv](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) {
if (argvValue && argvValue.pureToBoolean() != TriState::False) {
Bun::V::validateArray(throwScope, globalObject, argvValue, "options.argv"_s, jsNumber(0));
RETURN_IF_EXCEPTION(throwScope, {});
forEachInIterable(lexicalGlobalObject, argvValue, [&options](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) {
auto scope = DECLARE_THROW_SCOPE(vm);
String str = nextValue.toWTFString(lexicalGlobalObject).isolatedCopy();
if (UNLIKELY(scope.exception()))
return;
argv.append(str);
RETURN_IF_EXCEPTION(scope, );
options.argv.append(str);
});
options.bun.argv = std::make_unique<Vector<String>>(WTFMove(argv));
}
JSValue execArgvValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "execArgv"_s));
RETURN_IF_EXCEPTION(throwScope, {});
if (execArgvValue && execArgvValue.isCell() && execArgvValue.asCell()->type() == JSC::JSType::ArrayType) {
if (execArgvValue && execArgvValue.pureToBoolean() != TriState::False) {
Vector<String> execArgv;
Bun::V::validateArray(throwScope, globalObject, execArgvValue, "options.execArgv"_s, jsNumber(0));
RETURN_IF_EXCEPTION(throwScope, {});
forEachInIterable(lexicalGlobalObject, execArgvValue, [&execArgv](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) {
auto scope = DECLARE_THROW_SCOPE(vm);
String str = nextValue.toWTFString(lexicalGlobalObject).isolatedCopy();
if (UNLIKELY(scope.exception()))
return;
RETURN_IF_EXCEPTION(scope, );
execArgv.append(str);
});
options.bun.execArgv = std::make_unique<Vector<String>>(WTFMove(execArgv));
options.execArgv.emplace(WTFMove(execArgv));
}
}

View File

@@ -118,12 +118,13 @@ extern "C" void* WebWorker__create(
uint32_t contextId,
bool miniMode,
bool unrefByDefault,
StringImpl* argvPtr,
uint32_t argvLen,
StringImpl* execArgvPtr,
uint32_t execArgvLen,
StringImpl** argvPtr,
size_t argvLen,
bool defaultExecArgv,
StringImpl** execArgvPtr,
size_t execArgvLen,
BunString* preloadModulesPtr,
uint32_t preloadModulesLen);
size_t preloadModulesLen);
extern "C" void WebWorker__setRef(
void* worker,
bool ref);
@@ -161,26 +162,34 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
BunString errorMessage = BunStringEmpty;
BunString nameStr = Bun::toString(worker->m_options.name);
bool miniMode = worker->m_options.bun.mini;
bool unrefByDefault = worker->m_options.bun.unref;
bool miniMode = worker->m_options.mini;
bool unrefByDefault = worker->m_options.unref;
Vector<String>* argv = worker->m_options.bun.argv.get();
Vector<String>* execArgv = worker->m_options.bun.execArgv.get();
Vector<String>* preloadModuleStrings = &worker->m_options.bun.preloadModules;
auto& preloadModuleStrings = worker->m_options.preloadModules;
Vector<BunString> preloadModules;
preloadModules.reserveInitialCapacity(preloadModuleStrings->size());
for (auto& str : *preloadModuleStrings) {
preloadModules.reserveInitialCapacity(preloadModuleStrings.size());
for (auto& str : preloadModuleStrings) {
if (str.startsWith("file://"_s)) {
WTF::URL urlObject = WTF::URL(str);
if (!urlObject.isValid()) {
return Exception { TypeError, makeString("Invalid file URL: \""_s, str, '"') };
}
// We need to replace the string inside preloadModuleStrings (this line replaces because
// we are iterating by-ref). Otherwise, the string returned by fileSystemPath() will be
// freed in this block, before it is used by Zig code.
str = urlObject.fileSystemPath();
}
preloadModules.append(Bun::toString(str));
}
// try to ensure the cast from String* to StringImpl** is sane
static_assert(sizeof(WTF::String) == sizeof(WTF::StringImpl*));
std::span<WTF::StringImpl*> execArgv = worker->m_options.execArgv
.transform([](Vector<String>& vec) -> std::span<WTF::StringImpl*> {
return { reinterpret_cast<WTF::StringImpl**>(vec.data()), vec.size() };
})
.value_or(std::span<WTF::StringImpl*> {});
void* impl = WebWorker__create(
worker.ptr(),
jsCast<Zig::GlobalObject*>(context.jsGlobalObject())->bunVM(),
@@ -191,16 +200,17 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
static_cast<uint32_t>(worker->m_clientIdentifier),
miniMode,
unrefByDefault,
argv ? reinterpret_cast<StringImpl*>(argv->data()) : nullptr,
argv ? static_cast<uint32_t>(argv->size()) : 0,
execArgv ? reinterpret_cast<StringImpl*>(execArgv->data()) : nullptr,
execArgv ? static_cast<uint32_t>(execArgv->size()) : 0,
preloadModules.size() ? preloadModules.data() : nullptr,
static_cast<uint32_t>(preloadModules.size()));
reinterpret_cast<WTF::StringImpl**>(worker->m_options.argv.data()),
worker->m_options.argv.size(),
!worker->m_options.execArgv.has_value(),
execArgv.data(),
execArgv.size(),
preloadModules.data(),
preloadModules.size());
// now referenced by Zig
worker->ref();
preloadModuleStrings->clear();
preloadModuleStrings.clear();
if (!impl) {
return Exception { TypeError, errorMessage.toWTFString(BunString::ZeroCopy) };
@@ -498,9 +508,9 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject)
if (auto* worker = WebWorker__getParentWorker(globalObject->bunVM())) {
auto& options = worker->options();
if (worker && options.bun.data) {
auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.bun.dataMessagePorts));
RefPtr<WebCore::SerializedScriptValue> serialized = WTFMove(options.bun.data);
if (worker && options.data) {
auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.dataMessagePorts));
RefPtr<WebCore::SerializedScriptValue> serialized = WTFMove(options.data);
JSValue deserialized = serialized->deserialize(*globalObject, globalObject, WTFMove(ports));
RETURN_IF_EXCEPTION(scope, {});
workerData = deserialized;

View File

@@ -7,23 +7,16 @@
namespace WebCore {
struct BunOptions {
struct WorkerOptions {
String name;
bool mini { false };
bool unref { false };
RefPtr<SerializedScriptValue> data;
Vector<TransferredMessagePort> dataMessagePorts;
Vector<String> preloadModules;
std::unique_ptr<HashMap<String, String>> env { nullptr };
std::unique_ptr<Vector<String>> argv { nullptr };
std::unique_ptr<Vector<String>> execArgv { nullptr };
};
struct WorkerOptions {
// WorkerType type { WorkerType::Classic };
// FetchRequestCredentials credentials { FetchRequestCredentials::SameOrigin };
String name;
BunOptions bun {};
std::optional<HashMap<String, String>> env; // TODO(@190n) allow shared
Vector<String> argv;
std::optional<Vector<String>> execArgv;
};
} // namespace WebCore

View File

@@ -909,6 +909,12 @@ pub const VirtualMachine = struct {
// if one disconnect event listener should be ignored
channel_ref_should_ignore_one_disconnect_event_listener: bool = false,
/// Whether this VM should be destroyed after it exits, even if it is the main thread's VM.
/// 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. Controlled by
/// Options.destruct_main_thread_on_exit.
destruct_main_thread_on_exit: bool,
pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSGlobalObject, JSValue) void;
pub const OnException = fn (*ZigException) void;
@@ -1211,7 +1217,6 @@ pub const VirtualMachine = struct {
extern fn Bun__handleUncaughtException(*JSGlobalObject, err: JSValue, is_rejection: c_int) c_int;
extern fn Bun__handleUnhandledRejection(*JSGlobalObject, reason: JSValue, promise: JSValue) c_int;
extern fn Bun__Process__exit(*JSGlobalObject, code: c_int) noreturn;
export fn Bun__VirtualMachine__exitDuringUncaughtException(this: *JSC.VirtualMachine) void {
this.exit_on_uncaught_exception = true;
@@ -1251,12 +1256,12 @@ pub const VirtualMachine = struct {
if (this.is_handling_uncaught_exception) {
this.runErrorHandler(err, null);
Bun__Process__exit(globalObject, 7);
JSC.Process.exit(globalObject, 7);
@panic("Uncaught exception while handling uncaught exception");
}
if (this.exit_on_uncaught_exception) {
this.runErrorHandler(err, null);
Bun__Process__exit(globalObject, 1);
JSC.Process.exit(globalObject, 1);
@panic("made it past Bun__Process__exit");
}
this.is_handling_uncaught_exception = true;
@@ -1437,7 +1442,13 @@ pub const VirtualMachine = struct {
}
}
extern fn Zig__GlobalObject__destructOnExit(*JSGlobalObject) void;
pub fn globalExit(this: *VirtualMachine) noreturn {
if (this.destruct_main_thread_on_exit and this.is_main_thread) {
Zig__GlobalObject__destructOnExit(this.global);
this.deinit();
}
bun.Global.exit(this.exit_handler.exit_code);
}
@@ -1936,6 +1947,7 @@ pub const VirtualMachine = struct {
.ref_strings_mutex = .{},
.standalone_module_graph = opts.graph.?,
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(),
.destruct_main_thread_on_exit = opts.destruct_main_thread_on_exit,
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.tasks = EventLoop.Queue.init(
@@ -2008,6 +2020,10 @@ pub const VirtualMachine = struct {
graph: ?*bun.StandaloneModuleGraph = null,
debugger: bun.CLI.Command.Debugger = .{ .unspecified = {} },
is_main_thread: bool = false,
/// Whether this VM should be destroyed after it exits, even if it is the main thread's VM.
/// 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,
};
pub var is_smol_mode = false;
@@ -2058,6 +2074,7 @@ pub const VirtualMachine = struct {
.ref_strings = JSC.RefString.Map.init(allocator),
.ref_strings_mutex = .{},
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(),
.destruct_main_thread_on_exit = opts.destruct_main_thread_on_exit,
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.tasks = EventLoop.Queue.init(
@@ -2219,6 +2236,8 @@ pub const VirtualMachine = struct {
.standalone_module_graph = worker.parent.standalone_module_graph,
.worker = worker,
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(),
// This option is irrelevant for Workers
.destruct_main_thread_on_exit = false,
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.tasks = EventLoop.Queue.init(
@@ -2312,6 +2331,7 @@ pub const VirtualMachine = struct {
.ref_strings = JSC.RefString.Map.init(allocator),
.ref_strings_mutex = .{},
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId(),
.destruct_main_thread_on_exit = opts.destruct_main_thread_on_exit,
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.tasks = EventLoop.Queue.init(

View File

@@ -40,10 +40,14 @@ pub fn setDefaultAutoSelectFamily(global: *JSC.JSGlobalObject) JSC.JSValue {
}).setter, 1, .{});
}
//
//
pub var autoSelectFamilyAttemptTimeoutDefault: u32 = 250;
/// This is only used to provide the getDefaultAutoSelectFamilyAttemptTimeout and
/// setDefaultAutoSelectFamilyAttemptTimeout functions, not currently read by any other code. It's
/// `threadlocal` because Node.js expects each Worker to have its own copy of this, and currently
/// it can only be accessed by accessor functions which run on each Worker's main JavaScript thread.
///
/// If this becomes used in more places, and especially if it can be read by other threads, we may
/// need to store it as a field in the VirtualMachine instead of in a `threadlocal`.
pub threadlocal var autoSelectFamilyAttemptTimeoutDefault: u32 = 250;
pub fn getDefaultAutoSelectFamilyAttemptTimeout(global: *JSC.JSGlobalObject) JSC.JSValue {
return JSC.JSFunction.create(global, "getDefaultAutoSelectFamilyAttemptTimeout", (struct {

View File

@@ -1972,7 +1972,7 @@ pub const Process = struct {
var args_count: usize = vm.argv.len;
if (vm.worker) |worker| {
args_count = if (worker.argv) |argv| argv.len else 0;
args_count = worker.argv.len;
}
const args = allocator.alloc(
@@ -2007,10 +2007,8 @@ pub const Process = struct {
defer allocator.free(args);
if (vm.worker) |worker| {
if (worker.argv) |argv| {
for (argv) |arg| {
args_list.appendAssumeCapacity(bun.String.init(arg));
}
for (worker.argv) |arg| {
args_list.appendAssumeCapacity(bun.String.init(arg));
}
} else {
for (vm.argv) |arg| {
@@ -2082,7 +2080,8 @@ pub const Process = struct {
}
}
pub fn exit(globalObject: *JSC.JSGlobalObject, code: u8) callconv(.C) void {
// TODO(@190n) this may need to be noreturn
pub fn exit(globalObject: *JSC.JSGlobalObject, code: u8) callconv(.c) void {
var vm = globalObject.bunVM();
if (vm.worker) |worker| {
vm.exit_handler.exit_code = code;

View File

@@ -35,7 +35,8 @@ pub const WebWorker = struct {
worker_event_loop_running: bool = true,
parent_poll_ref: Async.KeepAlive = .{},
argv: ?[]const WTFStringImpl,
// kept alive by C++ Worker object
argv: []const WTFStringImpl,
execArgv: ?[]const WTFStringImpl,
pub const Status = enum(u8) {
@@ -179,11 +180,12 @@ pub const WebWorker = struct {
mini: bool,
default_unref: bool,
argv_ptr: ?[*]WTFStringImpl,
argv_len: u32,
argv_len: usize,
inherit_execArgv: bool,
execArgv_ptr: ?[*]WTFStringImpl,
execArgv_len: u32,
execArgv_len: usize,
preload_modules_ptr: ?[*]bun.String,
preload_modules_len: u32,
preload_modules_len: usize,
) callconv(.C) ?*WebWorker {
JSC.markBinding(@src());
log("[{d}] WebWorker.create", .{this_context_id});
@@ -195,10 +197,7 @@ pub const WebWorker = struct {
defer parent.transpiler.setLog(prev_log);
defer temp_log.deinit();
const preload_modules = if (preload_modules_ptr) |ptr|
ptr[0..preload_modules_len]
else
&.{};
const preload_modules = if (preload_modules_ptr) |ptr| ptr[0..preload_modules_len] else &.{};
const path = resolveEntryPointSpecifier(parent, spec_slice.slice(), error_message, &temp_log) orelse {
return null;
@@ -238,8 +237,8 @@ pub const WebWorker = struct {
},
.user_keep_alive = !default_unref,
.worker_event_loop_running = true,
.argv = if (argv_ptr) |ptr| ptr[0..argv_len] else null,
.execArgv = if (execArgv_ptr) |ptr| ptr[0..execArgv_len] else null,
.argv = if (argv_ptr) |ptr| ptr[0..argv_len] else &.{},
.execArgv = if (inherit_execArgv) null else (if (execArgv_ptr) |ptr| ptr[0..execArgv_len] else &.{}),
.preloads = preloads.items,
};

View File

@@ -68,6 +68,7 @@ pub const Run = struct {
.args = ctx.args,
.graph = graph_ptr,
.is_main_thread = true,
.destruct_main_thread_on_exit = bun.getenvTruthy("BUN_DESTRUCT_VM_ON_EXIT"),
}),
.arena = arena,
.ctx = ctx,
@@ -205,6 +206,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,
.destruct_main_thread_on_exit = bun.getenvTruthy("BUN_DESTRUCT_VM_ON_EXIT"),
},
),
.arena = arena,

View File

@@ -1276,6 +1276,7 @@ pub const TestCommand = struct {
.smol = ctx.runtime_options.smol,
.debugger = ctx.runtime_options.debugger,
.is_main_thread = true,
.destruct_main_thread_on_exit = bun.getenvTruthy("BUN_DESTRUCT_VM_ON_EXIT"),
},
);
vm.argv = ctx.passthrough;
@@ -1335,7 +1336,7 @@ pub const TestCommand = struct {
strings.startsWith(arg, "./") or
strings.startsWith(arg, "../") or
(Environment.isWindows and (strings.startsWith(arg, ".\\") or
strings.startsWith(arg, "..\\")))) break true;
strings.startsWith(arg, "..\\")))) break true;
} else false) {
// One of the files is a filepath. Instead of treating the arguments as filters, treat them as filepaths
for (ctx.positionals[1..]) |arg| {
@@ -1453,9 +1454,9 @@ pub const TestCommand = struct {
if (has_file_like == null and
(strings.hasSuffixComptime(filter, ".ts") or
strings.hasSuffixComptime(filter, ".tsx") or
strings.hasSuffixComptime(filter, ".js") or
strings.hasSuffixComptime(filter, ".jsx")))
strings.hasSuffixComptime(filter, ".tsx") or
strings.hasSuffixComptime(filter, ".js") or
strings.hasSuffixComptime(filter, ".jsx")))
{
has_file_like = i;
}
@@ -1591,6 +1592,8 @@ pub const TestCommand = struct {
Global.exit(1);
} else if (reporter.jest.unhandled_errors_between_tests > 0) {
Global.exit(reporter.jest.unhandled_errors_between_tests);
} else {
vm.runWithAPILock(JSC.VirtualMachine, vm, JSC.VirtualMachine.globalExit);
}
}

View File

@@ -2164,7 +2164,7 @@ pub const PackageManifest = struct {
if (count > 0 and
((comptime !is_peer) or
optional_peer_dep_names.items.len == 0))
optional_peer_dep_names.items.len == 0))
{
const name_map_hash = name_hasher.final();
const version_map_hash = version_hasher.final();

View File

@@ -2467,10 +2467,8 @@ pub const Interpreter = struct {
}
if (vm.worker) |worker| {
if (worker.argv) |argv| {
if (int >= argv.len) return "";
return this.base.interpreter.getVmArgsUtf8(argv, int);
}
if (int >= worker.argv.len) return "";
return this.base.interpreter.getVmArgsUtf8(worker.argv, int);
}
const argv = vm.argv;
if (int >= argv.len) return "";

View File

@@ -0,0 +1,51 @@
'use strict';
const common = require('../common');
if (common.isIBMi)
common.skip('On IBMi, the rss memory always returns zero');
const assert = require('assert');
const util = require('util');
const { Worker } = require('worker_threads');
let numWorkers = +process.env.JOBS || require('os').availableParallelism();
if (numWorkers > 20) {
// Cap the number of workers at 20 (as an even divisor of 60 used as
// the total number of workers started) otherwise the test fails on
// machines with high core counts.
numWorkers = 20;
}
// Verify that a Worker's memory isn't kept in memory after the thread finishes.
function run(n, done) {
console.log(`run() called with n=${n} (numWorkers=${numWorkers})`);
if (n <= 0)
return done();
const worker = new Worker(
'require(\'worker_threads\').parentPort.postMessage(2 + 2)',
{ eval: true });
worker.on('message', common.mustCall((value) => {
assert.strictEqual(value, 4);
}));
worker.on('exit', common.mustCall(() => {
run(n - 1, done);
}));
}
const startStats = process.memoryUsage();
let finished = 0;
for (let i = 0; i < numWorkers; ++i) {
run(60 / numWorkers, () => {
console.log(`done() called (finished=${finished})`);
if (++finished === numWorkers) {
const finishStats = process.memoryUsage();
// A typical value for this ratio would be ~1.15.
// 5 as a upper limit is generous, but the main point is that we
// don't have the memory of 50 Isolates/Node.js environments just lying
// around somewhere.
assert.ok(finishStats.rss / startStats.rss < 5,
'Unexpected memory overhead: ' +
util.inspect([startStats, finishStats]));
}
});
}

View File

@@ -19,7 +19,7 @@ const { Worker } = require('worker_threads');
`, { eval: true });
w.on('message', common.mustCall(() => {
assert.strictEqual(local.toString(), 'Hello world!');
global.gc();
globalThis.gc();
w.terminate();
}));
w.postMessage({ sharedArrayBuffer });

View File

@@ -0,0 +1,11 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { Worker } = require('worker_threads');
// Regression for https://github.com/nodejs/node/issues/43182.
const w = new Worker(new URL('data:text/javascript,process.exit(1);await new Promise(()=>{ process.exit(2); })'));
w.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 1);
}));

View File

@@ -8,7 +8,7 @@ for (const fn of ['setTimeout', 'setImmediate', 'setInterval']) {
const worker = new Worker(`
const { parentPort } = require('worker_threads');
${fn}(() => {
parentPort.postMessage({});
require('worker_threads').parentPort.postMessage({});
while (true);
});`, { eval: true });

View File

@@ -0,0 +1,16 @@
'use strict';
const common = require('../common');
const { once } = require('events');
const { Worker } = require('worker_threads');
// Test that calling worker.terminate() on an unref()ed Worker instance
// still resolves the returned Promise.
async function test() {
const worker = new Worker('setTimeout(() => {}, 1000000);', { eval: true });
await once(worker, 'online');
worker.unref();
await worker.terminate();
}
test().then(common.mustCall());

View File

@@ -23,7 +23,7 @@ const { Worker } = require('worker_threads');
});
w.on('message', common.mustCall(() => {
assert.strictEqual(local.toString(), 'Hello world!');
global.gc();
globalThis.gc();
w.terminate();
}));
w.postMessage({});

View File

@@ -320,6 +320,23 @@ static napi_value create_weird_bigints(const Napi::CallbackInfo &info) {
return array;
}
static napi_value add_finalizer_to_object(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value js_object = info[0];
int *native_object = new int{123};
NODE_API_CALL(env,
napi_add_finalizer(
env, js_object, static_cast<void *>(native_object),
[](napi_env, void *data, void *) {
auto casted = static_cast<int *>(data);
printf("add_finalizer_to_object finalizer data = %d\n",
*casted);
delete casted;
},
nullptr, nullptr));
return ok(env);
}
void register_js_test_helpers(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, create_ref_with_finalizer);
REGISTER_FUNCTION(env, exports, was_finalize_called);
@@ -333,6 +350,7 @@ void register_js_test_helpers(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, try_add_tag);
REGISTER_FUNCTION(env, exports, check_tag);
REGISTER_FUNCTION(env, exports, create_weird_bigints);
REGISTER_FUNCTION(env, exports, add_finalizer_to_object);
}
} // namespace napitests

View File

@@ -2,6 +2,7 @@ const assert = require("node:assert");
const nativeTests = require("./build/Debug/napitests.node");
const secondAddon = require("./build/Debug/second_addon.node");
const asyncFinalizeAddon = require("./build/Debug/async_finalize_addon.node");
const { Worker } = require("node:worker_threads");
async function gcUntil(fn) {
const MAX = 100;
@@ -618,4 +619,12 @@ nativeTests.test_get_value_string = () => {
}
};
// Should be run with
// BUN_DESTRUCT_VM_ON_EXIT=1 -- makes us tear down the JSC::VM while exiting, so that finalizers run
// BUN_JSC_useGC=0 -- ensures the object's finalizer will be called at exit not during normal GC
nativeTests.test_finalizer_called_during_destruction = () => {
let object = {};
nativeTests.add_finalizer_to_object(object);
};
module.exports = nativeTests;

View File

@@ -1,6 +1,6 @@
import { spawnSync } from "bun";
import { beforeAll, describe, expect, it } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, isBroken, tempDirWithFiles } from "harness";
import { join } from "path";
describe("napi", () => {
@@ -453,18 +453,27 @@ describe("napi", () => {
it("works when the module register function throws", () => {
expect(() => require("./napi-app/build/Debug/throw_addon.node")).toThrow(new Error("oops!"));
});
describe("napi_add_finalizer", () => {
it.todoIf(isBroken)("does not crash if the finalizer is called during VM shutdown", () => {
checkSameOutput("test_finalizer_called_during_destruction", [], {
BUN_DESTRUCT_VM_ON_EXIT: "1",
BUN_JSC_useGC: "0",
});
});
});
});
function checkSameOutput(test: string, args: any[] | string) {
const nodeResult = runOn("node", test, args).trim();
let bunResult = runOn(bunExe(), test, args);
function checkSameOutput(test: string, args: any[] | string, env: object = {}) {
const nodeResult = runOn("node", test, args, env).trim();
let bunResult = runOn(bunExe(), test, args, env);
// remove all debug logs
bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim();
expect(bunResult).toEqual(nodeResult);
return nodeResult;
}
function runOn(executable: string, test: string, args: any[] | string) {
function runOn(executable: string, test: string, args: any[] | string, env: object) {
// when the inspector runs (can be due to VSCode extension), there is
// a bug that in debug modes the console logs extra stuff
const { BUN_INSPECT_CONNECT_TO: _, ...rest } = bunEnv;
@@ -476,7 +485,8 @@ function runOn(executable: string, test: string, args: any[] | string) {
test,
typeof args == "string" ? args : JSON.stringify(args),
],
env: rest,
env: { ...rest, ...env },
cwd: join(__dirname, "napi-app"),
});
const errs = exec.stderr.toString();
if (errs !== "") {