mirror of
https://github.com/oven-sh/bun
synced 2026-02-07 01:18:51 +00:00
Compare commits
21 Commits
claude/imp
...
ben/udp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f2b530133 | ||
|
|
62aade4493 | ||
|
|
dfcbdcba23 | ||
|
|
488fc97b5e | ||
|
|
806b59c7fe | ||
|
|
6382ae9b61 | ||
|
|
ea40a1dab9 | ||
|
|
31e778efa4 | ||
|
|
a0e41dc4f7 | ||
|
|
92a73de564 | ||
|
|
db77d8c9f6 | ||
|
|
c882d75071 | ||
|
|
eacc092e2e | ||
|
|
3724c34353 | ||
|
|
b0628fea99 | ||
|
|
1448e6ad91 | ||
|
|
985bd5cc4e | ||
|
|
5ea190b12f | ||
|
|
c434e07b0a | ||
|
|
35e2a6cbfa | ||
|
|
e222f8ceb3 |
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
src/bun.js/bindings/headers.h
generated
2
src/bun.js/bindings/headers.h
generated
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
32
src/bun.js/bindings/test-vm-on-exit.cpp
Normal file
32
src/bun.js/bindings/test-vm-on-exit.cpp
Normal 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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
51
test/js/node/test/parallel/test-worker-memory.js
Normal file
51
test/js/node/test/parallel/test-worker-memory.js
Normal 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]));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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 });
|
||||
|
||||
|
||||
16
test/js/node/test/parallel/test-worker-terminate-unrefed.js
Normal file
16
test/js/node/test/parallel/test-worker-terminate-unrefed.js
Normal 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());
|
||||
@@ -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({});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 !== "") {
|
||||
|
||||
Reference in New Issue
Block a user