diff --git a/src/Global.zig b/src/Global.zig index 084ab3ac5a1c49..8f97d4fecc787a 100644 --- a/src/Global.zig +++ b/src/Global.zig @@ -225,10 +225,6 @@ pub export fn Bun__onExit() void { std.mem.doNotOptimizeAway(&Bun__atexit); Output.Source.Stdio.restore(); - - if (Environment.isWindows) { - bun.windows.libuv.uv_library_shutdown(); - } } comptime { diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index 41f58ff9baa3e1..9422b9542b4acf 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -43,7 +43,7 @@ fn onDrain(socket: *uws.udp.Socket) callconv(.C) void { event_loop.enter(); defer event_loop.exit(); _ = callback.call(this.globalThis, this.thisValue, &.{this.thisValue}) catch |err| { - _ = this.callErrorHandler(.zero, &.{this.globalThis.takeException(err)}); + this.callErrorHandler(.zero, this.globalThis.takeException(err)); }; } @@ -111,7 +111,7 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c JSC.jsNumber(port), hostname_string.transferToJS(globalThis), }) catch |err| { - _ = udpSocket.callErrorHandler(.zero, &.{udpSocket.globalThis.takeException(err)}); + udpSocket.callErrorHandler(.zero, udpSocket.globalThis.takeException(err)); }; } } @@ -375,22 +375,21 @@ pub const UDPSocket = struct { pub fn callErrorHandler( this: *This, thisValue: JSValue, - err: []const JSValue, - ) bool { + err: JSValue, + ) void { const callback = this.config.on_error; const globalThis = this.globalThis; const vm = globalThis.bunVM(); + if (err.isTerminationException(vm.jsc)) { + return; + } if (callback == .zero) { - if (err.len > 0) - _ = vm.uncaughtException(globalThis, err[0], false); - - return false; + _ = vm.uncaughtException(globalThis, err, false); + return; } - _ = callback.call(globalThis, thisValue, err) catch |e| globalThis.reportActiveExceptionAsUnhandled(e); - - return true; + _ = callback.call(globalThis, thisValue, &.{err}) catch |e| globalThis.reportActiveExceptionAsUnhandled(e); } pub fn setBroadcast(this: *This, globalThis: *JSGlobalObject, callframe: *CallFrame) bun.JSError!JSValue { diff --git a/src/bun.js/bindings/BunInjectedScriptHost.cpp b/src/bun.js/bindings/BunInjectedScriptHost.cpp index 51e8ef1ef5ed91..8cda82b022dd13 100644 --- a/src/bun.js/bindings/BunInjectedScriptHost.cpp +++ b/src/bun.js/bindings/BunInjectedScriptHost.cpp @@ -54,12 +54,14 @@ static JSObject* objectForEventTargetListeners(VM& vm, JSGlobalObject* exec, Eve auto* scriptExecutionContext = eventTarget->scriptExecutionContext(); if (!scriptExecutionContext) return nullptr; + auto scope = DECLARE_THROW_SCOPE(vm); JSObject* listeners = nullptr; for (auto& eventType : eventTarget->eventTypes()) { unsigned listenersForEventIndex = 0; auto* listenersForEvent = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); for (auto& eventListener : eventTarget->eventListeners(eventType)) { if (!is(eventListener->callback())) @@ -74,6 +76,7 @@ static JSObject* objectForEventTargetListeners(VM& vm, JSGlobalObject* exec, Eve continue; auto* propertiesForListener = constructEmptyObject(exec); + RETURN_IF_EXCEPTION(scope, {}); propertiesForListener->putDirect(vm, Identifier::fromString(vm, "callback"_s), jsFunction); propertiesForListener->putDirect(vm, Identifier::fromString(vm, "capture"_s), jsBoolean(eventListener->useCapture())); propertiesForListener->putDirect(vm, Identifier::fromString(vm, "passive"_s), jsBoolean(eventListener->isPassive())); @@ -82,8 +85,10 @@ static JSObject* objectForEventTargetListeners(VM& vm, JSGlobalObject* exec, Eve } if (listenersForEventIndex) { - if (!listeners) + if (!listeners) { listeners = constructEmptyObject(exec); + RETURN_IF_EXCEPTION(scope, {}); + } listeners->putDirect(vm, Identifier::fromString(vm, eventType), listenersForEvent); } } @@ -121,6 +126,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (auto* worker = JSWorker::toWrapped(vm, value)) { unsigned index = 0; auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); String name = worker->name(); if (!name.isEmpty()) @@ -142,6 +148,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (type == JSDOMWrapperType) { if (auto* headers = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, headers)); RETURN_IF_EXCEPTION(scope, {}); return array; @@ -149,6 +156,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (auto* formData = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, formData)); RETURN_IF_EXCEPTION(scope, {}); return array; @@ -157,6 +165,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe } else if (type == JSAsJSONType) { if (auto* params = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, params)); RETURN_IF_EXCEPTION(scope, {}); return array; @@ -164,6 +173,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (auto* cookie = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, cookie)); RETURN_IF_EXCEPTION(scope, {}); return array; @@ -171,6 +181,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (auto* cookieMap = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, cookieMap)); RETURN_IF_EXCEPTION(scope, {}); return array; @@ -181,11 +192,13 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe if (auto* eventTarget = JSEventTarget::toWrapped(vm, value)) { unsigned index = 0; auto* array = constructEmptyArray(exec, nullptr); + RETURN_IF_EXCEPTION(scope, {}); - if (auto* listeners = objectForEventTargetListeners(vm, exec, eventTarget)) + if (auto* listeners = objectForEventTargetListeners(vm, exec, eventTarget)) { array->putDirectIndex(exec, index++, constructInternalProperty(vm, exec, "listeners"_s, listeners)); + RETURN_IF_EXCEPTION(scope, {}); + } - RETURN_IF_EXCEPTION(scope, {}); return array; } diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 092596470232e4..b3f29a0fd7cb32 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -179,8 +179,10 @@ static JSValue constructPlatform(VM& vm, JSObject* processObject) static JSValue constructVersions(VM& vm, JSObject* processObject) { + auto scope = DECLARE_THROW_SCOPE(vm); auto* globalObject = processObject->globalObject(); JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 24); + RETURN_IF_EXCEPTION(scope, {}); object->putDirect(vm, JSC::Identifier::fromString(vm, "node"_s), JSC::JSValue(JSC::jsOwnedString(vm, makeAtomString(ASCIILiteral::fromLiteralUnsafe(REPORTED_NODEJS_VERSION))))); @@ -1470,274 +1472,11 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessConnected, (JSC::JSGlobalObject * lexicalGlob static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalObject, const String& fileName) { + auto scope = DECLARE_THROW_SCOPE(vm); #if !OS(WINDOWS) - // macOS output: - // { - // header: { - // reportVersion: 3, - // event: 'JavaScript API', - // trigger: 'GetReport', - // filename: null, - // dumpEventTime: '2023-11-16T17:56:55Z', - // dumpEventTimeStamp: '1700186215013', - // processId: 18234, - // threadId: 0, - // cwd: '/Users/jarred/Code/bun', - // commandLine: [ 'node' ], - // nodejsVersion: 'v20.8.0', - // wordSize: 64, - // arch: 'arm64', - // platform: 'darwin', - // componentVersions: process.versions, - // release: { - // name: 'node', - // headersUrl: 'https://nodejs.org/download/release/v20.8.0/node-v20.8.0-headers.tar.gz', - // sourceUrl: 'https://nodejs.org/download/release/v20.8.0/node-v20.8.0.tar.gz' - // }, - // osName: 'Darwin', - // osRelease: '22.6.0', - // osVersion: 'Darwin Kernel Version 22.6.0: Wed Jul 5 22:22:05 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T6000', - // osMachine: 'arm64', - // cpus: [], - // networkInterfaces: [], - // host: 'macbook.local' - // }, - // javascriptStack: { - // message: 'Error [ERR_SYNTHETIC]: JavaScript Callstack', - // stack: [ - // 'at new NodeError (node:internal/errors:406:5)', - // 'at Object.getReport (node:internal/process/report:36:13)', - // 'at REPL68:1:16', - // 'at Script.runInThisContext (node:vm:122:12)', - // 'at REPLServer.defaultEval (node:repl:594:29)', - // 'at bound (node:domain:432:15)', - // 'at REPLServer.runBound [as eval] (node:domain:443:12)', - // 'at REPLServer.onLine (node:repl:924:10)', - // 'at REPLServer.emit (node:events:526:35)' - // ], - // errorProperties: { code: 'ERR_SYNTHETIC' } - // }, - // javascriptHeap: { - // totalMemory: 5734400, - // executableMemory: 524288, - // totalCommittedMemory: 4931584, - // availableMemory: 4341838112, - // totalGlobalHandlesMemory: 8192, - // usedGlobalHandlesMemory: 8000, - // usedMemory: 4304384, - // memoryLimit: 4345298944, - // mallocedMemory: 147560, - // externalMemory: 2152593, - // peakMallocedMemory: 892416, - // nativeContextCount: 1, - // detachedContextCount: 0, - // doesZapGarbage: 0, - // heapSpaces: { - // read_only_space: [Object], - // new_space: [Object], - // old_space: [Object], - // code_space: [Object], - // shared_space: [Object], - // new_large_object_space: [Object], - // large_object_space: [Object], - // code_large_object_space: [Object], - // shared_large_object_space: [Object] - // } - // }, - // nativeStack: [ - // { - // pc: '0x0000000105293a44', - // symbol: 'node::GetNodeReport(node::Environment*, char const*, char const*, v8::Local, std::__1::basic_ostream>&) [/opt/homebrew/Cellar/node/20.8.0/bin/node]' - // }, - // ], - // resourceUsage: { - // free_memory: 14188216320, - // total_memory: 68719476736, - // rss: 40009728, - // available_memory: 14188216320, - // userCpuSeconds: 0.244133, - // kernelCpuSeconds: 0.058853, - // cpuConsumptionPercent: 1.16533, - // userCpuConsumptionPercent: 0.938973, - // kernelCpuConsumptionPercent: 0.226358, - // maxRss: 41697280, - // pageFaults: { IORequired: 1465, IONotRequired: 1689 }, - // fsActivity: { reads: 0, writes: 0 } - // }, - // libuv: [], - // workers: [], - // environmentVariables: { - // PATH: '', - // }, - // userLimits: { - // core_file_size_blocks: { soft: 0, hard: 'unlimited' }, - // data_seg_size_kbytes: { soft: 'unlimited', hard: 'unlimited' }, - // file_size_blocks: { soft: 'unlimited', hard: 'unlimited' }, - // max_locked_memory_bytes: { soft: 'unlimited', hard: 'unlimited' }, - // max_memory_size_kbytes: { soft: 'unlimited', hard: 'unlimited' }, - // open_files: { soft: 2147483646, hard: 2147483646 }, - // stack_size_bytes: { soft: 8372224, hard: 67092480 }, - // cpu_time_seconds: { soft: 'unlimited', hard: 'unlimited' }, - // max_user_processes: { soft: 10666, hard: 16000 }, - // virtual_memory_kbytes: { soft: 'unlimited', hard: 'unlimited' } - // }, - // sharedObjects: [ - // '/opt/homebrew/Cellar/node/20.8.0/bin/node', - // ] - - // linux: - // { - // header: { - // reportVersion: 3, - // event: 'JavaScript API', - // trigger: 'GetReport', - // filename: null, - // dumpEventTime: '2023-11-16T18:41:38Z', - // dumpEventTimeStamp: '1700188898941', - // processId: 1621753, - // threadId: 0, - // cwd: '/home/jarred', - // commandLine: [ 'node' ], - // nodejsVersion: 'v20.5.0', - // glibcVersionRuntime: '2.35', - // glibcVersionCompiler: '2.28', - // wordSize: 64, - // arch: 'x64', - // platform: 'linux', - // componentVersions: { - // acorn: '8.10.0', - // ada: '2.5.1', - // ares: '1.19.1', - // base64: '0.5.0', - // brotli: '1.0.9', - // cjs_module_lexer: '1.2.2', - // cldr: '43.1', - // icu: '73.2', - // llhttp: '8.1.1', - // modules: '115', - // napi: '9', - // nghttp2: '1.55.1', - // nghttp3: '0.7.0', - // ngtcp2: '0.8.1', - // node: '20.5.0', - // openssl: '3.0.9+quic', - // simdutf: '3.2.14', - // tz: '2023c', - // undici: '5.22.1', - // unicode: '15.0', - // uv: '1.46.0', - // uvwasi: '0.0.18', - // v8: '11.3.244.8-node.10', - // zlib: '1.2.13.1-motley' - // }, - // release: { - // name: 'node', - // headersUrl: 'https://nodejs.org/download/release/v20.5.0/node-v20.5.0-headers.tar.gz', - // sourceUrl: 'https://nodejs.org/download/release/v20.5.0/node-v20.5.0.tar.gz' - // }, - // osName: 'Linux', - // osRelease: '5.17.0-1016-oem', - // osVersion: '#17-Ubuntu SMP PREEMPT Mon Aug 22 11:31:08 UTC 2022', - // osMachine: 'x86_64', - // cpus: [ - // ], - // networkInterfaces: [ - - // ], - // host: 'jarred-desktop' - // }, - // javascriptStack: { - // message: 'Error [ERR_SYNTHETIC]: JavaScript Callstack', - // stack: [ - // 'at new NodeError (node:internal/errors:405:5)', - // 'at Object.getReport (node:internal/process/report:36:13)', - // 'at REPL18:1:16', - // 'at Script.runInThisContext (node:vm:122:12)', - // 'at REPLServer.defaultEval (node:repl:593:29)', - // 'at bound (node:domain:433:15)', - // 'at REPLServer.runBound [as eval] (node:domain:444:12)', - // 'at REPLServer.onLine (node:repl:923:10)', - // 'at REPLServer.emit (node:events:526:35)' - // ], - // errorProperties: { code: 'ERR_SYNTHETIC' } - // }, - // javascriptHeap: { - // totalMemory: 6696960, - // executableMemory: 262144, - // totalCommittedMemory: 6811648, - // availableMemory: 4339915016, - // totalGlobalHandlesMemory: 8192, - // usedGlobalHandlesMemory: 4416, - // usedMemory: 5251032, - // memoryLimit: 4345298944, - // mallocedMemory: 262312, - // externalMemory: 2120511, - // peakMallocedMemory: 521312, - // nativeContextCount: 2, - // detachedContextCount: 0, - // doesZapGarbage: 0, - // heapSpaces: { - // read_only_space: [Object], - // new_space: [Object], - // old_space: [Object], - // code_space: [Object], - // shared_space: [Object], - // new_large_object_space: [Object], - // large_object_space: [Object], - // code_large_object_space: [Object], - // shared_large_object_space: [Object] - // } - // }, - // nativeStack: [ - - // ], - // resourceUsage: { - // free_memory: 64445558784, - // total_memory: 67358441472, - // rss: 52109312, - // constrained_memory: 18446744073709552000, - // available_memory: 18446744073657442000, - // userCpuSeconds: 0.105635, - // kernelCpuSeconds: 0.033611, - // cpuConsumptionPercent: 4.64153, - // userCpuConsumptionPercent: 3.52117, - // kernelCpuConsumptionPercent: 1.12037, - // maxRss: 52150272, - // pageFaults: { IORequired: 26, IONotRequired: 3917 }, - // fsActivity: { reads: 3536, writes: 24 } - // }, - // uvthreadResourceUsage: { - // userCpuSeconds: 0.088644, - // kernelCpuSeconds: 0.005214, - // cpuConsumptionPercent: 3.1286, - // userCpuConsumptionPercent: 2.9548, - // kernelCpuConsumptionPercent: 0.1738, - // fsActivity: { reads: 3512, writes: 0 } - // }, - // libuv: [ - - // ], - // workers: [], - // environmentVariables: { - // }, - // userLimits: { - // core_file_size_blocks: { soft: 'unlimited', hard: 'unlimited' }, - // data_seg_size_kbytes: { soft: 'unlimited', hard: 'unlimited' }, - // file_size_blocks: { soft: 'unlimited', hard: 'unlimited' }, - // max_locked_memory_bytes: { soft: 8419803136, hard: 8419803136 }, - // max_memory_size_kbytes: { soft: 'unlimited', hard: 'unlimited' }, - // open_files: { soft: 1048576, hard: 1048576 }, - // stack_size_bytes: { soft: 8388608, hard: 'unlimited' }, - // cpu_time_seconds: { soft: 'unlimited', hard: 'unlimited' }, - // max_user_processes: { soft: 256637, hard: 256637 }, - // virtual_memory_kbytes: { soft: 'unlimited', hard: 'unlimited' } - // }, - // sharedObjects: [ - // - // ] - // } auto constructUserLimits = [&]() -> JSValue { JSC::JSObject* userLimits = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 11); + RETURN_IF_EXCEPTION(scope, {}); static constexpr int resourceLimits[] = { RLIMIT_CORE, @@ -1767,6 +1506,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb for (size_t i = 0; i < std::size(resourceLimits); i++) { JSC::JSObject* limitObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + RETURN_IF_EXCEPTION(scope, {}); struct rlimit limit; getrlimit(resourceLimits[i], &limit); @@ -1787,6 +1527,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructResourceUsage = [&]() -> JSC::JSValue { JSC::JSObject* resourceUsage = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 11); + RETURN_IF_EXCEPTION(scope, {}); rusage usage; @@ -1804,12 +1545,14 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb resourceUsage->putDirect(vm, JSC::Identifier::fromString(vm, "maxRss"_s), JSC::jsNumber(usage.ru_maxrss), 0); JSC::JSObject* pageFaults = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + RETURN_IF_EXCEPTION(scope, {}); pageFaults->putDirect(vm, JSC::Identifier::fromString(vm, "IORequired"_s), JSC::jsNumber(usage.ru_majflt), 0); pageFaults->putDirect(vm, JSC::Identifier::fromString(vm, "IONotRequired"_s), JSC::jsNumber(usage.ru_minflt), 0); resourceUsage->putDirect(vm, JSC::Identifier::fromString(vm, "pageFaults"_s), pageFaults, 0); JSC::JSObject* fsActivity = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + RETURN_IF_EXCEPTION(scope, {}); fsActivity->putDirect(vm, JSC::Identifier::fromString(vm, "reads"_s), JSC::jsNumber(usage.ru_inblock), 0); fsActivity->putDirect(vm, JSC::Identifier::fromString(vm, "writes"_s), JSC::jsNumber(usage.ru_oublock), 0); @@ -1820,6 +1563,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructHeader = [&]() -> JSC::JSValue { JSC::JSObject* header = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype()); + RETURN_IF_EXCEPTION(scope, {}); header->putDirect(vm, JSC::Identifier::fromString(vm, "reportVersion"_s), JSC::jsNumber(3), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "event"_s), JSC::jsString(vm, String("JavaScript API"_s)), 0); @@ -1850,15 +1594,19 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb } header->putDirect(vm, JSC::Identifier::fromString(vm, "cwd"_s), JSC::jsString(vm, String::fromUTF8ReplacingInvalidSequences(std::span { reinterpret_cast(cwd), strlen(cwd) })), 0); + RETURN_IF_EXCEPTION(scope, {}); } header->putDirect(vm, JSC::Identifier::fromString(vm, "commandLine"_s), JSValue::decode(Bun__Process__createExecArgv(globalObject)), 0); + RETURN_IF_EXCEPTION(scope, {}); header->putDirect(vm, JSC::Identifier::fromString(vm, "nodejsVersion"_s), JSC::jsString(vm, String::fromLatin1(REPORTED_NODEJS_VERSION)), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "wordSize"_s), JSC::jsNumber(64), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), constructArch(vm, header), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), constructPlatform(vm, header), 0); header->putDirect(vm, JSC::Identifier::fromString(vm, "componentVersions"_s), constructVersions(vm, header), 0); + RETURN_IF_EXCEPTION(scope, {}); header->putDirect(vm, JSC::Identifier::fromString(vm, "release"_s), constructProcessReleaseObject(vm, header), 0); + RETURN_IF_EXCEPTION(scope, {}); { // uname @@ -1893,24 +1641,36 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb #endif header->putDirect(vm, Identifier::fromString(vm, "cpus"_s), JSC::constructEmptyArray(globalObject, nullptr), 0); + RETURN_IF_EXCEPTION(scope, {}); header->putDirect(vm, Identifier::fromString(vm, "networkInterfaces"_s), JSC::constructEmptyArray(globalObject, nullptr), 0); + RETURN_IF_EXCEPTION(scope, {}); return header; }; auto constructJavaScriptHeap = [&]() -> JSC::JSValue { JSC::JSObject* heap = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 16); + RETURN_IF_EXCEPTION(scope, {}); JSC::JSObject* heapSpaces = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 9); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "read_only_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "new_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "old_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "code_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "shared_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "new_large_object_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "large_object_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "code_large_object_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heapSpaces->putDirect(vm, JSC::Identifier::fromString(vm, "shared_large_object_space"_s), JSC::constructEmptyObject(globalObject), 0); + RETURN_IF_EXCEPTION(scope, {}); heap->putDirect(vm, JSC::Identifier::fromString(vm, "totalMemory"_s), JSC::jsDoubleNumber(static_cast(WTF::ramSize())), 0); heap->putDirect(vm, JSC::Identifier::fromString(vm, "executableMemory"_s), jsNumber(0), 0); @@ -1933,6 +1693,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructUVThreadResourceUsage = [&]() -> JSC::JSValue { JSC::JSObject* uvthreadResourceUsage = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 6); + RETURN_IF_EXCEPTION(scope, {}); uvthreadResourceUsage->putDirect(vm, JSC::Identifier::fromString(vm, "userCpuSeconds"_s), JSC::jsNumber(0), 0); uvthreadResourceUsage->putDirect(vm, JSC::Identifier::fromString(vm, "kernelCpuSeconds"_s), JSC::jsNumber(0), 0); @@ -1941,6 +1702,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb uvthreadResourceUsage->putDirect(vm, JSC::Identifier::fromString(vm, "kernelCpuConsumptionPercent"_s), JSC::jsNumber(0), 0); JSC::JSObject* fsActivity = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); + RETURN_IF_EXCEPTION(scope, {}); fsActivity->putDirect(vm, JSC::Identifier::fromString(vm, "reads"_s), JSC::jsNumber(0), 0); fsActivity->putDirect(vm, JSC::Identifier::fromString(vm, "writes"_s), JSC::jsNumber(0), 0); @@ -1951,6 +1713,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructJavaScriptStack = [&]() -> JSC::JSValue { JSC::JSObject* javascriptStack = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 3); + RETURN_IF_EXCEPTION(scope, {}); javascriptStack->putDirect(vm, vm.propertyNames->message, JSC::jsString(vm, String("Error [ERR_SYNTHETIC]: JavaScript Callstack"_s)), 0); @@ -1976,15 +1739,19 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb } JSC::JSArray* stackArray = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); stack.split('\n', [&](const WTF::StringView& line) { stackArray->push(globalObject, JSC::jsString(vm, line.toString().trim(isASCIIWhitespace))); + RETURN_IF_EXCEPTION(scope, ); }); + RETURN_IF_EXCEPTION(scope, {}); javascriptStack->putDirect(vm, vm.propertyNames->stack, stackArray, 0); } JSC::JSObject* errorProperties = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 1); + RETURN_IF_EXCEPTION(scope, {}); errorProperties->putDirect(vm, JSC::Identifier::fromString(vm, "code"_s), JSC::jsString(vm, String("ERR_SYNTHETIC"_s)), 0); javascriptStack->putDirect(vm, JSC::Identifier::fromString(vm, "errorProperties"_s), errorProperties, 0); return javascriptStack; @@ -1992,6 +1759,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructSharedObjects = [&]() -> JSC::JSValue { JSC::JSObject* sharedObjects = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2000,6 +1768,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructLibUV = [&]() -> JSC::JSValue { JSC::JSObject* libuv = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2008,6 +1777,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructWorkers = [&]() -> JSC::JSValue { JSC::JSObject* workers = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2020,6 +1790,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructCpus = [&]() -> JSC::JSValue { JSC::JSObject* cpus = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2028,6 +1799,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructNetworkInterfaces = [&]() -> JSC::JSValue { JSC::JSObject* networkInterfaces = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2036,6 +1808,7 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb auto constructNativeStack = [&]() -> JSC::JSValue { JSC::JSObject* nativeStack = JSC::constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); // TODO: @@ -2044,20 +1817,34 @@ static JSValue constructReportObjectComplete(VM& vm, Zig::GlobalObject* globalOb { JSC::JSObject* report = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 19); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "header"_s), constructHeader(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptStack"_s), constructJavaScriptStack(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "javascriptHeap"_s), constructJavaScriptHeap(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "nativeStack"_s), constructNativeStack(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "resourceUsage"_s), constructResourceUsage(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "uvthreadResourceUsage"_s), constructUVThreadResourceUsage(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "libuv"_s), constructLibUV(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "workers"_s), constructWorkers(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "environmentVariables"_s), constructEnvironmentVariables(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "userLimits"_s), constructUserLimits(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "sharedObjects"_s), constructSharedObjects(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "cpus"_s), constructCpus(), 0); + RETURN_IF_EXCEPTION(scope, {}); report->putDirect(vm, JSC::Identifier::fromString(vm, "networkInterfaces"_s), constructNetworkInterfaces(), 0); + RETURN_IF_EXCEPTION(scope, {}); return report; } @@ -2162,7 +1949,7 @@ extern "C" void Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(JSC::JSGl static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int fd) { auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); + auto scope = DECLARE_CATCH_SCOPE(vm); JSC::JSFunction* getStdioWriteStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdioWriteStreamCodeGenerator(vm), globalObject); JSC::MarkedArgumentBuffer args; @@ -2173,18 +1960,9 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int JSC::CallData callData = JSC::getCallData(getStdioWriteStream); - NakedPtr returnedException = nullptr; - auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject->globalThis(), args, returnedException); - RETURN_IF_EXCEPTION(scope, {}); - - if (auto* exception = returnedException.get()) { -#if ASSERT_ENABLED - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); -#endif - scope.throwException(globalObject, exception->value()); - returnedException.clear(); - return {}; - } + auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject->globalThis(), args); + scope.assertNoExceptionExceptTermination(); + CLEAR_AND_RETURN_IF_EXCEPTION(scope, jsUndefined()); ASSERT_WITH_MESSAGE(JSC::isJSArray(result), "Expected an array from getStdioWriteStream"); JSC::JSArray* resultObject = JSC::jsCast(result); @@ -2239,20 +2017,9 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) args.append(jsNumber(static_cast(fdType))); JSC::CallData callData = JSC::getCallData(getStdioWriteStream); - NakedPtr returnedException = nullptr; - auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject, args, returnedException); + auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject, args); RETURN_IF_EXCEPTION(scope, {}); - - if (auto* exception = returnedException.get()) { -#if ASSERT_ENABLED - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); -#endif - scope.throwException(globalObject, exception->value()); - returnedException.clear(); - return {}; - } - - RELEASE_AND_RETURN(scope, result); + return result; } JSC_DEFINE_CUSTOM_GETTER(processThrowDeprecation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) @@ -2309,19 +2076,8 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject) JSC::MarkedArgumentBuffer args; JSC::CallData callData = JSC::getCallData(getControl); - NakedPtr returnedException = nullptr; - auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getControl, callData, globalObject->globalThis(), args, returnedException); + auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getControl, callData, globalObject->globalThis(), args); RETURN_IF_EXCEPTION(scope, {}); - - if (auto* exception = returnedException.get()) { -#if ASSERT_ENABLED - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); -#endif - scope.throwException(globalObject, exception->value()); - returnedException.clear(); - return {}; - } - return result; } else { return jsUndefined(); @@ -2828,6 +2584,7 @@ void Process::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_memoryUsageStructure.visit(visitor); thisObject->m_bindingUV.visit(visitor); thisObject->m_bindingNatives.visit(visitor); + thisObject->m_emitHelperFunction.visit(visitor); } DEFINE_VISIT_CHILDREN(Process); @@ -3307,6 +3064,14 @@ void Process::queueNextTick(JSC::JSGlobalObject* globalObject, JSValue func, con this->queueNextTick(globalObject, argsBuffer); } +void Process::emitOnNextTick(Zig::GlobalObject* globalObject, ASCIILiteral eventName, JSValue event) +{ + auto& vm = getVM(globalObject); + auto* function = m_emitHelperFunction.getInitializedOnMainThread(this); + JSValue args[] = { jsString(vm, String(eventName)), event }; + queueNextTick(globalObject, function, args); +} + extern "C" void Bun__Process__queueNextTick1(GlobalObject* globalObject, EncodedJSValue func, EncodedJSValue arg1) { auto process = jsCast(globalObject->processObject()); @@ -3568,15 +3333,9 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObje args.append(jsNumber(signal)); JSC::CallData callData = JSC::getCallData(_killFn); - NakedPtr returnedException = nullptr; - auto result = JSC::profiledCall(globalObject, ProfilingReason::API, _killFn, callData, globalObject->globalThis(), args, returnedException); + auto result = JSC::profiledCall(globalObject, ProfilingReason::API, _killFn, callData, globalObject->globalThis(), args); RETURN_IF_EXCEPTION(scope, {}); - if (auto* exception = returnedException.get()) { - scope.throwException(globalObject, exception->value()); - returnedException.clear(); - return {}; - } auto err = result.toInt32(globalObject); if (err) { throwSystemError(scope, globalObject, "kill"_s, err); @@ -3609,6 +3368,24 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionLoadBuiltinModule, (JSGlobalObject * gl RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); } +JSC_DEFINE_HOST_FUNCTION(Process_functionEmitHelper, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + auto* process = zigGlobalObject->processObject(); + auto scope = DECLARE_THROW_SCOPE(vm); + auto emit = process->get(globalObject, Identifier::fromString(vm, "emit"_s)); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = JSC::getCallData(emit); + if (callData.type == CallData::Type::None) { + scope.throwException(globalObject, createNotAFunctionError(globalObject, emit)); + return {}; + } + auto ret = JSC::call(globalObject, emit, callData, process, callFrame); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(ret); +} + extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSValue value, EncodedJSValue handle) { auto* process = static_cast(global->processObject()); @@ -3760,6 +3537,9 @@ void Process::finishCreation(JSC::VM& vm) m_bindingNatives.initLater([](const JSC::LazyProperty::Initializer& init) { init.set(Bun::ProcessBindingNatives::create(init.vm, ProcessBindingNatives::createStructure(init.vm, init.owner->globalObject()))); }); + m_emitHelperFunction.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(JSFunction::create(init.vm, init.owner->globalObject(), 2, "emit"_s, Process_functionEmitHelper, ImplementationVisibility::Private)); + }); putDirect(vm, vm.propertyNames->toStringTagSymbol, jsString(vm, String("process"_s)), 0); putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(false), 0); diff --git a/src/bun.js/bindings/BunProcess.h b/src/bun.js/bindings/BunProcess.h index a4ab194f1c6128..cf6c0c71dad83d 100644 --- a/src/bun.js/bindings/BunProcess.h +++ b/src/bun.js/bindings/BunProcess.h @@ -22,6 +22,9 @@ class Process : public WebCore::JSEventEmitter { LazyProperty m_memoryUsageStructure; LazyProperty m_bindingUV; LazyProperty m_bindingNatives; + // Function that looks up "emit" on "process" and calls it with the provided arguments + // Only used by internal code via passing to queueNextTick + LazyProperty m_emitHelperFunction; WriteBarrier m_uncaughtExceptionCaptureCallback; WriteBarrier m_nextTickFunction; // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/bootstrap/switches/does_own_process_state.js#L113-L116 @@ -57,6 +60,10 @@ class Process : public WebCore::JSEventEmitter { template void queueNextTick(JSC::JSGlobalObject* globalObject, JSValue func, const JSValue (&args)[NumArgs]); + // Some Node.js events want to be emitted on the next tick rather than synchronously. + // This is equivalent to `process.nextTick(() => process.emit(eventName, event))` from JavaScript. + void emitOnNextTick(Zig::GlobalObject* globalObject, ASCIILiteral eventName, JSValue event); + static JSValue emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor); JSString* cachedCwd() { return m_cachedCwd.get(); } diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 98d990ff1ea517..b58b5312afb6a3 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -429,13 +429,11 @@ extern "C" JSC::EncodedJSValue BunString__createArray( // Using tryCreateUninitialized here breaks stuff.. // https://github.com/oven-sh/bun/issues/3931 JSC::JSArray* array = constructEmptyArray(globalObject, nullptr, length); - if (!array) { - JSC::throwOutOfMemoryError(globalObject, throwScope); - RELEASE_AND_RETURN(throwScope, JSValue::encode(JSC::JSValue())); - } + RETURN_IF_EXCEPTION(throwScope, {}); for (size_t i = 0; i < length; ++i) { array->putDirectIndex(globalObject, i, Bun::toJS(globalObject, *ptr++)); + RETURN_IF_EXCEPTION(throwScope, {}); } return JSValue::encode(array); diff --git a/src/bun.js/bindings/JSCommonJSModule.cpp b/src/bun.js/bindings/JSCommonJSModule.cpp index 6b40963e6f7c98..aea46b74983e17 100644 --- a/src/bun.js/bindings/JSCommonJSModule.cpp +++ b/src/bun.js/bindings/JSCommonJSModule.cpp @@ -128,18 +128,23 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj globalObject->requireResolveFunctionUnbound(), moduleObject->filename(), ArgList(), 1, globalObject->commonStrings().resolveString(globalObject)); + RETURN_IF_EXCEPTION(scope, ); requireFunction = JSC::JSBoundFunction::create(vm, globalObject, globalObject->requireFunctionUnbound(), moduleObject, ArgList(), 1, globalObject->commonStrings().requireString(globalObject)); + RETURN_IF_EXCEPTION(scope, ); requireFunction->putDirect(vm, vm.propertyNames->resolve, resolveFunction, 0); + RETURN_IF_EXCEPTION(scope, ); moduleObject->putDirect(vm, WebCore::clientData(vm)->builtinNames().requirePublicName(), requireFunction, 0); + RETURN_IF_EXCEPTION(scope, ); moduleObject->hasEvaluated = true; }; if (UNLIKELY(Bun__VM__specifierIsEvalEntryPoint(globalObject->bunVM(), JSValue::encode(filename)))) { initializeModuleObject(); + scope.assertNoExceptionExceptTermination(); // Using same approach as node, `arguments` in the entry point isn't defined // https://github.com/nodejs/node/blob/592c6907bfe1922f36240e9df076be1864c3d1bd/lib/internal/process/execution.js#L92 @@ -148,15 +153,9 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj globalObject->putDirect(vm, Identifier::fromString(vm, "module"_s), moduleObject, 0); globalObject->putDirect(vm, Identifier::fromString(vm, "__filename"_s), filename, 0); globalObject->putDirect(vm, Identifier::fromString(vm, "__dirname"_s), dirname, 0); - scope.assertNoException(); - WTF::NakedPtr returnedException; - JSValue result = JSC::evaluate(globalObject, code, jsUndefined(), returnedException); - if (UNLIKELY(returnedException)) { - scope.throwException(globalObject, returnedException.get()); - return false; - } - ASSERT(!scope.exception()); + JSValue result = JSC::evaluate(globalObject, code, jsUndefined()); + RETURN_IF_EXCEPTION(scope, false); ASSERT(result); Bun__VM__setEntryPointEvalResultCJS(globalObject->bunVM(), JSValue::encode(result)); @@ -164,13 +163,8 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj RELEASE_AND_RETURN(scope, true); } - WTF::NakedPtr returnedException; - JSValue fnValue = JSC::evaluate(globalObject, code, jsUndefined(), returnedException); - if (UNLIKELY(returnedException)) { - scope.throwException(globalObject, returnedException.get()); - RELEASE_AND_RETURN(scope, false); - } - ASSERT(!scope.exception()); + JSValue fnValue = JSC::evaluate(globalObject, code, jsUndefined()); + RETURN_IF_EXCEPTION(scope, false); ASSERT(fnValue); JSObject* fn = fnValue.getObject(); @@ -209,13 +203,9 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj // // fn(exports, require, module, __filename, __dirname) { /* code */ }(exports, require, module, __filename, __dirname) // - JSC::profiledCall(globalObject, ProfilingReason::API, fn, callData, moduleObject, args, returnedException); - if (UNLIKELY(returnedException)) { - scope.throwException(globalObject, returnedException.get()); - return false; - } - ASSERT(!scope.exception()); - RELEASE_AND_RETURN(scope, true); + JSC::profiledCall(globalObject, ProfilingReason::API, fn, callData, moduleObject, args); + RETURN_IF_EXCEPTION(scope, false); + return true; } bool JSCommonJSModule::load(JSC::VM& vm, Zig::GlobalObject* globalObject) @@ -1313,7 +1303,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireNativeModule, (JSGlobalObject * lexica if (res.success) return JSC::JSValue::encode(result); } - ASSERT_WITH_MESSAGE(false, "Failed to fetch builtin module %s", specifier.utf8().data()); + throwScope.assertNoExceptionExceptTermination(); return throwVMError(globalObject, throwScope, "Failed to fetch builtin module"_s); } diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp index efff692442cf29..647d3b5c8671e8 100644 --- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp +++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp @@ -293,6 +293,7 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) #if OS(WINDOWS) JSArray* keyArray = constructEmptyArray(globalObject, nullptr, count); + RETURN_IF_EXCEPTION(scope, {}); #endif static NeverDestroyed TZ = MAKE_STATIC_STRING_IMPL("TZ"); @@ -337,6 +338,7 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) ZigString nameStr = toZigString(name); if (Bun__getEnvValue(globalObject, &nameStr, &valueString)) { JSValue value = jsString(vm, Zig::toStringCopy(valueString)); + RETURN_IF_EXCEPTION(scope, {}); object->putDirectIndex(globalObject, *index, value, 0, PutDirectIndexLikePutDirect); } continue; diff --git a/src/bun.js/bindings/JSGlobalObject.zig b/src/bun.js/bindings/JSGlobalObject.zig index d8d31cf3d3655d..72eeebf4798a87 100644 --- a/src/bun.js/bindings/JSGlobalObject.zig +++ b/src/bun.js/bindings/JSGlobalObject.zig @@ -533,7 +533,10 @@ pub const JSGlobalObject = opaque { /// return global.reportActiveExceptionAsUnhandled(err); /// pub fn reportActiveExceptionAsUnhandled(this: *JSGlobalObject, err: bun.JSError) void { - _ = this.bunVM().uncaughtException(this, this.takeException(err), false); + const exception = this.takeException(err); + if (!exception.isTerminationException(this.vm())) { + _ = this.bunVM().uncaughtException(this, exception, false); + } } pub fn vm(this: *JSGlobalObject) *VM { @@ -854,6 +857,12 @@ pub const JSGlobalObject = opaque { return JSC.Error.INVALID_ARG_TYPE.fmt(global, fmt, args); } + extern fn ScriptExecutionContextIdentifier__forGlobalObject(global: *JSC.JSGlobalObject) u32; + + pub fn scriptExecutionContextIdentifier(global: *JSC.JSGlobalObject) bun.webcore.ScriptExecutionContext.Identifier { + return @enumFromInt(ScriptExecutionContextIdentifier__forGlobalObject(global)); + } + pub const Extern = [_][]const u8{ "create", "getModuleRegistryMap", "resetModuleRegistryMap" }; comptime { diff --git a/src/bun.js/bindings/JSNextTickQueue.cpp b/src/bun.js/bindings/JSNextTickQueue.cpp index 642ff260453b26..190b5714b5d530 100644 --- a/src/bun.js/bindings/JSNextTickQueue.cpp +++ b/src/bun.js/bindings/JSNextTickQueue.cpp @@ -76,20 +76,24 @@ bool JSNextTickQueue::isEmpty() void JSNextTickQueue::drain(JSC::VM& vm, JSC::JSGlobalObject* globalObject) { + auto throwScope = DECLARE_THROW_SCOPE(vm); bool mustResetContext = false; if (isEmpty()) { + RETURN_IF_EXCEPTION(throwScope, ); vm.drainMicrotasks(); + RETURN_IF_EXCEPTION(throwScope, ); mustResetContext = true; } if (!isEmpty()) { + RETURN_IF_EXCEPTION(throwScope, ); if (mustResetContext) { globalObject->m_asyncContextData.get()->putInternalField(vm, 0, jsUndefined()); } auto* drainFn = internalField(2).get().getObject(); - auto throwScope = DECLARE_THROW_SCOPE(vm); MarkedArgumentBuffer drainArgs; JSC::call(globalObject, drainFn, drainArgs, "Failed to drain next tick queue"_s); + RETURN_IF_EXCEPTION(throwScope, ); } } diff --git a/src/bun.js/bindings/NodeURL.cpp b/src/bun.js/bindings/NodeURL.cpp index 15f0581e764e12..86b1e70bea227b 100644 --- a/src/bun.js/bindings/NodeURL.cpp +++ b/src/bun.js/bindings/NodeURL.cpp @@ -146,16 +146,23 @@ JSC_DEFINE_HOST_FUNCTION(jsDomainToUnicode, (JSC::JSGlobalObject * globalObject, JSC::JSValue createNodeURLBinding(Zig::GlobalObject* globalObject) { VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); auto binding = constructEmptyArray(globalObject, nullptr, 2); + RETURN_IF_EXCEPTION(scope, {}); + ASSERT(binding); + auto domainToAsciiFunction = JSC::JSFunction::create(vm, globalObject, 1, "domainToAscii"_s, jsDomainToASCII, ImplementationVisibility::Public); + ASSERT(domainToAsciiFunction); + auto domainToUnicodeFunction = JSC::JSFunction::create(vm, globalObject, 1, "domainToUnicode"_s, jsDomainToUnicode, ImplementationVisibility::Public); + ASSERT(domainToUnicodeFunction); binding->putByIndexInline( globalObject, (unsigned)0, - JSC::JSFunction::create(vm, globalObject, 1, "domainToAscii"_s, jsDomainToASCII, ImplementationVisibility::Public), + domainToAsciiFunction, false); binding->putByIndexInline( globalObject, (unsigned)1, - JSC::JSFunction::create(vm, globalObject, 1, "domainToUnicode"_s, jsDomainToUnicode, ImplementationVisibility::Public), + domainToUnicodeFunction, false); return binding; } diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index cae14b6f7b9dd1..e43991ceb6fe0b 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -391,4 +391,17 @@ void ScriptExecutionContext::postTaskOnTimeout(FunctionscriptExecutionContext()->identifier(); } + +extern "C" JSC::JSGlobalObject* ScriptExecutionContextIdentifier__getGlobalObject(ScriptExecutionContextIdentifier id) +{ + auto* context = ScriptExecutionContext::getScriptExecutionContext(id); + if (!context) return nullptr; + return context->globalObject(); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 84f10bf7c0c219..5e534e5e107168 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -233,8 +233,6 @@ constexpr size_t DEFAULT_ERROR_STACK_TRACE_LIMIT = 10; Structure* createMemoryFootprintStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject); -extern "C" WebCore::Worker* WebWorker__getParentWorker(void*); - #ifndef BUN_WEBKIT_VERSION #ifndef ASSERT_ENABLED #warning "BUN_WEBKIT_VERSION is not defined. WebKit's cmakeconfig.h is supposed to define that. If you're building a release build locally, ignore this warning. If you're seeing this warning in CI, please file an issue." @@ -1302,11 +1300,6 @@ void GlobalObject::destroy(JSCell* cell) static_cast(cell)->GlobalObject::~GlobalObject(); } -WebCore::ScriptExecutionContext* GlobalObject::scriptExecutionContext() -{ - return m_scriptExecutionContext; -} - WebCore::ScriptExecutionContext* GlobalObject::scriptExecutionContext() const { return m_scriptExecutionContext; @@ -4382,6 +4375,8 @@ bool GlobalObject::hasNapiFinalizers() const return false; } +void GlobalObject::setNodeWorkerEnvironmentData(JSMap* data) { m_nodeWorkerEnvironmentData.set(vm(), this, data); } + extern "C" void Zig__GlobalObject__destructOnExit(Zig::GlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index e2c4cc90cc3392..827c99f0c926f7 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -164,7 +164,6 @@ class GlobalObject : public Bun::GlobalScope { bool worldIsNormal() const { return m_worldIsNormal; } static ptrdiff_t offsetOfWorldIsNormal() { return OBJECT_OFFSETOF(GlobalObject, m_worldIsNormal); } - WebCore::ScriptExecutionContext* scriptExecutionContext(); WebCore::ScriptExecutionContext* scriptExecutionContext() const; void queueTask(WebCore::EventLoopTask* task); @@ -469,6 +468,10 @@ class GlobalObject : public Bun::GlobalScope { /* move them off the stack which will cause them to get collected if not in the handle scope. */ \ V(public, JSC::WriteBarrier, m_currentNapiHandleScopeImpl) \ \ + /* Supports getEnvironmentData() and setEnvironmentData(), and is cloned into newly-created */ \ + /* Workers. Initialized in createNodeWorkerThreadsBinding. */ \ + V(private, WriteBarrier, m_nodeWorkerEnvironmentData) \ + \ /* The original, unmodified Error.prepareStackTrace. */ \ /* */ \ /* We set a default value for this to mimic Node.js behavior It is a */ \ @@ -601,9 +604,14 @@ class GlobalObject : public Bun::GlobalScope { #define DECLARE_GLOBALOBJECT_GC_MEMBER(visibility, T, name) \ visibility: \ T name; + FOR_EACH_GLOBALOBJECT_GC_MEMBER(DECLARE_GLOBALOBJECT_GC_MEMBER) + #undef DECLARE_GLOBALOBJECT_GC_MEMBER + // Ensure that everything below here has a consistent visibility instead of taking the + // visibility of the last thing declared with FOR_EACH_GLOBALOBJECT_GC_MEMBER +public: WTF::String m_moduleWrapperStart; WTF::String m_moduleWrapperEnd; @@ -662,6 +670,9 @@ class GlobalObject : public Bun::GlobalScope { JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); } JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); } + JSMap* nodeWorkerEnvironmentData() { return m_nodeWorkerEnvironmentData.get(); } + void setNodeWorkerEnvironmentData(JSMap* data); + Bun::CommonStrings& commonStrings() { return m_commonStrings; } Bun::Http2CommonStrings& http2CommonStrings() { return m_http2CommonStrings; } #include "ZigGeneratedClasses+lazyStructureHeader.h" diff --git a/src/bun.js/bindings/webcore/JSWorker.cpp b/src/bun.js/bindings/webcore/JSWorker.cpp index 676ea4543f54a4..0250768c45c695 100644 --- a/src/bun.js/bindings/webcore/JSWorker.cpp +++ b/src/bun.js/bindings/webcore/JSWorker.cpp @@ -39,6 +39,7 @@ #include "JSDOMExceptionHandling.h" #include "JSDOMGlobalObjectInlines.h" #include "JSDOMOperation.h" +#include "JSDOMURL.h" #include "JSDOMWrapperCache.h" #include "JSEventListener.h" #include "NodeValidator.h" @@ -57,6 +58,8 @@ #include #include #include "SerializedScriptValue.h" +#include "BunProcess.h" +#include namespace WebCore { using namespace JSC; @@ -117,6 +120,7 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); auto* castedThis = jsCast(callFrame->jsCallee()); + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); ASSERT(castedThis); if (UNLIKELY(callFrame->argumentCount() < 1)) return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); @@ -124,11 +128,25 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: if (UNLIKELY(!context)) return throwConstructorScriptExecutionContextUnavailableError(*lexicalGlobalObject, throwScope, "Worker"_s); EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto scriptUrl = convert(*lexicalGlobalObject, argument0.value()); + String scriptUrl; + if (auto* domUrl = jsDynamicCast(argument0.value()); domUrl && domUrl->wrapped().href().isValid()) { + scriptUrl = domUrl->wrapped().href().string(); + } else if (argument0.value().isString()) { + scriptUrl = argument0.value().getString(lexicalGlobalObject); + } else { + return Bun::ERR::INVALID_ARG_TYPE(throwScope, lexicalGlobalObject, "filename"_s, "string or an instance of URL"_s, argument0.value()); + } RETURN_IF_EXCEPTION(throwScope, {}); EnsureStillAliveScope argument1 = callFrame->argument(1); + JSValue nodeWorkerObject {}; + if (callFrame->argumentCount() == 3) { + nodeWorkerObject = callFrame->argument(2); + } + RETURN_IF_EXCEPTION(throwScope, {}); auto options = WorkerOptions {}; + JSValue workerData = jsUndefined(); + Vector> transferList; if (JSObject* optionsObject = JSC::jsDynamicCast(argument1.value())) { if (auto nameValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, vm.propertyNames->name)) { @@ -137,6 +155,7 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: RETURN_IF_EXCEPTION(throwScope, {}); } } + RETURN_IF_EXCEPTION(throwScope, {}); if (auto miniModeValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "smol"_s))) { options.mini = miniModeValue.toBoolean(lexicalGlobalObject); @@ -177,52 +196,27 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: } } - auto workerData = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "workerData"_s)); + workerData = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "workerData"_s)); if (!workerData) { workerData = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "data"_s)); + if (!workerData) workerData = jsUndefined(); } + RETURN_IF_EXCEPTION(throwScope, {}); - if (workerData) { - Vector> ports; - Vector> transferList; - - if (JSValue transferListValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "transferList"_s))) { - if (transferListValue.isObject()) { - JSC::JSObject* transferListObject = transferListValue.getObject(); - if (auto* transferListArray = jsDynamicCast(transferListObject)) { - for (unsigned i = 0; i < transferListArray->length(); i++) { - JSC::JSValue transferListValue = transferListArray->get(lexicalGlobalObject, i); - if (transferListValue.isObject()) { - JSC::JSObject* transferListObject = transferListValue.getObject(); - transferList.append(JSC::Strong(vm, transferListObject)); - } + if (JSValue transferListValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "transferList"_s))) { + if (transferListValue.isObject()) { + JSC::JSObject* transferListObject = transferListValue.getObject(); + if (auto* transferListArray = jsDynamicCast(transferListObject)) { + JSC::forEachInArrayLike(globalObject, transferListArray, [&](JSValue transferValue) -> bool { + if (auto* transferObject = transferValue.getObject()) { + transferList.append({ vm, transferObject }); } - } - } - } - - ExceptionOr> serialized = SerializedScriptValue::create(*lexicalGlobalObject, workerData, WTFMove(transferList), ports, SerializationForStorage::No, SerializationContext::WorkerPostMessage); - if (serialized.hasException()) { - WebCore::propagateException(*lexicalGlobalObject, throwScope, serialized.releaseException()); - return encodedJSValue(); - } - - Vector transferredPorts; - - if (!ports.isEmpty()) { - auto disentangleResult = MessagePort::disentanglePorts(WTFMove(ports)); - if (disentangleResult.hasException()) { - WebCore::propagateException(*lexicalGlobalObject, throwScope, disentangleResult.releaseException()); - return encodedJSValue(); + return true; + }); } - transferredPorts = disentangleResult.releaseReturnValue(); } - - options.data = serialized.releaseReturnValue(); - options.dataMessagePorts = WTFMove(transferredPorts); } - auto* globalObject = jsCast(lexicalGlobalObject); auto envValue = optionsObject->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "env"_s)); RETURN_IF_EXCEPTION(throwScope, {}); // for now, we don't permit SHARE_ENV, because the behavior isn't implemented @@ -293,10 +287,39 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: RETURN_IF_EXCEPTION(scope, ); execArgv.append(str); }); + RETURN_IF_EXCEPTION(throwScope, {}); options.execArgv.emplace(WTFMove(execArgv)); } } + Vector> ports; + auto* valueToTransfer = constructEmptyArray(globalObject, nullptr, 2); + RETURN_IF_EXCEPTION(throwScope, {}); + valueToTransfer->putDirectIndex(globalObject, 0, workerData); + auto* environmentData = globalObject->nodeWorkerEnvironmentData(); + // If node:worker_threads has not been imported, environment data will not be set up yet. + valueToTransfer->putDirectIndex(globalObject, 1, environmentData ? environmentData : jsUndefined()); + + ExceptionOr> serialized = SerializedScriptValue::create(*lexicalGlobalObject, valueToTransfer, WTFMove(transferList), ports, SerializationForStorage::No, SerializationContext::WorkerPostMessage); + if (serialized.hasException()) { + WebCore::propagateException(*lexicalGlobalObject, throwScope, serialized.releaseException()); + return encodedJSValue(); + } + + Vector transferredPorts; + + if (!ports.isEmpty()) { + auto disentangleResult = MessagePort::disentanglePorts(WTFMove(ports)); + if (disentangleResult.hasException()) { + WebCore::propagateException(*lexicalGlobalObject, throwScope, disentangleResult.releaseException()); + return encodedJSValue(); + } + transferredPorts = disentangleResult.releaseReturnValue(); + } + + options.workerDataAndEnvironmentData = serialized.releaseReturnValue(); + options.dataMessagePorts = WTFMove(transferredPorts); + RETURN_IF_EXCEPTION(throwScope, {}); auto object = Worker::create(*context, WTFMove(scriptUrl), WTFMove(options)); if constexpr (IsExceptionOr) @@ -315,6 +338,16 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWorkerDOMConstructor:: setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, asObject(jsValue)); RETURN_IF_EXCEPTION(throwScope, {}); + // Emit the 'worker' event on the process. If we are constructing a `node:worker_threads` + // worker, we emit the event with the worker_threads Worker object instead of the Web Worker. + JSValue workerToEmit = jsValue; + if (nodeWorkerObject) { + workerToEmit = nodeWorkerObject; + } + auto* process = jsCast(globalObject->processObject()); + process->emitOnNextTick(globalObject, "worker"_s, workerToEmit); + RETURN_IF_EXCEPTION(throwScope, {}); + return JSValue::encode(jsValue); } JSC_ANNOTATE_HOST_FUNCTION(JSWorkerDOMConstructorConstruct, JSWorkerDOMConstructor::construct); @@ -341,11 +374,13 @@ JSC_DEFINE_CUSTOM_GETTER(jsWorker_threadIdGetter, (JSGlobalObject * lexicalGloba if (UNLIKELY(!castedThis)) return JSValue::encode(jsUndefined()); + auto& worker = castedThis->wrapped(); + if (worker.isClosingOrTerminated()) return JSValue::encode(jsNumber(-1)); // Main thread starts at 1 // // Note that we cannot use posix thread ids here because we don't know their thread id until the thread starts // - return JSValue::encode(jsNumber(castedThis->wrapped().clientIdentifier() - 1)); + return JSValue::encode(jsNumber(worker.clientIdentifier() - 1)); } /* Hash table for prototype */ @@ -686,4 +721,4 @@ Worker* JSWorker::toWrapped(JSC::VM&, JSC::JSValue value) return &wrapper->wrapped(); return nullptr; } -} +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 7e0a8662f576ce..36f0905ccc1246 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -5726,8 +5726,15 @@ ExceptionOr> SerializedScriptValue::create(JSGlobalOb #endif HashSet uniqueTransferables; for (auto& transferable : transferList) { - if (!uniqueTransferables.add(transferable.get()).isNewEntry) - return Exception { DataCloneError, "Duplicate transferable for structured clone"_s }; + if (!uniqueTransferables.add(transferable.get()).isNewEntry) { + if (toPossiblySharedArrayBuffer(vm, transferable.get())) { + return Exception { DataCloneError, "Transfer list contains duplicate ArrayBuffer"_s }; + } else if (JSMessagePort::toWrapped(vm, transferable.get())) { + return Exception { DataCloneError, "Transfer list contains duplicate MessagePort"_s }; + } else { + return Exception { DataCloneError, "Duplicate transferable for structured clone"_s }; + } + } if (auto arrayBuffer = toPossiblySharedArrayBuffer(vm, transferable.get())) { if (arrayBuffer->isDetached() || arrayBuffer->isShared()) diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp index d904af02662fc4..e08cb923d51256 100644 --- a/src/bun.js/bindings/webcore/Worker.cpp +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -29,6 +29,7 @@ // #include "ContentSecurityPolicy.h" // #include "DedicatedWorkerGlobalScope.h" +#include "ErrorCode.h" #include "ErrorEvent.h" #include "Event.h" #include "EventNames.h" @@ -68,7 +69,7 @@ namespace WebCore { WTF_MAKE_TZONE_ALLOCATED_IMPL(Worker); -extern "C" void WebWorker__requestTerminate( +extern "C" void WebWorker__notifyNeedTermination( void* worker); static Lock allWorkersLock; @@ -190,7 +191,7 @@ ExceptionOr> Worker::create(ScriptExecutionContext& context, const S .value_or(std::span {}); void* impl = WebWorker__create( worker.ptr(), - jsCast(context.jsGlobalObject())->bunVM(), + bunVM(context.jsGlobalObject()), nameStr, urlStr, &errorMessage, @@ -262,7 +263,7 @@ void Worker::terminate() { // m_contextProxy.terminateWorkerGlobalScope(); m_terminationFlags.fetch_or(TerminateRequestedFlag); - WebWorker__requestTerminate(impl_); + WebWorker__notifyNeedTermination(impl_); } // const char* Worker::activeDOMObjectName() const @@ -291,6 +292,11 @@ void Worker::terminate() // } // } +bool Worker::wasTerminated() const +{ + return m_terminationFlags & TerminatedFlag; +} + bool Worker::hasPendingActivity() const { auto onlineClosingFlags = m_onlineClosingFlags.load(); @@ -301,6 +307,11 @@ bool Worker::hasPendingActivity() const return !(m_terminationFlags & TerminatedFlag); } +bool Worker::isClosingOrTerminated() const +{ + return m_onlineClosingFlags & ClosingFlag; +} + void Worker::dispatchEvent(Event& event) { if (!m_terminationFlags) @@ -356,25 +367,28 @@ void Worker::dispatchOnline(Zig::GlobalObject* workerGlobalObject) } RELEASE_ASSERT(&thisContext->vm() == &workerGlobalObject->vm()); RELEASE_ASSERT(thisContext == workerGlobalObject->globalEventScope->scriptExecutionContext()); +} +void Worker::fireEarlyMessages(Zig::GlobalObject* workerGlobalObject) +{ + auto tasks = [&]() { + Locker lock(this->m_pendingTasksMutex); + return std::exchange(this->m_pendingTasks, {}); + }(); + auto* thisContext = workerGlobalObject->scriptExecutionContext(); if (workerGlobalObject->globalEventScope->hasActiveEventListeners(eventNames().messageEvent)) { - auto tasks = std::exchange(this->m_pendingTasks, {}); - lock.unlockEarly(); for (auto& task : tasks) { task(*thisContext); } } else { - auto tasks = std::exchange(this->m_pendingTasks, {}); - lock.unlockEarly(); - thisContext->postTask([tasks = WTFMove(tasks)](auto& ctx) mutable { for (auto& task : tasks) { task(ctx); } - tasks.clear(); }); } } + void Worker::dispatchError(WTF::String message) { @@ -454,6 +468,11 @@ extern "C" void WebWorker__dispatchOnline(Worker* worker, Zig::GlobalObject* glo worker->dispatchOnline(globalObject); } +extern "C" void WebWorker__fireEarlyMessages(Worker* worker, Zig::GlobalObject* globalObject) +{ + worker->fireEarlyMessages(globalObject); +} + extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker* worker, BunString message, JSC::EncodedJSValue errorValue) { JSValue error = JSC::JSValue::decode(errorValue); @@ -467,7 +486,7 @@ extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker worker->dispatchError(message.toWTFString(BunString::ZeroCopy)); } -extern "C" WebCore::Worker* WebWorker__getParentWorker(void*); +extern "C" WebCore::Worker* WebWorker__getParentWorker(void* bunVM); JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { @@ -482,8 +501,7 @@ JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobal auto port = callFrame->argument(0); if (!port.isObject()) { - throwTypeError(lexicalGlobalObject, scope, "the \"port\" argument must be a MessagePort instance"_s); - return {}; + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, "The \"port\" argument must be a MessagePort instance"_s); } if (auto* messagePort = jsDynamicCast(port)) { @@ -493,8 +511,7 @@ JSC_DEFINE_HOST_FUNCTION(jsReceiveMessageOnPort, (JSGlobalObject * lexicalGlobal return JSC::JSValue::encode(jsUndefined()); } - throwTypeError(lexicalGlobalObject, scope, "the \"port\" argument must be a MessagePort instance"_s); - return {}; + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, "The \"port\" argument must be a MessagePort instance"_s); } JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject) @@ -504,27 +521,41 @@ JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject) auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); JSValue workerData = jsNull(); JSValue threadId = jsNumber(0); + JSMap* environmentData = nullptr; if (auto* worker = WebWorker__getParentWorker(globalObject->bunVM())) { auto& options = worker->options(); - if (worker && options.data) { - auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.dataMessagePorts)); - RefPtr serialized = WTFMove(options.data); - JSValue deserialized = serialized->deserialize(*globalObject, globalObject, WTFMove(ports)); - RETURN_IF_EXCEPTION(scope, {}); - workerData = deserialized; - } + auto ports = MessagePort::entanglePorts(*ScriptExecutionContext::getScriptExecutionContext(worker->clientIdentifier()), WTFMove(options.dataMessagePorts)); + RefPtr serialized = WTFMove(options.workerDataAndEnvironmentData); + JSValue deserialized = serialized->deserialize(*globalObject, globalObject, WTFMove(ports)); + RETURN_IF_EXCEPTION(scope, {}); + // Should always be set to an Array of length 2 in the constructor in JSWorker.cpp + auto* pair = jsCast(deserialized); + ASSERT(pair->length() == 2); + ASSERT(pair->canGetIndexQuickly(0u)); + ASSERT(pair->canGetIndexQuickly(1u)); + workerData = pair->getIndexQuickly(0); + RETURN_IF_EXCEPTION(scope, {}); + // it might not be a Map if the parent had not set up environmentData yet + environmentData = jsDynamicCast(pair->getIndexQuickly(1)); + RETURN_IF_EXCEPTION(scope, {}); // Main thread starts at 1 threadId = jsNumber(worker->clientIdentifier() - 1); } + if (!environmentData) { + environmentData = JSMap::create(vm, globalObject->mapStructure()); + RETURN_IF_EXCEPTION(scope, {}); + } + ASSERT(environmentData); + globalObject->setNodeWorkerEnvironmentData(environmentData); - JSObject* array = constructEmptyObject(globalObject, globalObject->objectPrototype(), 3); - + JSObject* array = constructEmptyArray(globalObject, nullptr, 4); + RETURN_IF_EXCEPTION(scope, {}); array->putDirectIndex(globalObject, 0, workerData); array->putDirectIndex(globalObject, 1, threadId); array->putDirectIndex(globalObject, 2, JSFunction::create(vm, globalObject, 1, "receiveMessageOnPort"_s, jsReceiveMessageOnPort, ImplementationVisibility::Public, NoIntrinsic)); - + array->putDirectIndex(globalObject, 3, environmentData); return array; } diff --git a/src/bun.js/bindings/webcore/Worker.h b/src/bun.js/bindings/webcore/Worker.h index 1c1c9924720a36..1aec9a7c95417d 100644 --- a/src/bun.js/bindings/webcore/Worker.h +++ b/src/bun.js/bindings/webcore/Worker.h @@ -68,8 +68,9 @@ class Worker final : public ThreadSafeRefCounted, public EventTargetWith using ThreadSafeRefCounted::ref; void terminate(); - bool wasTerminated() const { return m_terminationFlags & TerminatedFlag; } + bool wasTerminated() const; bool hasPendingActivity() const; + bool isClosingOrTerminated() const; bool updatePtr(); String identifier() const { return m_identifier; } @@ -94,6 +95,8 @@ class Worker final : public ThreadSafeRefCounted, public EventTargetWith void drainEvents(); void dispatchOnline(Zig::GlobalObject* workerGlobalObject); + // Fire a 'message' event in the Worker for messages that were sent before the Worker started running + void fireEarlyMessages(Zig::GlobalObject* workerGlobalObject); void dispatchError(WTF::String message); void dispatchExit(int32_t exitCode); ScriptExecutionContext* scriptExecutionContext() const final { return ContextDestructionObserver::scriptExecutionContext(); } diff --git a/src/bun.js/bindings/webcore/WorkerOptions.h b/src/bun.js/bindings/webcore/WorkerOptions.h index 43249a3466a18c..f01d1bf5704e9a 100644 --- a/src/bun.js/bindings/webcore/WorkerOptions.h +++ b/src/bun.js/bindings/webcore/WorkerOptions.h @@ -16,7 +16,10 @@ struct WorkerOptions { // true, then we need to make sure that `process.argv` contains "[worker eval]" instead of the // Blob URL. bool evalMode { false }; - RefPtr data; + // Serialized array containing [workerData, environmentData] + // (environmentData is always a Map) + RefPtr workerDataAndEnvironmentData; + // Objects transferred for either data or environmentData in the transferList Vector dataMessagePorts; Vector preloadModules; std::optional> env; // TODO(@190n) allow shared diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index 5a6a1b52267916..109812fc800c96 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -110,7 +110,6 @@ export default [ noConstructor: true, finalize: true, configurable: false, - hasPendingActivity: true, klass: {}, JSType: "0b11101110", proto: { diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index a7246adb3a4756..ee888f340333ec 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -45,12 +45,25 @@ pub const StatWatcherScheduler = struct { .tag = .StatWatcherScheduler, }, + ref_count: RefCount, + + const RefCount = bun.ptr.ThreadSafeRefCount(StatWatcherScheduler, "ref_count", deinit, .{ .debug_name = "StatWatcherScheduler" }); + pub const ref = RefCount.ref; + pub const deref = RefCount.deref; + const WatcherQueue = UnboundedQueue(StatWatcher, .next); - pub fn init(allocator: std.mem.Allocator, vm: *bun.JSC.VirtualMachine) *StatWatcherScheduler { - const this = allocator.create(StatWatcherScheduler) catch bun.outOfMemory(); - this.* = .{ .main_thread = std.Thread.getCurrentId(), .vm = vm }; - return this; + pub fn init(vm: *bun.JSC.VirtualMachine) bun.ptr.RefPtr(StatWatcherScheduler) { + return .new(.{ + .ref_count = .init(), + .main_thread = std.Thread.getCurrentId(), + .vm = vm, + }); + } + + fn deinit(this: *StatWatcherScheduler) void { + bun.assertf(this.watchers.count == 0, "destroying StatWatcherScheduler while it still has {} watchers", .{this.watchers.count}); + bun.destroy(this); } pub fn append(this: *StatWatcherScheduler, watcher: *StatWatcher) void { @@ -58,7 +71,9 @@ pub const StatWatcherScheduler = struct { bun.assert(watcher.closed == false); bun.assert(watcher.next == null); + watcher.ref(); this.watchers.push(watcher); + log("push watcher {x} -> {d} watchers", .{ @intFromPtr(watcher), this.watchers.count }); const current = this.getInterval(); if (current == 0 or current > watcher.interval) { // we are not running or the new watcher has a smaller interval @@ -72,6 +87,7 @@ pub const StatWatcherScheduler = struct { /// Update the current interval and set the timer (this function is thread safe) fn setInterval(this: *StatWatcherScheduler, interval: i32) void { + this.ref(); this.current_interval.store(interval, .monotonic); if (this.main_thread == std.Thread.getCurrentId()) { @@ -135,17 +151,20 @@ pub const StatWatcherScheduler = struct { pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { var this: *StatWatcherScheduler = @alignCast(@fieldParentPtr("task", task)); + // ref'd when the timer was scheduled + defer this.deref(); // Instant.now will not fail on our target platforms. const now = std.time.Instant.now() catch unreachable; var batch = this.watchers.popBatch(); + log("pop batch of {d} -> {d} watchers", .{ batch.count, this.watchers.count }); var iter = batch.iterator(); var min_interval: i32 = std.math.maxInt(i32); var closest_next_check: u64 = @intCast(min_interval); var contain_watchers = false; while (iter.next()) |watcher| { if (watcher.closed) { - watcher.used_by_scheduler_thread.store(false, .release); + watcher.deref(); continue; } contain_watchers = true; @@ -161,6 +180,7 @@ pub const StatWatcherScheduler = struct { } min_interval = @min(min_interval, watcher.interval); this.watchers.push(watcher); + log("reinsert {x} -> {d} watchers", .{ @intFromPtr(watcher), this.watchers.count }); } if (contain_watchers) { @@ -181,11 +201,10 @@ pub const StatWatcher = struct { ctx: *VirtualMachine, - /// Closed is set to true to tell the scheduler to remove from list and mark `used_by_scheduler_thread` as false. - closed: bool, - /// When this is marked `false` this StatWatcher can get freed - used_by_scheduler_thread: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + ref_count: RefCount, + /// Closed is set to true to tell the scheduler to remove from list and deref. + closed: bool, path: [:0]u8, persistent: bool, bigint: bool, @@ -200,6 +219,12 @@ pub const StatWatcher = struct { last_stat: bun.Stat, last_jsvalue: JSC.Strong.Optional, + scheduler: bun.ptr.RefPtr(StatWatcherScheduler), + + const RefCount = bun.ptr.ThreadSafeRefCount(StatWatcher, "ref_count", deinit, .{ .debug_name = "StatWatcher" }); + pub const ref = RefCount.ref; + pub const deref = RefCount.deref; + pub const js = JSC.Codegen.JSStatWatcher; pub const toJS = js.toJS; pub const fromJS = js.fromJS; @@ -214,8 +239,7 @@ pub const StatWatcher = struct { } pub fn deinit(this: *StatWatcher) void { - log("deinit\n", .{}); - bun.assert(!this.hasPendingActivity()); + log("deinit {x}", .{@intFromPtr(this)}); if (this.persistent) { this.persistent = false; @@ -311,10 +335,6 @@ pub const StatWatcher = struct { return .undefined; } - pub fn hasPendingActivity(this: *StatWatcher) bool { - return this.used_by_scheduler_thread.load(.acquire); - } - /// Stops file watching but does not free the instance. pub fn close(this: *StatWatcher) void { if (this.persistent) { @@ -333,7 +353,9 @@ pub const StatWatcher = struct { /// If the scheduler is not using this, free instantly, otherwise mark for being freed. pub fn finalize(this: *StatWatcher) void { log("Finalize\n", .{}); - this.deinit(); + this.closed = true; + this.scheduler.deref(); + this.deref(); // but don't deinit until the scheduler drops its reference } pub const InitialStatTask = struct { @@ -351,7 +373,6 @@ pub const StatWatcher = struct { const this = initial_stat_task.watcher; if (this.closed) { - this.used_by_scheduler_thread.store(false, .release); return; } @@ -374,28 +395,23 @@ pub const StatWatcher = struct { pub fn initialStatSuccessOnMainThread(this: *StatWatcher) void { if (this.closed) { - this.used_by_scheduler_thread.store(false, .release); return; } const jsvalue = statToJSStats(this.globalThis, &this.last_stat, this.bigint); this.last_jsvalue = .create(jsvalue, this.globalThis); - const vm = this.globalThis.bunVM(); - vm.rareData().nodeFSStatWatcherScheduler(vm).append(this); + this.scheduler.data.append(this); } pub fn initialStatErrorOnMainThread(this: *StatWatcher) void { if (this.closed) { - this.used_by_scheduler_thread.store(false, .release); return; } const jsvalue = statToJSStats(this.globalThis, &this.last_stat, this.bigint); this.last_jsvalue = .create(jsvalue, this.globalThis); - const vm = this.globalThis.bunVM(); - _ = js.listenerGetCached(this.js_this).?.call( this.globalThis, .undefined, @@ -406,10 +422,9 @@ pub const StatWatcher = struct { ) catch |err| this.globalThis.reportActiveExceptionAsUnhandled(err); if (this.closed) { - this.used_by_scheduler_thread.store(false, .release); return; } - vm.rareData().nodeFSStatWatcherScheduler(vm).append(this); + this.scheduler.data.append(this); } /// Called from any thread @@ -476,12 +491,13 @@ pub const StatWatcher = struct { .js_this = .zero, .closed = false, .path = alloc_file_path, - .used_by_scheduler_thread = std.atomic.Value(bool).init(true), // Instant.now will not fail on our target platforms. .last_check = std.time.Instant.now() catch unreachable, // InitStatTask is responsible for setting this .last_stat = std.mem.zeroes(bun.Stat), .last_jsvalue = .empty, + .scheduler = vm.rareData().nodeFSStatWatcherScheduler(vm), + .ref_count = .init(), }; errdefer this.deinit(); diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index 1be6cd62ec22b2..90655438025186 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -255,15 +255,15 @@ fn setCwd_(globalObject: *JSC.JSGlobalObject, to: *JSC.ZigString) bun.JSError!JS // TODO(@190n) this may need to be noreturn pub fn exit(globalObject: *JSC.JSGlobalObject, code: u8) callconv(.c) void { var vm = globalObject.bunVM(); + vm.exit_handler.exit_code = code; if (vm.worker) |worker| { - vm.exit_handler.exit_code = code; - worker.requestTerminate(); - return; + // TODO(@190n) we may need to use requestTerminate or throwTerminationException + // instead to terminate the worker sooner + worker.exit(); + } else { + vm.onExit(); + vm.globalExit(); } - - vm.exit_handler.exit_code = code; - vm.onExit(); - vm.globalExit(); } // TODO: switch this to using *bun.wtf.String when it is added diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 580fbf21ba675f..ca0f4d46ae88e8 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -41,7 +41,7 @@ spawn_ipc_usockets_context: ?*uws.SocketContext = null, mime_types: ?bun.http.MimeType.Map = null, -node_fs_stat_watcher_scheduler: ?*StatWatcherScheduler = null, +node_fs_stat_watcher_scheduler: ?bun.ptr.RefPtr(StatWatcherScheduler) = null, listening_sockets_for_watch_mode: std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, listening_sockets_for_watch_mode_lock: bun.Mutex = .{}, @@ -453,11 +453,11 @@ pub fn globalDNSResolver(rare: *RareData, vm: *JSC.VirtualMachine) *api.DNS.DNSR return &rare.global_dns_data.?.resolver; } -pub fn nodeFSStatWatcherScheduler(rare: *RareData, vm: *JSC.VirtualMachine) *StatWatcherScheduler { - return rare.node_fs_stat_watcher_scheduler orelse { - rare.node_fs_stat_watcher_scheduler = StatWatcherScheduler.init(vm.allocator, vm); - return rare.node_fs_stat_watcher_scheduler.?; - }; +pub fn nodeFSStatWatcherScheduler(rare: *RareData, vm: *JSC.VirtualMachine) bun.ptr.RefPtr(StatWatcherScheduler) { + return (rare.node_fs_stat_watcher_scheduler orelse init: { + rare.node_fs_stat_watcher_scheduler = StatWatcherScheduler.init(vm); + break :init rare.node_fs_stat_watcher_scheduler.?; + }).dupeRef(); } pub fn s3DefaultClient(rare: *RareData, globalThis: *JSC.JSGlobalObject) JSC.JSValue { diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 18b54d5dbd6f6d..5c20ea12119dce 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -1560,6 +1560,7 @@ pub const TestRunnerTask = struct { } }; + this.globalThis.clearTerminationException(); _ = vm.uncaughtException(this.globalThis, err, true); } diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 8ec6ea59e75e20..d98cdb4dfbfc3f 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -87,8 +87,10 @@ pub export fn Bun__reportUnhandledError(globalObject: *JSGlobalObject, value: JS JSC.markBinding(@src()); // This JSGlobalObject might not be the main script execution context // See the crash in https://github.com/oven-sh/bun/issues/9778 - const jsc_vm = JSC.VirtualMachine.get(); - _ = jsc_vm.uncaughtException(globalObject, value, false); + const vm = JSC.VirtualMachine.get(); + if (!value.isTerminationException(vm.jsc)) { + _ = vm.uncaughtException(globalObject, value, false); + } return .undefined; } diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index 62ffc57bfbe482..2f1e127d43885a 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -7,21 +7,19 @@ const JSValue = JSC.JSValue; const Async = bun.Async; const WTFStringImpl = @import("../string.zig").WTFStringImpl; -const Bool = std.atomic.Value(bool); - /// Shared implementation of Web and Node `Worker` pub const WebWorker = struct { /// null when haven't started yet vm: ?*JSC.VirtualMachine = null, - status: std.atomic.Value(Status) = std.atomic.Value(Status).init(.start), + status: std.atomic.Value(Status) = .init(.start), /// To prevent UAF, the `spin` function (aka the worker's event loop) will call deinit once this is set and properly exit the loop. - requested_terminate: Bool = Bool.init(false), + requested_terminate: std.atomic.Value(bool) = .init(false), execution_context_id: u32 = 0, parent_context_id: u32 = 0, parent: *JSC.VirtualMachine, - /// Already resolved. - specifier: []const u8 = "", + /// To be resolved on the Worker thread at startup, in spin(). + unresolved_specifier: []const u8, preloads: [][]const u8 = &.{}, store_fd: bool = false, arena: ?bun.MimallocArena = null, @@ -44,6 +42,9 @@ pub const WebWorker = struct { argv: []const WTFStringImpl, execArgv: ?[]const WTFStringImpl, + /// Used to distinguish between terminate() called by exit(), and terminate() called for other reasons + exit_called: bool = false, + pub const Status = enum(u8) { start, starting, @@ -52,7 +53,8 @@ pub const WebWorker = struct { }; extern fn WebWorker__dispatchExit(?*JSC.JSGlobalObject, *anyopaque, i32) void; - extern fn WebWorker__dispatchOnline(this: *anyopaque, *JSC.JSGlobalObject) void; + extern fn WebWorker__dispatchOnline(cpp_worker: *anyopaque, *JSC.JSGlobalObject) void; + extern fn WebWorker__fireEarlyMessages(cpp_worker: *anyopaque, *JSC.JSGlobalObject) void; extern fn WebWorker__dispatchError(*JSC.JSGlobalObject, *anyopaque, bun.String, JSValue) void; export fn WebWorker__getParentWorker(vm: *JSC.VirtualMachine) ?*anyopaque { @@ -205,10 +207,6 @@ pub const WebWorker = struct { 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; - }; - var preloads = std.ArrayList([]const u8).initCapacity(bun.default_allocator, preload_modules_len) catch bun.outOfMemory(); for (preload_modules) |module| { const utf8_slice = module.toUTF8(bun.default_allocator); @@ -234,7 +232,7 @@ pub const WebWorker = struct { .execution_context_id = this_context_id, .mini = mini, .eval_mode = eval_mode, - .specifier = bun.default_allocator.dupe(u8, path) catch bun.outOfMemory(), + .unresolved_specifier = (spec_slice.toOwned(bun.default_allocator) catch bun.outOfMemory()).slice(), .store_fd = parent.transpiler.resolver.store_fd, .name = brk: { if (!name_str.isEmpty()) { @@ -324,7 +322,7 @@ pub const WebWorker = struct { fn deinit(this: *WebWorker) void { log("[{d}] deinit", .{this.execution_context_id}); this.parent_poll_ref.unrefConcurrently(this.parent); - bun.default_allocator.free(this.specifier); + bun.default_allocator.free(this.unresolved_specifier); for (this.preloads) |preload| { bun.default_allocator.free(preload); } @@ -409,7 +407,32 @@ pub const WebWorker = struct { assert(this.status.load(.acquire) == .start); this.setStatus(.starting); vm.preload = this.preloads; - var promise = vm.loadEntryPointForWebWorker(this.specifier) catch { + // resolve entrypoint + var resolve_error = bun.String.empty; + defer resolve_error.deref(); + const path = resolveEntryPointSpecifier(vm, this.unresolved_specifier, &resolve_error, vm.log) orelse { + vm.exit_handler.exit_code = 1; + if (vm.log.errors == 0 and !resolve_error.isEmpty()) { + const err = resolve_error.toUTF8(bun.default_allocator); + defer err.deinit(); + vm.log.addError(null, .Empty, err.slice()) catch bun.outOfMemory(); + } + this.flushLogs(); + this.exitAndDeinit(); + return; + }; + defer bun.default_allocator.free(path); + + // If the worker is terminated before we even try to run any code, the exit code should be 0 + if (this.hasRequestedTerminate()) { + this.flushLogs(); + this.exitAndDeinit(); + return; + } + + var promise = vm.loadEntryPointForWebWorker(path) catch { + // If we called process.exit(), don't override the exit code + if (!this.exit_called) vm.exit_handler.exit_code = 1; this.flushLogs(); this.exitAndDeinit(); return; @@ -429,7 +452,10 @@ pub const WebWorker = struct { this.flushLogs(); log("[{d}] event loop start", .{this.execution_context_id}); + // TODO(@190n) call dispatchOnline earlier (basically as soon as spin() starts, before + // we start running JS) WebWorker__dispatchOnline(this.cpp_worker, vm.global); + WebWorker__fireEarlyMessages(this.cpp_worker, vm.global); this.setStatus(.running); // don't run the GC if we don't actually need to @@ -481,28 +507,34 @@ pub const WebWorker = struct { } } - /// Request a terminate (Called from main thread from worker.terminate(), or inside worker in process.exit()) - /// The termination will actually happen after the next tick of the worker's loop. - pub fn requestTerminate(this: *WebWorker) callconv(.C) void { - // TODO(@heimskr): make WebWorker termination more immediate. Currently, console.log after process.exit will go through if in a WebWorker. + /// Implement process.exit(). May only be called from the Worker thread. + pub fn exit(this: *WebWorker) void { + this.exit_called = true; + this.notifyNeedTermination(); + } + + /// Request a terminate from any thread. + pub fn notifyNeedTermination(this: *WebWorker) callconv(.C) void { if (this.status.load(.acquire) == .terminated) { return; } if (this.setRequestedTerminate()) { return; } - log("[{d}] requestTerminate", .{this.execution_context_id}); + log("[{d}] notifyNeedTermination", .{this.execution_context_id}); if (this.vm) |vm| { vm.eventLoop().wakeup(); + // TODO(@190n) notifyNeedTermination } + // TODO(@190n) delete this.setRefInternal(false); } /// This handles cleanup, emitting the "close" event, and deinit. /// Only call after the VM is initialized AND on the same thread as the worker. - /// Otherwise, call `requestTerminate` to cause the event loop to safely terminate after the next tick. + /// Otherwise, call `notifyNeedTermination` to cause the event loop to safely terminate. pub fn exitAndDeinit(this: *WebWorker) noreturn { JSC.markBinding(@src()); this.setStatus(.terminated); @@ -546,7 +578,7 @@ pub const WebWorker = struct { comptime { @export(&create, .{ .name = "WebWorker__create" }); - @export(&requestTerminate, .{ .name = "WebWorker__requestTerminate" }); + @export(¬ifyNeedTermination, .{ .name = "WebWorker__notifyNeedTermination" }); @export(&setRef, .{ .name = "WebWorker__setRef" }); _ = WebWorker__updatePtr; } diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index e128398d2632b3..55e645a20bb5a7 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -35,6 +35,7 @@ pub const FetchHeaders = @import("bindings/FetchHeaders.zig").FetchHeaders; pub const ByteBlobLoader = @import("webcore/ByteBlobLoader.zig"); pub const ByteStream = @import("webcore/ByteStream.zig"); pub const FileReader = @import("webcore/FileReader.zig"); +pub const ScriptExecutionContext = @import("webcore/ScriptExecutionContext.zig"); pub const streams = @import("webcore/streams.zig"); pub const NetworkSink = streams.NetworkSink; diff --git a/src/bun.js/webcore/ScriptExecutionContext.zig b/src/bun.js/webcore/ScriptExecutionContext.zig new file mode 100644 index 00000000000000..2a3a7096953adb --- /dev/null +++ b/src/bun.js/webcore/ScriptExecutionContext.zig @@ -0,0 +1,20 @@ +const bun = @import("bun"); + +extern fn ScriptExecutionContextIdentifier__getGlobalObject(id: u32) ?*bun.jsc.JSGlobalObject; + +/// Safe handle to a JavaScript execution environment that may have exited. +/// Obtain with global_object.scriptExecutionContextIdentifier() +pub const Identifier = enum(u32) { + _, + + /// Returns null if the context referred to by `self` no longer exists + pub fn globalObject(self: Identifier) ?*bun.jsc.JSGlobalObject { + return ScriptExecutionContextIdentifier__getGlobalObject(@intFromEnum(self)); + } + + /// Returns null if the context referred to by `self` no longer exists + pub fn bunVM(self: Identifier) ?*bun.jsc.VirtualMachine { + // concurrently because we expect these identifiers are mostly used by off-thread tasks + return (self.globalObject() orelse return null).bunVMConcurrently(); + } +}; diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts index 59cb63c6773eac..02faea4130a88e 100644 --- a/src/js/node/worker_threads.ts +++ b/src/js/node/worker_threads.ts @@ -7,11 +7,30 @@ const EventEmitter = require("node:events"); const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared"); const { validateObject, validateBoolean } = require("internal/validators"); -const { MessageChannel, BroadcastChannel, Worker: WebWorker } = globalThis; +const { + MessageChannel, + BroadcastChannel, + Worker: WebWorker, +} = globalThis as typeof globalThis & { + // The Worker constructor secretly takes an extra parameter to provide the node:worker_threads + // instance. This is so that it can emit the `worker` event on the process with the + // node:worker_threads instance instead of the Web Worker instance. + Worker: new (...args: [...ConstructorParameters, nodeWorker: Worker]) => WebWorker; +}; const SHARE_ENV = Symbol("nodejs.worker_threads.SHARE_ENV"); const isMainThread = Bun.isMainThread; -const { 0: _workerData, 1: _threadId, 2: _receiveMessageOnPort } = $cpp("Worker.cpp", "createNodeWorkerThreadsBinding"); +const { + 0: _workerData, + 1: _threadId, + 2: _receiveMessageOnPort, + 3: environmentData, +} = $cpp("Worker.cpp", "createNodeWorkerThreadsBinding") as [ + unknown, + number, + (port: unknown) => unknown, + Map, +]; type NodeWorkerOptions = import("node:worker_threads").WorkerOptions; @@ -184,12 +203,16 @@ function fakeParentPort() { } let parentPort: MessagePort | null = isMainThread ? null : fakeParentPort(); -function getEnvironmentData() { - return process.env; +function getEnvironmentData(key: unknown): unknown { + return environmentData.get(key); } -function setEnvironmentData(env: any) { - process.env = env; +function setEnvironmentData(key: unknown, value: unknown): void { + if (value === undefined) { + environmentData.delete(key); + } else { + environmentData.set(key, value); + } } function markAsUntransferable() { @@ -234,7 +257,7 @@ class Worker extends EventEmitter { } } try { - this.#worker = new WebWorker(filename, options); + this.#worker = new WebWorker(filename, options as Bun.WorkerOptions, this); } catch (e) { if (this.#urlToRevoke) { URL.revokeObjectURL(this.#urlToRevoke); diff --git a/src/ptr/ref_count.zig b/src/ptr/ref_count.zig index 84ca5469be64c8..48a117b2dfe6fb 100644 --- a/src/ptr/ref_count.zig +++ b/src/ptr/ref_count.zig @@ -229,30 +229,30 @@ pub fn ThreadSafeRefCount(T: type, field_name: []const u8, destructor: fn (*T) v pub fn ref(self: *T) void { const counter = getCounter(self); if (enable_debug) counter.debug.assertValid(); - const new_count = counter.active_counts.fetchAdd(1, .seq_cst); + const old_count = counter.active_counts.fetchAdd(1, .seq_cst); if (comptime bun.Environment.enable_logs) { scope.log("0x{x} ref {d} -> {d}", .{ @intFromPtr(self), - new_count - 1, - new_count, + old_count, + old_count + 1, }); } - bun.debugAssert(new_count > 0); + bun.debugAssert(old_count > 0); } pub fn deref(self: *T) void { const counter = getCounter(self); if (enable_debug) counter.debug.assertValid(); - const new_count = counter.active_counts.fetchSub(1, .seq_cst); + const old_count = counter.active_counts.fetchSub(1, .seq_cst); if (comptime bun.Environment.enable_logs) { scope.log("0x{x} deref {d} -> {d}", .{ @intFromPtr(self), - new_count + 1, - new_count, + old_count, + old_count - 1, }); } - bun.debugAssert(new_count > 0); - if (new_count == 1) { + bun.debugAssert(old_count > 0); + if (old_count == 1) { if (enable_debug) { counter.debug.deinit(std.mem.asBytes(self), @returnAddress()); } @@ -275,7 +275,7 @@ pub fn ThreadSafeRefCount(T: type, field_name: []const u8, destructor: fn (*T) v pub fn dumpActiveRefs(count: *@This()) void { if (enable_debug) { - const ptr: *T = @fieldParentPtr(field_name, count); + const ptr: *T = @alignCast(@fieldParentPtr(field_name, count)); count.debug.dump(@typeName(T), ptr, count.active_counts.load(.seq_cst)); } } diff --git a/test/cli/test/test-timeout-behavior.test.ts b/test/cli/test/test-timeout-behavior.test.ts index f1ebe37943e0f5..c7188fe6330954 100644 --- a/test/cli/test/test-timeout-behavior.test.ts +++ b/test/cli/test/test-timeout-behavior.test.ts @@ -18,14 +18,18 @@ if (isFlaky && isLinux) { env: bunEnv, }); const [out, err, exitCode] = await Promise.all([new Response(stdout).text(), new Response(stderr).text(), exited]); - console.log(out); - console.log(err); + // merge outputs so that this test still works if we change which things are printed to stdout + // and which to stderr + const combined = out + err; // exit code should indicate failed tests, not abort or anything expect(exitCode).toBe(1); - expect(out).not.toContain("This should not be printed!"); - expect(err).toContain("killed 1 dangling process"); + expect(combined).not.toContain("This should not be printed!"); + expect(combined).toContain("killed 1 dangling process"); + // we should not expose the termination exception + expect(combined).not.toContain("Unhandled error between tests"); + expect(combined).not.toContain("JavaScript execution terminated"); // both tests should have run with the expected result - expect(err).toContain("(fail) test timeout kills dangling processes"); - expect(err).toContain("(pass) slow test after test timeout"); + expect(combined).toContain("(fail) test timeout kills dangling processes"); + expect(combined).toContain("(pass) slow test after test timeout"); }); } diff --git a/test/js/bun/util/main-worker-file.js b/test/js/bun/util/main-worker-file.js index 2d5c3aedde5ab0..3764803978776e 100644 --- a/test/js/bun/util/main-worker-file.js +++ b/test/js/bun/util/main-worker-file.js @@ -11,6 +11,4 @@ if (isMainThread) { }); await promise; - - worker.terminate(); } diff --git a/test/js/node/test/parallel/test-worker-environmentdata.js b/test/js/node/test/parallel/test-worker-environmentdata.js new file mode 100644 index 00000000000000..5943666d060b36 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-environmentdata.js @@ -0,0 +1,39 @@ +'use strict'; +// Flags: --expose-internals + +require('../common'); +const { + Worker, + getEnvironmentData, + setEnvironmentData, + threadId, +} = require('worker_threads'); + +// BUN: skip using this internal module, it doesn't actually affect behavior of the test +// const { assignEnvironmentData } = require('internal/worker'); + +const { + deepStrictEqual, + strictEqual, +} = require('assert'); + +if (!process.env.HAS_STARTED_WORKER) { + process.env.HAS_STARTED_WORKER = 1; + setEnvironmentData('foo', 'bar'); + setEnvironmentData('hello', { value: 'world' }); + setEnvironmentData(1, 2); + strictEqual(getEnvironmentData(1), 2); + setEnvironmentData(1); // Delete it, key won't show up in the worker. + new Worker(__filename); + setEnvironmentData('hello'); // Delete it. Has no impact on the worker. +} else { + strictEqual(getEnvironmentData('foo'), 'bar'); + deepStrictEqual(getEnvironmentData('hello'), { value: 'world' }); + strictEqual(getEnvironmentData(1), undefined); + // assignEnvironmentData(undefined); // It won't setup any key. + strictEqual(getEnvironmentData(undefined), undefined); + + // Recurse to make sure the environment data is inherited + if (threadId <= 2) + new Worker(__filename); +} diff --git a/test/js/node/test/parallel/test-worker-esm-missing-main.js b/test/js/node/test/parallel/test-worker-esm-missing-main.js new file mode 100644 index 00000000000000..dbcb050b77c061 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-esm-missing-main.js @@ -0,0 +1,16 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const missing = tmpdir.resolve('does-not-exist.js'); + +const worker = new Worker(missing); + +worker.on('error', common.mustCall((err) => { + // eslint-disable-next-line node-core/no-unescaped-regexp-dot + // BUN: this error comes from our bundler where it'd be impractical to rewrite all the errors to match Node + assert.match(err.message, /(Cannot find module|ModuleNotFound) .+does-not-exist.js/); +})); diff --git a/test/js/node/test/parallel/test-worker-event.js b/test/js/node/test/parallel/test-worker-event.js new file mode 100644 index 00000000000000..01e95ead8316cb --- /dev/null +++ b/test/js/node/test/parallel/test-worker-event.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + Worker, + threadId: parentThreadId, +} = require('worker_threads'); + +process.on('worker', common.mustCall(({ threadId }) => { + assert.strictEqual(threadId, parentThreadId + 1); +})); + +new Worker('', { eval: true }); diff --git a/test/js/node/test/parallel/test-worker-message-port-receive-message.js b/test/js/node/test/parallel/test-worker-message-port-receive-message.js new file mode 100644 index 00000000000000..bafcd3f7a7042f --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-port-receive-message.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { MessageChannel, receiveMessageOnPort } = require('worker_threads'); + +const { port1, port2 } = new MessageChannel(); + +const message1 = { hello: 'world' }; +const message2 = { foo: 'bar' }; + +// Make sure receiveMessageOnPort() works in a FIFO way, the same way it does +// when we’re using events. +assert.strictEqual(receiveMessageOnPort(port2), undefined); +port1.postMessage(message1); +port1.postMessage(message2); +assert.deepStrictEqual(receiveMessageOnPort(port2), { message: message1 }); +assert.deepStrictEqual(receiveMessageOnPort(port2), { message: message2 }); +assert.strictEqual(receiveMessageOnPort(port2), undefined); +assert.strictEqual(receiveMessageOnPort(port2), undefined); + +// Make sure message handlers aren’t called. +port2.on('message', common.mustNotCall()); +port1.postMessage(message1); +assert.deepStrictEqual(receiveMessageOnPort(port2), { message: message1 }); +port1.close(); + +for (const value of [null, 0, -1, {}, []]) { + assert.throws(() => receiveMessageOnPort(value), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "port" argument must be a MessagePort instance' + }); +} diff --git a/test/js/node/test/parallel/test-worker-message-port-transfer-duplicate.js b/test/js/node/test/parallel/test-worker-message-port-transfer-duplicate.js new file mode 100644 index 00000000000000..ad0a2d8aca1f01 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-message-port-transfer-duplicate.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { MessageChannel } = require('worker_threads'); + +// Test that passing duplicate transferables in the transfer list throws +// DataCloneError exceptions. + +{ + const { port1, port2 } = new MessageChannel(); + port2.once('message', common.mustNotCall()); + + const port3 = new MessageChannel().port1; + assert.throws(() => { + port1.postMessage(port3, [port3, port3]); + }, /^DataCloneError: Transfer list contains duplicate MessagePort$/); + port1.close(); +} + +{ + const { port1, port2 } = new MessageChannel(); + port2.once('message', common.mustNotCall()); + + const buf = new Uint8Array(10); + assert.throws(() => { + port1.postMessage(buf, [buf.buffer, buf.buffer]); + }, /^DataCloneError: Transfer list contains duplicate ArrayBuffer$/); + port1.close(); +} diff --git a/test/js/node/test/parallel/test-worker-type-check.js b/test/js/node/test/parallel/test-worker-type-check.js new file mode 100644 index 00000000000000..9a718dfad055b4 --- /dev/null +++ b/test/js/node/test/parallel/test-worker-type-check.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + [ + undefined, + null, + false, + 0, + Symbol('test'), + {}, + [], + () => {}, + ].forEach((val) => { + assert.throws( + () => new Worker(val), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "filename" argument must be of type string ' + + 'or an instance of URL.' + + common.invalidArgTypeHelper(val) + } + ); + }); +} diff --git a/test/js/node/worker_threads/emit-non-function-fixture.js b/test/js/node/worker_threads/emit-non-function-fixture.js new file mode 100644 index 00000000000000..7f6cf6dfc50954 --- /dev/null +++ b/test/js/node/worker_threads/emit-non-function-fixture.js @@ -0,0 +1,22 @@ +import { Worker } from "node:worker_threads"; +import assert from "node:assert"; + +const { promise, resolve, reject } = Promise.withResolvers(); + +process.on("worker", assert.fail); +process.once("uncaughtException", exception => { + try { + assert.strictEqual(exception.name, "TypeError"); + assert(exception.message.includes("5 is not a function"), "message should include '5 is not a function'"); + resolve(); + } catch (e) { + reject(e); + } +}); + +// this will emit the "worker" event on the next tick +new Worker("", { eval: true }); +// override it for when we try to emit the event and look up "emit" +process.emit = 5; +// wait for the error +await promise; diff --git a/test/js/node/worker_threads/environmentdata-empty-fixture.js b/test/js/node/worker_threads/environmentdata-empty-fixture.js new file mode 100644 index 00000000000000..a17907be651d39 --- /dev/null +++ b/test/js/node/worker_threads/environmentdata-empty-fixture.js @@ -0,0 +1,20 @@ +// when the main thread's environmentData has not been set up (because worker_threads was not imported) +// child threads should still be able to use environmentData + +const innerWorkerSrc = /* js */ ` + const assert = require("assert"); + const { getEnvironmentData } = require("worker_threads"); + assert.strictEqual(getEnvironmentData("foo"), "bar"); +`; + +const outerWorkerSrc = /* js */ ` + const { Worker, setEnvironmentData } = require("worker_threads"); + setEnvironmentData("foo", "bar"); + new Worker(${"`"}${innerWorkerSrc}${"`"}, { eval: true }).on("error", e => { + throw e; + }); +`; + +new Worker("data:text/javascript," + outerWorkerSrc).addEventListener("error", e => { + throw e; +}); diff --git a/test/js/node/worker_threads/environmentdata-inherit-fixture.js b/test/js/node/worker_threads/environmentdata-inherit-fixture.js new file mode 100644 index 00000000000000..5e9475728847ce --- /dev/null +++ b/test/js/node/worker_threads/environmentdata-inherit-fixture.js @@ -0,0 +1,13 @@ +const { Worker, getEnvironmentData, setEnvironmentData, workerData, isMainThread } = require("worker_threads"); + +if (isMainThread) { + // this value should be passed all the way down even through worker threads that don't call setEnvironmentData + setEnvironmentData("inherited", "foo"); + new Worker(__filename, { workerData: { depth: 0 } }); +} else { + console.log(getEnvironmentData("inherited")); + const { depth } = workerData; + if (depth + 1 < 5) { + new Worker(__filename, { workerData: { depth: depth + 1 } }); + } +} diff --git a/test/js/node/worker_threads/worker_destruction.test.ts b/test/js/node/worker_threads/worker_destruction.test.ts index 083a2243bd09d5..5e483a5ba3f175 100644 --- a/test/js/node/worker_threads/worker_destruction.test.ts +++ b/test/js/node/worker_threads/worker_destruction.test.ts @@ -1,10 +1,14 @@ import { describe, expect, test } from "bun:test"; import "harness"; +import { isBroken } from "harness"; import { join } from "path"; describe("Worker destruction", () => { - const method = ["Bun.connect", "Bun.listen"]; - test.each(method)("bun closes cleanly when %s is used in a Worker that is terminating", method => { - expect([join(import.meta.dir, "worker_thread_check.ts"), method]).toRun(); + const method = ["Bun.connect", "Bun.listen", "fetch"]; + describe.each(method)("bun when %s is used in a Worker that is terminating", method => { + // fetch: ASAN failure + test.skipIf(isBroken && method == "fetch")("exits cleanly", () => { + expect([join(import.meta.dir, "worker_thread_check.ts"), method]).toRun(); + }); }); }); diff --git a/test/js/node/worker_threads/worker_thread_check.ts b/test/js/node/worker_threads/worker_thread_check.ts index 004786cad6e93a..df171482228eae 100644 --- a/test/js/node/worker_threads/worker_thread_check.ts +++ b/test/js/node/worker_threads/worker_thread_check.ts @@ -5,11 +5,42 @@ import { Worker, isMainThread, workerData } from "worker_threads"; const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const actions = { + async ["Bun.connect"](port) { + await Bun.connect({ + hostname: "localhost", + port, + socket: { + open() {}, + error() {}, + data() {}, + drain() {}, + close() {}, + }, + }); + }, + async ["Bun.listen"](port) { + const server = Bun.listen({ + hostname: "localhost", + port: 0, + socket: { + open() {}, + error() {}, + data() {}, + drain() {}, + close() {}, + }, + }); + }, + async ["fetch"](port) { + const resp = await fetch("http://localhost:" + port); + await resp.blob(); + }, +}; + if (isMainThread) { let action = process.argv.at(-1); - if (process.argv.length === 2) { - action = "Bun.connect"; - } + if (actions[action!] === undefined) throw new Error("not found"); const server = Bun.serve({ port: 0, @@ -20,7 +51,7 @@ if (isMainThread) { let remaining = RUN_COUNT; while (remaining--) { - const promises = []; + const promises: Promise[] = []; for (let i = 0; i < CONCURRENCY; i++) { const worker = new Worker(import.meta.url, { @@ -31,7 +62,7 @@ if (isMainThread) { env: process.env, }); worker.ref(); - const { promise, resolve } = Promise.withResolvers(); + const { promise, resolve, reject } = Promise.withResolvers(); promises.push(promise); worker.on("online", () => { @@ -41,6 +72,7 @@ if (isMainThread) { }) .finally(resolve); }); + worker.on("error", e => reject(e)); } await Promise.all(promises); @@ -51,40 +83,5 @@ if (isMainThread) { } else { Bun.gc(true); const { action, port } = workerData; - - switch (action) { - case "Bun.connect": { - await Bun.connect({ - hostname: "localhost", - port, - socket: { - open() {}, - error() {}, - data() {}, - drain() {}, - close() {}, - }, - }); - break; - } - case "Bun.listen": { - const server = Bun.listen({ - hostname: "localhost", - port: 0, - socket: { - open() {}, - error() {}, - data() {}, - drain() {}, - close() {}, - }, - }); - break; - } - case "fetch": { - const resp = await fetch("http://localhost:" + port); - await resp.blob(); - break; - } - } + await actions[action](port); } diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 4e36578c460487..ab0a1a63cbaaec 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1,4 +1,5 @@ import { bunEnv, bunExe } from "harness"; +import { once } from "node:events"; import fs from "node:fs"; import { join, relative, resolve } from "node:path"; import wt, { @@ -291,3 +292,110 @@ test("eval does not leak source code", async () => { if (errors.length > 0) throw new Error(errors); expect(proc.exitCode).toBe(0); }); + +describe("worker event", () => { + test("is emitted on the next tick with the right value", () => { + const { promise, resolve } = Promise.withResolvers(); + let worker: Worker | undefined = undefined; + let called = false; + process.once("worker", eventWorker => { + called = true; + expect(eventWorker as any).toBe(worker); + resolve(); + }); + worker = new Worker(new URL("data:text/javascript,")); + expect(called).toBeFalse(); + return promise; + }); + + test("uses an overridden process.emit function", async () => { + const previousEmit = process.emit; + try { + const { promise, resolve, reject } = Promise.withResolvers(); + let worker: Worker | undefined; + // should not actually emit the event + process.on("worker", expect.unreachable); + worker = new Worker("", { eval: true }); + // should look up process.emit on the next tick, not synchronously during the Worker constructor + (process as any).emit = (event, value) => { + try { + expect(event).toBe("worker"); + expect(value).toBe(worker); + resolve(); + } catch (e) { + reject(e); + } + }; + await promise; + } finally { + process.emit = previousEmit; + process.off("worker", expect.unreachable); + } + }); + + test("throws if process.emit is not a function", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "emit-non-function-fixture.js"], + env: bunEnv, + cwd: __dirname, + stderr: "pipe", + stdout: "ignore", + }); + await proc.exited; + const errors = await new Response(proc.stderr).text(); + if (errors.length > 0) throw new Error(errors); + expect(proc.exitCode).toBe(0); + }); +}); + +describe("environmentData", () => { + test("can pass a value to a child", async () => { + setEnvironmentData("foo", new Map([["hello", "world"]])); + const worker = new Worker( + /* js */ ` + const { getEnvironmentData, parentPort } = require("worker_threads"); + parentPort.postMessage(getEnvironmentData("foo")); + `, + { eval: true }, + ); + const [msg] = await once(worker, "message"); + expect(msg).toEqual(new Map([["hello", "world"]])); + }); + + test("child modifications do not affect parent", async () => { + const worker = new Worker('require("worker_threads").setEnvironmentData("does_not_exist", "foo")', { eval: true }); + const [code] = await once(worker, "exit"); + expect(code).toBe(0); + expect(getEnvironmentData("does_not_exist")).toBeUndefined(); + }); + + test("is deeply inherited", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "environmentdata-inherit-fixture.js"], + env: bunEnv, + cwd: __dirname, + stderr: "pipe", + stdout: "pipe", + }); + await proc.exited; + const errors = await new Response(proc.stderr).text(); + if (errors.length > 0) throw new Error(errors); + expect(proc.exitCode).toBe(0); + const out = await new Response(proc.stdout).text(); + expect(out).toBe("foo\n".repeat(5)); + }); + + test("can be used if parent thread had not imported worker_threads", async () => { + const proc = Bun.spawn({ + cmd: [bunExe(), "environmentdata-empty-fixture.js"], + env: bunEnv, + cwd: __dirname, + stderr: "pipe", + stdout: "ignore", + }); + await proc.exited; + const errors = await new Response(proc.stderr).text(); + if (errors.length > 0) throw new Error(errors); + expect(proc.exitCode).toBe(0); + }); +}); diff --git a/test/js/web/workers/worker.test.ts b/test/js/web/workers/worker.test.ts index 219406c6ee47aa..47925159708980 100644 --- a/test/js/web/workers/worker.test.ts +++ b/test/js/web/workers/worker.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { once } from "events"; import { bunEnv, bunExe } from "harness"; import path from "path"; import wt from "worker_threads"; @@ -269,6 +270,22 @@ describe("web worker", () => { done(); }); }); + + describe("worker event", () => { + test("is fired with the right object", () => { + const { promise, resolve } = Promise.withResolvers(); + let worker: Worker | undefined = undefined; + let called = false; + process.once("worker", eventWorker => { + called = true; + expect(eventWorker as any).toBe(worker); + resolve(); + }); + worker = new Worker(new URL("data:text/javascript,")); + expect(called).toBeFalse(); + return promise; + }); + }); }); // TODO: move to node:worker_threads tests directory @@ -311,7 +328,7 @@ describe("worker_threads", () => { worker.on("message", () => done()); }); const code = await worker.terminate(); - expect(code).toBe(0); + expect(code).toBe(1); }); test("worker without argv/execArgv", async () => { @@ -349,14 +366,10 @@ describe("worker_threads", () => { test("worker with eval = false fails with code", async () => { let has_error = false; - try { - const worker = new wt.Worker("console.log('this should not get printed')", { eval: false }); - } catch (err) { - expect(err.constructor.name).toEqual("TypeError"); - expect(err.message).toMatch(/BuildMessage: ModuleNotFound.+/); - has_error = true; - } - expect(has_error).toBe(true); + const worker = new wt.Worker("console.log('this should not get printed')", { eval: false }); + const [err] = await once(worker, "error"); + expect(err.constructor.name).toEqual("Error"); + expect(err.message).toMatch(/BuildMessage: ModuleNotFound.+/); }); test("worker with eval = true succeeds with valid code", async () => { diff --git a/test/js/web/workers/worker_blob.test.ts b/test/js/web/workers/worker_blob.test.ts index b3c7eaea3b3d9e..aaf58ae0ea3e26 100644 --- a/test/js/web/workers/worker_blob.test.ts +++ b/test/js/web/workers/worker_blob.test.ts @@ -57,9 +57,10 @@ test("TypeScript Worker from a Blob", async () => { }); test("Worker from a blob errors on invalid blob", async () => { - expect(() => { - new Worker("blob:i dont exist!"); - }).toThrow(); + const { promise, reject } = Promise.withResolvers(); + const worker = new Worker("blob:i dont exist!"); + worker.addEventListener("error", e => reject(e.message)); + expect(promise).rejects.toBe('BuildMessage: ModuleNotFound resolving "blob:i dont exist!" (entry point)'); }); test("Revoking an object URL after a Worker is created before it loads should throw an error", async () => { @@ -80,7 +81,7 @@ test("Revoking an object URL after a Worker is created before it loads should th worker.onerror = resolve; }); expect(result).toBeInstanceOf(ErrorEvent); - expect((result as ErrorEvent).message).toContain(url); + expect((result as ErrorEvent).message).toBe("BuildMessage: Blob URL is missing"); break; } catch (e) { if (attempt === 9) {