From ff635551436123022ba3980b39580d53973c80a2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 24 Jun 2023 06:02:16 -0700 Subject: Rewrite Bun's runtime CommonJS loader (#3379) * wip changes for CommonJS * this rewrite is almost complete * even more code * wip * Remove usages of `import.meta.require` from builtins * Remove usages of require * Regenerate * :scissors: builtin rewrite commonjs in printer * Use lazy custom getters for import.meta * fixups * Remove depd * ugh * still crashing * fixup undici * comment out import.meta.require.resolve temporarily not a real solution but it stops the crashes * Redo import.meta.primordials * Builtins now have a `builtin://` protocol in source origin * Seems to work? * Finsih getting rid of primordials * switcharoo * No more function * just one more bug * Update launch.json * Implement `require.main` * :scissors: * Bump WebKit * Fixup import cycles * Fixup improt cycles * export more things * Implement `createCommonJSModule` builtin * More exports * regenerate * i broke some stuff * some of these tests work now * We lost the encoding * Sort of fix zlib * Sort of fix util * Update events.js * bump * bump * bump * Fix missing export in fs * fix some bugs with builtin esm modules (stream, worker_threads, events). its not perfect yet. * fix some other internal module bugs * oops * fix some extra require default stuff * uncomment this file but it crsahes on my machine * tidy code here * fixup tls exports * make simdutf happier * Add hasPrefix binding * Add test for `require.main` * Fix CommonJS evaluation order race condition * Make node:http load faster * Add missing exports to tls.js * Use the getter * Regenerate builtins * Fix assertion failure in Bun.write() * revamp dotEnv parser (#3347) - fixes `strings.indexOfAny()` - fixes OOB array access fixes #411 fixes #2823 fixes #3042 * fix tests for `expect()` (#3384) - extend test job time-out for `darwin-aarch64` * `expect().resolves` and `expect().rejects` (#3318) * Move expect and snapshots to their own files * expect().resolves and expect().rejects * Fix promise being added to unhandled rejection list * Handle timeouts in expect() * wip merge * Fix merge issue --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> * fixup min/memcopy (#3388) * Fix crash in builtins * Don't attempt to evaluate modules with no source code * Update WebCoreJSBuiltins.cpp * Update WebCoreJSBuiltins.cpp * Update WebCoreJSBuiltins.cpp * Fix crash * cleanup * Fix test cc @paperdave * Fixup Undici * Fix issue in node:http * Create util-deprecate.mjs * Fix several bugs * Use the identifier * Support error.code in `util.deprecate` * make the CJs loader slightly more resilient * Update WebCoreJSBuiltins.cpp * Fix macros --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: dave caruso Co-authored-by: Alex Lam S.L Co-authored-by: Ashcon Partovi Co-authored-by: Ciro Spaciari --- src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + 2 files changed, 2 insertions(+) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 3997c1d88..65875d091 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -29,6 +29,7 @@ public: std::unique_ptr m_clientSubspaceForReadableState; std::unique_ptr m_clientSubspaceForPendingVirtualModuleResult; std::unique_ptr m_clientSubspaceForCallSite; + std::unique_ptr m_clientSubspaceForImportMeta; std::unique_ptr m_clientSubspaceForNapiExternal; std::unique_ptr m_clientSubspaceForRequireResolveFunction; std::unique_ptr m_clientSubspaceForBundlerPlugin; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 4feca1754..433832450 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -30,6 +30,7 @@ public: std::unique_ptr m_subspaceForPendingVirtualModuleResult; std::unique_ptr m_subspaceForCallSite; std::unique_ptr m_subspaceForNapiExternal; + std::unique_ptr m_subspaceForImportMeta; std::unique_ptr m_subspaceForRequireResolveFunction; std::unique_ptr m_subspaceForBundlerPlugin; std::unique_ptr m_subspaceForNodeVMScript; -- cgit v1.2.3 From 777f98bd1068239033718c44bdcfca87e58a91d5 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Sun, 25 Jun 2023 03:31:55 -0700 Subject: make these strings lazy --- src/bun.js/bindings/webcore/WebSocket.cpp | 63 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 26 deletions(-) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index a346175df..519b86d59 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -1032,6 +1032,17 @@ void WebSocket::didConnect(us_socket_t* socket, char* bufferedData, size_t buffe this->didConnect(); } + +#define LAZY_NEVER_DESTROYED(name) \ + ([]() -> const String& { \ + static LazyNeverDestroyed str; \ + static std::once_flag onceFlag; \ + std::call_once(onceFlag, [] { \ + str.construct(MAKE_STATIC_STRING_IMPL(name)); \ + }); \ + return str; \ + })() + void WebSocket::didFailWithErrorCode(int32_t code) { // from new WebSocket() -> connect() @@ -1050,158 +1061,158 @@ void WebSocket::didFailWithErrorCode(int32_t code) } // invalid_response case 1: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid response"); + const String& message = LAZY_NEVER_DESTROYED("Invalid response"); didReceiveMessageError(1002, message); break; } // expected_101_status_code case 2: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Expected 101 status code"); + const String& message = LAZY_NEVER_DESTROYED("Expected 101 status code"); didReceiveMessageError(1002, message); break; } // missing_upgrade_header case 3: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing upgrade header"); + const String& message = LAZY_NEVER_DESTROYED("Missing upgrade header"); didReceiveMessageError(1002, message); break; } // missing_connection_header case 4: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing connection header"); + const String& message = LAZY_NEVER_DESTROYED("Missing connection header"); didReceiveMessageError(1002, message); break; } // missing_websocket_accept_header case 5: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing websocket accept header"); + const String& message = LAZY_NEVER_DESTROYED("Missing websocket accept header"); didReceiveMessageError(1002, message); break; } // invalid_upgrade_header case 6: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid upgrade header"); + const String& message = LAZY_NEVER_DESTROYED("Invalid upgrade header"); didReceiveMessageError(1002, message); break; } // invalid_connection_header case 7: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid connection header"); + const String& message = LAZY_NEVER_DESTROYED("Invalid connection header"); didReceiveMessageError(1002, message); break; } // invalid_websocket_version case 8: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid websocket version"); + const String& message = LAZY_NEVER_DESTROYED("Invalid websocket version"); didReceiveMessageError(1002, message); break; } // mismatch_websocket_accept_header case 9: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Mismatch websocket accept header"); + const String& message = LAZY_NEVER_DESTROYED("Mismatch websocket accept header"); didReceiveMessageError(1002, message); break; } // missing_client_protocol case 10: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing client protocol"); + const String& message = LAZY_NEVER_DESTROYED("Missing client protocol"); didReceiveMessageError(1002, message); break; } // mismatch_client_protocol case 11: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Mismatch client protocol"); + const String& message = LAZY_NEVER_DESTROYED("Mismatch client protocol"); didReceiveMessageError(1002, message); break; } // timeout case 12: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Timeout"); + const String& message = LAZY_NEVER_DESTROYED("Timeout"); didReceiveMessageError(1013, message); break; } // closed case 13: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Closed by client"); + const String& message = LAZY_NEVER_DESTROYED("Closed by client"); didReceiveMessageError(1000, message); break; } // failed_to_write case 14: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to write"); + const String& message = LAZY_NEVER_DESTROYED("Failed to write"); didReceiveMessageError(1006, message); break; } // failed_to_connect case 15: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to connect"); + const String& message = LAZY_NEVER_DESTROYED("Failed to connect"); didReceiveMessageError(1006, message); break; } // headers_too_large case 16: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Headers too large"); + const String& message = LAZY_NEVER_DESTROYED("Headers too large"); didReceiveMessageError(1007, message); break; } // ended case 17: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Closed by server"); + const String& message = LAZY_NEVER_DESTROYED("Closed by server"); didReceiveMessageError(1001, message); break; } // failed_to_allocate_memory case 18: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to allocate memory"); + const String& message = LAZY_NEVER_DESTROYED("Failed to allocate memory"); didReceiveMessageError(1001, message); break; } // control_frame_is_fragmented case 19: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - control frame is fragmented"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - control frame is fragmented"); didReceiveMessageError(1002, message); break; } // invalid_control_frame case 20: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - invalid control frame"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - invalid control frame"); didReceiveMessageError(1002, message); break; } // compression_unsupported case 21: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Compression not implemented yet"); + const String& message = LAZY_NEVER_DESTROYED("Compression not implemented yet"); didReceiveMessageError(1011, message); break; } // unexpected_mask_from_server case 22: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected mask from server"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - unexpected mask from server"); didReceiveMessageError(1002, message); break; } // expected_control_frame case 23: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - expected control frame"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - expected control frame"); didReceiveMessageError(1002, message); break; } // unsupported_control_frame case 24: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unsupported control frame"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - unsupported control frame"); didReceiveMessageError(1002, message); break; } // unexpected_opcode case 25: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected opcode"); + const String& message = LAZY_NEVER_DESTROYED("Protocol error - unexpected opcode"); didReceiveMessageError(1002, message); break; } // invalid_utf8 case 26: { - static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Server sent invalid UTF8"); + const String& message = LAZY_NEVER_DESTROYED("Server sent invalid UTF8"); didReceiveMessageError(1003, message); break; } -- cgit v1.2.3 From f8abd167d86b94da817d91631ef0137b019a9999 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Sun, 25 Jun 2023 04:17:07 -0700 Subject: Revert "make these strings lazy" This reverts commit 777f98bd1068239033718c44bdcfca87e58a91d5. --- src/bun.js/bindings/webcore/WebSocket.cpp | 63 +++++++++++++------------------ 1 file changed, 26 insertions(+), 37 deletions(-) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index 519b86d59..a346175df 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -1032,17 +1032,6 @@ void WebSocket::didConnect(us_socket_t* socket, char* bufferedData, size_t buffe this->didConnect(); } - -#define LAZY_NEVER_DESTROYED(name) \ - ([]() -> const String& { \ - static LazyNeverDestroyed str; \ - static std::once_flag onceFlag; \ - std::call_once(onceFlag, [] { \ - str.construct(MAKE_STATIC_STRING_IMPL(name)); \ - }); \ - return str; \ - })() - void WebSocket::didFailWithErrorCode(int32_t code) { // from new WebSocket() -> connect() @@ -1061,158 +1050,158 @@ void WebSocket::didFailWithErrorCode(int32_t code) } // invalid_response case 1: { - const String& message = LAZY_NEVER_DESTROYED("Invalid response"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid response"); didReceiveMessageError(1002, message); break; } // expected_101_status_code case 2: { - const String& message = LAZY_NEVER_DESTROYED("Expected 101 status code"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Expected 101 status code"); didReceiveMessageError(1002, message); break; } // missing_upgrade_header case 3: { - const String& message = LAZY_NEVER_DESTROYED("Missing upgrade header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing upgrade header"); didReceiveMessageError(1002, message); break; } // missing_connection_header case 4: { - const String& message = LAZY_NEVER_DESTROYED("Missing connection header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing connection header"); didReceiveMessageError(1002, message); break; } // missing_websocket_accept_header case 5: { - const String& message = LAZY_NEVER_DESTROYED("Missing websocket accept header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing websocket accept header"); didReceiveMessageError(1002, message); break; } // invalid_upgrade_header case 6: { - const String& message = LAZY_NEVER_DESTROYED("Invalid upgrade header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid upgrade header"); didReceiveMessageError(1002, message); break; } // invalid_connection_header case 7: { - const String& message = LAZY_NEVER_DESTROYED("Invalid connection header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid connection header"); didReceiveMessageError(1002, message); break; } // invalid_websocket_version case 8: { - const String& message = LAZY_NEVER_DESTROYED("Invalid websocket version"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Invalid websocket version"); didReceiveMessageError(1002, message); break; } // mismatch_websocket_accept_header case 9: { - const String& message = LAZY_NEVER_DESTROYED("Mismatch websocket accept header"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Mismatch websocket accept header"); didReceiveMessageError(1002, message); break; } // missing_client_protocol case 10: { - const String& message = LAZY_NEVER_DESTROYED("Missing client protocol"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Missing client protocol"); didReceiveMessageError(1002, message); break; } // mismatch_client_protocol case 11: { - const String& message = LAZY_NEVER_DESTROYED("Mismatch client protocol"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Mismatch client protocol"); didReceiveMessageError(1002, message); break; } // timeout case 12: { - const String& message = LAZY_NEVER_DESTROYED("Timeout"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Timeout"); didReceiveMessageError(1013, message); break; } // closed case 13: { - const String& message = LAZY_NEVER_DESTROYED("Closed by client"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Closed by client"); didReceiveMessageError(1000, message); break; } // failed_to_write case 14: { - const String& message = LAZY_NEVER_DESTROYED("Failed to write"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to write"); didReceiveMessageError(1006, message); break; } // failed_to_connect case 15: { - const String& message = LAZY_NEVER_DESTROYED("Failed to connect"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to connect"); didReceiveMessageError(1006, message); break; } // headers_too_large case 16: { - const String& message = LAZY_NEVER_DESTROYED("Headers too large"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Headers too large"); didReceiveMessageError(1007, message); break; } // ended case 17: { - const String& message = LAZY_NEVER_DESTROYED("Closed by server"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Closed by server"); didReceiveMessageError(1001, message); break; } // failed_to_allocate_memory case 18: { - const String& message = LAZY_NEVER_DESTROYED("Failed to allocate memory"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Failed to allocate memory"); didReceiveMessageError(1001, message); break; } // control_frame_is_fragmented case 19: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - control frame is fragmented"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - control frame is fragmented"); didReceiveMessageError(1002, message); break; } // invalid_control_frame case 20: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - invalid control frame"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - invalid control frame"); didReceiveMessageError(1002, message); break; } // compression_unsupported case 21: { - const String& message = LAZY_NEVER_DESTROYED("Compression not implemented yet"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Compression not implemented yet"); didReceiveMessageError(1011, message); break; } // unexpected_mask_from_server case 22: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - unexpected mask from server"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected mask from server"); didReceiveMessageError(1002, message); break; } // expected_control_frame case 23: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - expected control frame"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - expected control frame"); didReceiveMessageError(1002, message); break; } // unsupported_control_frame case 24: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - unsupported control frame"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unsupported control frame"); didReceiveMessageError(1002, message); break; } // unexpected_opcode case 25: { - const String& message = LAZY_NEVER_DESTROYED("Protocol error - unexpected opcode"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected opcode"); didReceiveMessageError(1002, message); break; } // invalid_utf8 case 26: { - const String& message = LAZY_NEVER_DESTROYED("Server sent invalid UTF8"); + static NeverDestroyed message = MAKE_STATIC_STRING_IMPL("Server sent invalid UTF8"); didReceiveMessageError(1003, message); break; } -- cgit v1.2.3 From 039bbc68ad1f36ee7a734907ef657e90c7f504ac Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Thu, 29 Jun 2023 21:24:55 -0700 Subject: Add missing "prependListener" function --- src/bun.js/bindings/webcore/JSEventEmitter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp index 995d845cf..1957b404b 100644 --- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp +++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp @@ -148,7 +148,7 @@ static const HashTableValue JSEventEmitterPrototypeTableValues[] = { { "addListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_addListener, 2 } }, { "on"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_addListener, 2 } }, { "once"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_addOnceListener, 2 } }, - { "prepend"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependListener, 2 } }, + { "prependListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependListener, 2 } }, { "prependOnce"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependOnceListener, 2 } }, { "removeListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_removeListener, 2 } }, { "off"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_removeListener, 2 } }, -- cgit v1.2.3 From f00e2be548da21b9feaef178bb0ac22230801d6f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 1 Jul 2023 17:37:44 -0700 Subject: Use `BunString` in `SystemError` (#3485) * Use `BunString` in SystemError * Use Bun::toStringRef when we will de-ref strings * Move `napi_create_error` to C++ to support `code` being a Symbol potentially * Update blob.zig * Make this test less flaky --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/bun/dns_resolver.zig | 8 ++-- src/bun.js/api/bun/socket.zig | 12 ++--- src/bun.js/api/ffi.zig | 6 +-- src/bun.js/api/server.zig | 12 ++--- src/bun.js/bindings/BunString.cpp | 48 +++++++++++++++++--- src/bun.js/bindings/bindings.cpp | 66 +++++++++++++--------------- src/bun.js/bindings/bindings.zig | 8 ++-- src/bun.js/bindings/headers-handwritten.h | 12 +++-- src/bun.js/bindings/helpers.h | 4 +- src/bun.js/bindings/napi.cpp | 29 +++++++++--- src/bun.js/bindings/webcore/JSCloseEvent.cpp | 2 +- src/bun.js/node/node_os.zig | 32 +++++++------- src/bun.js/node/syscall.zig | 8 ++-- src/bun.js/webcore/blob.zig | 55 +++++++++++------------ src/bun.js/webcore/response.zig | 14 +++--- src/bun.js/webcore/streams.zig | 6 +-- src/bundler/bundle_v2.zig | 16 +++++-- src/http.zig | 5 ++- src/napi/napi.zig | 11 +---- test/js/bun/util/error-gc-test.test.js | 41 ++++++++++++++++- 20 files changed, 242 insertions(+), 153 deletions(-) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index fee834e5e..d0d4f5b7b 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -1925,8 +1925,8 @@ pub const DNSResolver = struct { .err => |err| { const system_error = JSC.SystemError{ .errno = -1, - .code = JSC.ZigString.init(err.code()), - .message = JSC.ZigString.init(err.label()), + .code = bun.String.static(err.code()), + .message = bun.String.static(err.label()), }; globalThis.throwValue(system_error.toErrorInstance(globalThis)); @@ -1972,8 +1972,8 @@ pub const DNSResolver = struct { .err => |err| { const system_error = JSC.SystemError{ .errno = -1, - .code = JSC.ZigString.init(err.code()), - .message = JSC.ZigString.init(err.label()), + .code = bun.String.static(err.code()), + .message = bun.String.static(err.label()), }; globalThis.throwValue(system_error.toErrorInstance(globalThis)); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 00e34a77d..69d6611cb 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1022,8 +1022,8 @@ fn NewSocket(comptime ssl: bool) type { var globalObject = handlers.globalObject; const err = JSC.SystemError{ .errno = errno, - .message = ZigString.init("Failed to connect"), - .syscall = ZigString.init("connect"), + .message = bun.String.static("Failed to connect"), + .syscall = bun.String.static("connect"), }; if (callback == .zero) { @@ -1232,8 +1232,8 @@ fn NewSocket(comptime ssl: bool) type { const reason = if (ssl_error.reason == null) "" else ssl_error.reason[0..bun.len(ssl_error.reason)]; const fallback = JSC.SystemError{ - .code = ZigString.init(code), - .message = ZigString.init(reason), + .code = bun.String.create(code), + .message = bun.String.create(reason), }; authorization_error = fallback.toErrorInstance(globalObject); @@ -1409,8 +1409,8 @@ fn NewSocket(comptime ssl: bool) type { const reason = if (ssl_error.reason == null) "" else ssl_error.reason[0..bun.len(ssl_error.reason)]; const fallback = JSC.SystemError{ - .code = ZigString.init(code), - .message = ZigString.init(reason), + .code = bun.String.create(code), + .message = bun.String.create(reason), }; return fallback.toErrorInstance(globalObject); diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index e46e054ec..ba31b67ed 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -311,9 +311,9 @@ pub const FFI = struct { break :brk std.DynLib.open(backup_name) catch { // Then, if that fails, report an error. const system_error = JSC.SystemError{ - .code = ZigString.init(@tagName(JSC.Node.ErrorCode.ERR_DLOPEN_FAILED)), - .message = ZigString.init("Failed to open library. This is usually caused by a missing library or an invalid library path."), - .syscall = ZigString.init("dlopen"), + .code = bun.String.create(@tagName(JSC.Node.ErrorCode.ERR_DLOPEN_FAILED)), + .message = bun.String.create("Failed to open library. This is usually caused by a missing library or an invalid library path."), + .syscall = bun.String.create("dlopen"), }; return system_error.toErrorInstance(global); }; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 136737069..140e62ce4 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1796,7 +1796,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .syscall = .sendfile, }; var sys = err.withPathLike(file.pathlike).toSystemError(); - sys.message = ZigString.init("MacOS does not support sending non-regular files"); + sys.message = bun.String.static("MacOS does not support sending non-regular files"); this.runErrorHandler(sys.toErrorInstance( this.server.globalThis, )); @@ -1815,7 +1815,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .syscall = .sendfile, }; var sys = err.withPathLike(file.pathlike).toSystemError(); - sys.message = ZigString.init("File must be regular or FIFO"); + sys.message = bun.String.static("File must be regular or FIFO"); this.runErrorHandler(sys.toErrorInstance( this.server.globalThis, )); @@ -2375,8 +2375,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } const fallback = JSC.SystemError{ - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_UNHANDLED_ERROR))), - .message = ZigString.init("Unhandled error in ReadableStream"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_UNHANDLED_ERROR))), + .message = bun.String.static("Unhandled error in ReadableStream"), }; req.handleReject(fallback.toErrorInstance(globalThis)); } @@ -2422,8 +2422,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (stream.isLocked(this.server.globalThis)) { streamLog("was locked but it shouldn't be", .{}); var err = JSC.SystemError{ - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), - .message = ZigString.init("Stream already used, please create a new one"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), + .message = bun.String.static("Stream already used, please create a new one"), }; stream.value.unprotect(); this.runErrorHandler(err.toErrorInstance(this.server.globalThis)); diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index f590edd35..4c8ff384e 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -86,31 +86,69 @@ BunString toString(JSC::JSGlobalObject* globalObject, JSValue value) return fromJS(globalObject, value); } +BunString toStringRef(JSC::JSGlobalObject* globalObject, JSValue value) +{ + auto str = value.toWTFString(globalObject); + if (str.isEmpty()) { + return { BunStringTag::Empty }; + } + + str.impl()->ref(); + + return { BunStringTag::WTFStringImpl, { .wtf = str.impl() } }; +} + BunString toString(WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; } BunString toString(const WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; } BunString toString(WTF::StringImpl* wtfString) { - if (wtfString->length() == 0) + if (wtfString->isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; } +BunString toStringRef(WTF::String& wtfString) +{ + if (wtfString.isEmpty()) + return { BunStringTag::Empty }; + + wtfString.impl()->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; +} +BunString toStringRef(const WTF::String& wtfString) +{ + if (wtfString.isEmpty()) + return { BunStringTag::Empty }; + + wtfString.impl()->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; +} +BunString toStringRef(WTF::StringImpl* wtfString) +{ + if (wtfString->isEmpty()) + return { BunStringTag::Empty }; + + wtfString->ref(); + + return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; +} + BunString fromString(WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; @@ -118,7 +156,7 @@ BunString fromString(WTF::String& wtfString) BunString fromString(WTF::StringImpl* wtfString) { - if (wtfString->length() == 0) + if (wtfString->isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 96fcde303..9f9b20c1e 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1279,15 +1279,14 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC__JSGlobalObject* globalObject) { - static const char* system_error_name = "SystemError"; SystemError err = *arg0; JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSC::JSValue message = JSC::jsUndefined(); - if (err.message.len > 0) { - message = Zig::toJSString(err.message, globalObject); + if (err.message.tag != BunStringTag::Empty) { + message = Bun::toJS(globalObject, err.message); } JSC::JSValue options = JSC::jsUndefined(); @@ -1297,8 +1296,8 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, auto clientData = WebCore::clientData(vm); - if (err.code.len > 0 && !(err.code.len == 1 and err.code.ptr[0] == 0)) { - JSC::JSValue code = Zig::toJSStringGC(err.code, globalObject); + if (err.code.tag != BunStringTag::Empty) { + JSC::JSValue code = Bun::toJS(globalObject, err.code); result->putDirect(vm, clientData->builtinNames().codePublicName(), code, JSC::PropertyAttribute::DontDelete | 0); @@ -1307,13 +1306,12 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, result->putDirect( vm, vm.propertyNames->name, - JSC::JSValue(JSC::jsOwnedString( - vm, WTF::String(WTF::StringImpl::createWithoutCopying(system_error_name, 11)))), + JSC::JSValue(jsString(vm, String("SystemError"_s))), JSC::PropertyAttribute::DontEnum | 0); } - if (err.path.len > 0) { - JSC::JSValue path = JSC::JSValue(Zig::toJSStringGC(err.path, globalObject)); + if (err.path.tag != BunStringTag::Empty) { + JSC::JSValue path = Bun::toJS(globalObject, err.path); result->putDirect(vm, clientData->builtinNames().pathPublicName(), path, JSC::PropertyAttribute::DontDelete | 0); } @@ -1324,8 +1322,8 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC::PropertyAttribute::DontDelete | 0); } - if (err.syscall.len > 0) { - JSC::JSValue syscall = JSC::JSValue(Zig::toJSString(err.syscall, globalObject)); + if (err.syscall.tag != BunStringTag::Empty) { + JSC::JSValue syscall = Bun::toJS(globalObject, err.syscall); result->putDirect(vm, clientData->builtinNames().syscallPublicName(), syscall, JSC::PropertyAttribute::DontDelete | 0); } @@ -3303,11 +3301,8 @@ bool JSC__JSValue__stringIncludes(JSC__JSValue value, JSC__JSGlobalObject* globa static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stackFrame, ZigStackFrame* frame) { - String str = stackFrame->sourceURL(vm); - if (!str.isEmpty()) - str.impl()->ref(); - frame->source_url = Bun::toString(str); + frame->source_url = Bun::toStringRef(stackFrame->sourceURL(vm)); if (stackFrame->isWasmFrame()) { frame->code_type = ZigStackFrameCodeWasm; @@ -3344,10 +3339,7 @@ static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stack JSC::JSObject* callee = JSC::jsCast(calleeCell); - String displayName = JSC::getCalculatedDisplayName(vm, callee); - if (!displayName.isEmpty()) - displayName.impl()->ref(); - frame->function_name = Bun::toString(displayName); + frame->function_name = Bun::toStringRef(JSC::getCalculatedDisplayName(vm, callee)); } // Based on // https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/src/shim/JSCStackTrace.cpp#L298 @@ -3421,7 +3413,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunStr // Most of the time, when you look at a stack trace, you want a couple lines above - source_lines[0] = Bun::toString(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); + source_lines[0] = Bun::toStringRef(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); source_line_numbers[0] = line; if (lineStart > 0) { @@ -3438,7 +3430,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunStr } // We are at the beginning of the line - source_lines[source_line_i] = Bun::toString(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); + source_lines[source_line_i] = Bun::toStringRef(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); source_line_numbers[source_line_i] = line - source_line_i; source_line_i++; @@ -3526,30 +3518,32 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->code = 8; } if (except->code == SYNTAX_ERROR_CODE) { - except->message = Bun::toString(err->sanitizedMessageString(global)); + except->message = Bun::toStringRef(err->sanitizedMessageString(global)); } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { - except->message = Bun::toString(global, message); + except->message = Bun::toStringRef(global, message); } else { - except->message = Bun::toString(err->sanitizedMessageString(global)); + except->message = Bun::toStringRef(err->sanitizedMessageString(global)); } - except->name = Bun::toString(err->sanitizedNameString(global)); + + except->name = Bun::toStringRef(err->sanitizedNameString(global)); + except->runtime_type = err->runtimeTypeForCause(); auto clientData = WebCore::clientData(vm); if (except->code != SYNTAX_ERROR_CODE) { if (JSC::JSValue syscall = obj->getIfPropertyExists(global, clientData->builtinNames().syscallPublicName())) { - except->syscall = Bun::toString(global, syscall); + except->syscall = Bun::toStringRef(global, syscall); } if (JSC::JSValue code = obj->getIfPropertyExists(global, clientData->builtinNames().codePublicName())) { - except->code_ = Bun::toString(global, code); + except->code_ = Bun::toStringRef(global, code); } if (JSC::JSValue path = obj->getIfPropertyExists(global, clientData->builtinNames().pathPublicName())) { - except->path = Bun::toString(global, path); + except->path = Bun::toStringRef(global, path); } if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(vm, "fd"_s))) { @@ -3565,7 +3559,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (getFromSourceURL) { if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, vm.propertyNames->sourceURL)) { - except->stack.frames_ptr[0].source_url = Bun::toString(global, sourceURL); + except->stack.frames_ptr[0].source_url = Bun::toStringRef(global, sourceURL); if (JSC::JSValue column = obj->getIfPropertyExists(global, vm.propertyNames->column)) { except->stack.frames_ptr[0].position.column_start = column.toInt32(global); @@ -3577,7 +3571,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "lineText"_s))) { if (JSC::JSString* jsStr = lineText.toStringOrNull(global)) { auto str = jsStr->value(global); - except->stack.source_lines_ptr[0] = Bun::toString(str); + except->stack.source_lines_ptr[0] = Bun::toStringRef(str); except->stack.source_lines_numbers[0] = except->stack.frames_ptr[0].position.line; except->stack.source_lines_len = 1; except->remapped = true; @@ -3600,7 +3594,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSObject* obj = JSC::jsDynamicCast(value)) { if (obj->hasProperty(global, global->vm().propertyNames->name)) { auto name_str = obj->getIfPropertyExists(global, global->vm().propertyNames->name).toWTFString(global); - except->name = Bun::toString(name_str); + except->name = Bun::toStringRef(name_str); if (name_str == "Error"_s) { except->code = JSErrorCodeError; } else if (name_str == "EvalError"_s) { @@ -3622,14 +3616,14 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSValue message = obj->getIfPropertyExists(global, global->vm().propertyNames->message)) { if (message) { - except->message = Bun::toString( + except->message = Bun::toStringRef( message.toWTFString(global)); } } if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, global->vm().propertyNames->sourceURL)) { if (sourceURL) { - except->stack.frames_ptr[0].source_url = Bun::toString( + except->stack.frames_ptr[0].source_url = Bun::toStringRef( sourceURL.toWTFString(global)); except->stack.frames_len = 1; } @@ -3658,7 +3652,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal } scope.release(); - except->message = Bun::toString(str); + except->message = Bun::toStringRef(str); } void JSC__VM__releaseWeakRefs(JSC__VM* arg0) @@ -3768,8 +3762,8 @@ void JSC__JSValue__toZigException(JSC__JSValue JSValue0, JSC__JSGlobalObject* ar JSC::JSValue value = JSC::JSValue::decode(JSValue0); if (value == JSC::JSValue {}) { exception->code = JSErrorCodeError; - exception->name = Bun::toString("Error"_s); - exception->message = Bun::toString("Unknown error"_s); + exception->name = Bun::toStringRef("Error"_s); + exception->message = Bun::toStringRef("Unknown error"_s); return; } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 1c09378a8..777860d3c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1561,10 +1561,10 @@ pub const FetchHeaders = opaque { pub const SystemError = extern struct { errno: c_int = 0, /// label for errno - code: ZigString = ZigString.init(""), - message: ZigString = ZigString.init(""), - path: ZigString = ZigString.init(""), - syscall: ZigString = ZigString.init(""), + code: String = String.empty, + message: String = String.empty, + path: String = String.empty, + syscall: String = String.empty, fd: i32 = -1, pub fn Maybe(comptime Result: type) type { diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index c7429b633..90c8f86d2 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -84,10 +84,10 @@ typedef struct ErrorableResolvedSource { typedef struct SystemError { int errno_; - ZigString code; - ZigString message; - ZigString path; - ZigString syscall; + BunString code; + BunString message; + BunString path; + BunString syscall; int fd; } SystemError; @@ -246,6 +246,10 @@ BunString toString(WTF::String& wtfString); BunString toString(const WTF::String& wtfString); BunString toString(WTF::StringImpl* wtfString); +BunString toStringRef(JSC::JSGlobalObject* globalObject, JSC::JSValue value); +BunString toStringRef(WTF::String& wtfString); +BunString toStringRef(const WTF::String& wtfString); +BunString toStringRef(WTF::StringImpl* wtfString); } using Uint8Array_alias = JSC::JSUint8Array; diff --git a/src/bun.js/bindings/helpers.h b/src/bun.js/bindings/helpers.h index 402807f3d..00777c304 100644 --- a/src/bun.js/bindings/helpers.h +++ b/src/bun.js/bindings/helpers.h @@ -342,10 +342,10 @@ static const WTF::String toStringStatic(ZigString str) } if (isTaggedUTF16Ptr(str.ptr)) { - return WTF::String(WTF::ExternalStringImpl::createStatic(reinterpret_cast(untag(str.ptr)), str.len)); + return WTF::String(AtomStringImpl::add(reinterpret_cast(untag(str.ptr)), str.len)); } - return WTF::String(WTF::ExternalStringImpl::createStatic( + return WTF::String(AtomStringImpl::add( reinterpret_cast(untag(str.ptr)), str.len)); } diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index a859e3ac5..bb62cb2a0 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -554,7 +554,6 @@ extern "C" napi_status napi_wrap(napi_env env, auto* globalObject = toJS(env); auto& vm = globalObject->vm(); - auto* val = jsDynamicCast(value); @@ -572,7 +571,7 @@ extern "C" napi_status napi_wrap(napi_env env, auto clientData = WebCore::clientData(vm); auto* ref = new NapiRef(globalObject, 1); - ref->strongRef.set(globalObject->vm(), value.getObject()); + ref->strongRef.set(globalObject->vm(), value.getObject()); if (finalize_cb) { ref->finalizer.finalize_cb = finalize_cb; @@ -816,7 +815,7 @@ extern "C" napi_status napi_create_reference(napi_env env, napi_value value, } } - if(object) { + if (object) { object->napiRef = ref; } @@ -1029,7 +1028,26 @@ extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, auto error = JSC::createTypeError(globalObject, messageValue.toWTFString(globalObject)); if (codeValue) { - error->putDirect(vm, Identifier::fromString(vm, "code"_s), codeValue, 0); + error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), codeValue, 0); + } + + *result = reinterpret_cast(JSC::JSValue::encode(error)); + return napi_ok; +} + +extern "C" napi_status napi_create_error(napi_env env, napi_value code, + napi_value msg, + napi_value* result) +{ + Zig::GlobalObject* globalObject = toJS(env); + JSC::VM& vm = globalObject->vm(); + + JSC::JSValue codeValue = JSC::JSValue::decode(reinterpret_cast(code)); + JSC::JSValue messageValue = JSC::JSValue::decode(reinterpret_cast(msg)); + + auto error = JSC::createError(globalObject, messageValue.toWTFString(globalObject)); + if (codeValue) { + error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), codeValue, 0); } *result = reinterpret_cast(JSC::JSValue::encode(error)); @@ -1474,7 +1492,8 @@ extern "C" napi_status napi_get_property_names(napi_env env, napi_value object, return napi_ok; } -extern "C" napi_status napi_create_object(napi_env env, napi_value* result){ +extern "C" napi_status napi_create_object(napi_env env, napi_value* result) +{ if (UNLIKELY(result == nullptr)) { return napi_invalid_arg; diff --git a/src/bun.js/bindings/webcore/JSCloseEvent.cpp b/src/bun.js/bindings/webcore/JSCloseEvent.cpp index be07cbcfe..ad7b6ed57 100644 --- a/src/bun.js/bindings/webcore/JSCloseEvent.cpp +++ b/src/bun.js/bindings/webcore/JSCloseEvent.cpp @@ -99,7 +99,7 @@ template<> CloseEvent::Init convertDictionary(JSGlobalObject& if (isNullOrUndefined) codeValue = jsUndefined(); else { - codeValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "code"_s)); + codeValue = object->get(&lexicalGlobalObject, WebCore::builtinNames(vm).codePublicName()); RETURN_IF_EXCEPTION(throwScope, {}); } if (!codeValue.isUndefined()) { diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index f71143315..483acb3e2 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -78,8 +78,8 @@ pub const Os = struct { return if (comptime Environment.isLinux) cpusImplLinux(globalThis) catch { const err = JSC.SystemError{ - .message = JSC.ZigString.init("Failed to get cpu information"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("Failed to get cpu information"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -88,8 +88,8 @@ pub const Os = struct { else if (comptime Environment.isMac) cpusImplDarwin(globalThis) catch { const err = JSC.SystemError{ - .message = JSC.ZigString.init("Failed to get cpu information"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("Failed to get cpu information"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -318,11 +318,11 @@ pub const Os = struct { //info.put(globalThis, JSC.ZigString.static("syscall"), JSC.ZigString.init("uv_os_getpriority").withEncoding().toValueGC(globalThis)); const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -3, - .syscall = JSC.ZigString.init("uv_os_getpriority"), + .syscall = bun.String.static("uv_os_getpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -377,10 +377,10 @@ pub const Os = struct { const rc = C.getifaddrs(&interface_start); if (rc != 0) { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: getifaddrs returned an error"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: getifaddrs returned an error"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), .errno = @intFromEnum(std.os.errno(rc)), - .syscall = JSC.ZigString.init("getifaddrs"), + .syscall = bun.String.static("getifaddrs"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -591,11 +591,11 @@ pub const Os = struct { switch (errcode) { .SRCH => { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -3, - .syscall = JSC.ZigString.init("uv_os_setpriority"), + .syscall = bun.String.static("uv_os_setpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -603,11 +603,11 @@ pub const Os = struct { }, .ACCES => { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -13, - .syscall = JSC.ZigString.init("uv_os_setpriority"), + .syscall = bun.String.static("uv_os_setpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); diff --git a/src/bun.js/node/syscall.zig b/src/bun.js/node/syscall.zig index 48c5b1305..5ff0b2f44 100644 --- a/src/bun.js/node/syscall.zig +++ b/src/bun.js/node/syscall.zig @@ -873,20 +873,20 @@ pub const Error = struct { pub fn toSystemError(this: Error) SystemError { var err = SystemError{ .errno = @as(c_int, this.errno) * -1, - .syscall = JSC.ZigString.init(@tagName(this.syscall)), + .syscall = bun.String.static(@tagName(this.syscall)), }; // errno label if (this.errno > 0 and this.errno < C.SystemErrno.max) { const system_errno = @enumFromInt(C.SystemErrno, this.errno); - err.code = JSC.ZigString.init(@tagName(system_errno)); + err.code = bun.String.static(@tagName(system_errno)); if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = JSC.ZigString.init(label); + err.message = bun.String.static(label); } } if (this.path.len > 0) { - err.path = JSC.ZigString.init(this.path); + err.path = bun.String.create(this.path); } if (this.fd != -1) { diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index faf503a3f..86b5414e3 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1194,9 +1194,6 @@ pub const Blob = struct { .syscall = .open, }).toSystemError(); - // assert we never end up reusing the memory - std.debug.assert(@intFromPtr(this.system_error.?.path.slice().ptr) != @intFromPtr(path_buffer)); - callback(this, null_fd); return; }; @@ -1359,12 +1356,13 @@ pub const Blob = struct { return; } else if (this.store == null) { bun.default_allocator.destroy(this); - cb(cb_ctx, ResultType{ .err = SystemError{ - .code = ZigString.init("INTERNAL_ERROR"), - .path = ZigString.Empty, - .message = ZigString.init("assertion failure - store should not be null"), - .syscall = ZigString.init("read"), - } }); + cb(cb_ctx, ResultType{ + .err = SystemError{ + .code = bun.String.static("INTERNAL_ERROR"), + .message = bun.String.static("assertion failure - store should not be null"), + .syscall = bun.String.static("read"), + }, + }); return; } @@ -1396,12 +1394,12 @@ pub const Blob = struct { }).toSystemError(); } else { this.system_error = JSC.SystemError{ - .code = ZigString.init(bun.asByteSlice(@errorName(err))), + .code = bun.String.static(bun.asByteSlice(@errorName(err))), .path = if (this.file_store.pathlike == .path) - ZigString.init(this.file_store.pathlike.path.slice()) + bun.String.create(this.file_store.pathlike.path.slice()) else - ZigString.Empty, - .syscall = ZigString.init("read"), + bun.String.empty, + .syscall = bun.String.static("read"), }; this.errno = err; @@ -1458,13 +1456,13 @@ pub const Blob = struct { if (std.os.S.ISDIR(stat.mode)) { this.errno = error.EISDIR; this.system_error = JSC.SystemError{ - .code = ZigString.init("EISDIR"), + .code = bun.String.static("EISDIR"), .path = if (this.file_store.pathlike == .path) - ZigString.init(this.file_store.pathlike.path.slice()) + bun.String.create(this.file_store.pathlike.path.slice()) else - ZigString.Empty, - .message = ZigString.init("Directories cannot be read like files"), - .syscall = ZigString.init("read"), + bun.String.empty, + .message = bun.String.static("Directories cannot be read like files"), + .syscall = bun.String.static("read"), }; return; } @@ -1643,8 +1641,8 @@ pub const Blob = struct { this.wrote += @truncate(SizeType, result catch |errno| { this.errno = errno; this.system_error = this.system_error orelse JSC.SystemError{ - .code = ZigString.init(bun.asByteSlice(@errorName(errno))), - .syscall = ZigString.init("write"), + .code = bun.String.static(bun.asByteSlice(@errorName(errno))), + .syscall = bun.String.static("write"), }; this.wrote = 0; @@ -1703,13 +1701,13 @@ pub const Blob = struct { const unsupported_directory_error = SystemError{ .errno = @intCast(c_int, @intFromEnum(bun.C.SystemErrno.EISDIR)), - .message = ZigString.init("That doesn't work on folders"), - .syscall = ZigString.init("fstat"), + .message = bun.String.static("That doesn't work on folders"), + .syscall = bun.String.static("fstat"), }; const unsupported_non_regular_file_error = SystemError{ .errno = @intCast(c_int, @intFromEnum(bun.C.SystemErrno.ENOTSUP)), - .message = ZigString.init("Non-regular files aren't supported yet"), - .syscall = ZigString.init("fstat"), + .message = bun.String.static("Non-regular files aren't supported yet"), + .syscall = bun.String.static("fstat"), }; // blocking, but off the main thread @@ -1777,13 +1775,12 @@ pub const Blob = struct { pub fn reject(this: *CopyFile, promise: *JSC.JSPromise) void { var globalThis = this.globalThis; var system_error: SystemError = this.system_error orelse SystemError{}; - if (this.source_file_store.pathlike == .path and system_error.path.len == 0) { - system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice()); - system_error.path.mark(); + if (this.source_file_store.pathlike == .path and system_error.path.isEmpty()) { + system_error.path = bun.String.create(this.source_file_store.pathlike.path.slice()); } - if (system_error.message.len == 0) { - system_error.message = ZigString.init("Failed to copy file"); + if (system_error.message.isEmpty()) { + system_error.message = bun.String.static("Failed to copy file"); } var instance = system_error.toErrorInstance(this.globalThis); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index b4ea08579..e888ffa5a 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -777,15 +777,15 @@ pub const Fetch = struct { } const fetch_error = JSC.SystemError{ - .code = ZigString.init(@errorName(this.result.fail)), + .code = bun.String.static(@errorName(this.result.fail)), .message = switch (this.result.fail) { - error.ConnectionClosed => ZigString.init("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), - error.FailedToOpenSocket => ZigString.init("Was there a typo in the url or port?"), - error.TooManyRedirects => ZigString.init("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), - error.ConnectionRefused => ZigString.init("Unable to connect. Is the computer able to access the url?"), - else => ZigString.init("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), + error.ConnectionClosed => bun.String.static("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), + error.FailedToOpenSocket => bun.String.static("Was there a typo in the url or port?"), + error.TooManyRedirects => bun.String.static("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), + error.ConnectionRefused => bun.String.static("Unable to connect. Is the computer able to access the url?"), + else => bun.String.static("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), }, - .path = ZigString.init(this.http.?.url.href), + .path = bun.String.create(this.http.?.url.href), }; return fetch_error.toErrorInstance(this.global_this); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 5986afac7..343ce37ab 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -1964,10 +1964,10 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { pub const message = std.fmt.comptimePrint("{s} is not constructable", .{SinkType.name}); }; const err = JSC.SystemError{ - .message = ZigString.init(Static.message), - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), + .message = bun.String.static(Static.message), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), }; - globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); + globalThis.throwValue(err.toErrorInstance(globalThis)); return JSC.JSValue.jsUndefined(); } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 814e49a20..f534e4184 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -9174,8 +9174,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + var message = err.toSystemError().message.toUTF8(bun.default_allocator); + defer message.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing sourcemap for chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(message.slice()), bun.fmt.quote(chunk.final_rel_path), }) catch unreachable; return error.WriteFailed; @@ -9242,8 +9244,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + var message = err.toSystemError().message.toUTF8(bun.default_allocator); + defer message.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(message.slice()), bun.fmt.quote(chunk.final_rel_path), }) catch unreachable; return error.WriteFailed; @@ -9309,8 +9313,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); + defer utf8.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(utf8.slice()), bun.fmt.quote(components_manifest_path), }) catch unreachable; return error.WriteFailed; @@ -9383,8 +9389,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); + defer utf8.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing file {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(utf8.slice()), bun.fmt.quote(src.src_path.text), }) catch unreachable; return error.WriteFailed; diff --git a/src/http.zig b/src/http.zig index 80718db2f..b1d97c382 100644 --- a/src/http.zig +++ b/src/http.zig @@ -684,8 +684,9 @@ pub const RequestContext = struct { if (erro == error.EBADF or erro == error.ECONNABORTED or erro == error.ECONNREFUSED) { return error.SocketClosed; } - - Output.prettyErrorln("send() error: {s}", .{err.toSystemError().message.slice()}); + const msg = err.toSystemError().message.toUTF8(bun.default_allocator); + defer msg.deinit(); + Output.prettyErrorln("send() error: {s}", .{msg.slice()}); return erro; }, diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 0973ca559..439319489 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -303,16 +303,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: [*]const char16_t, le return .ok; } pub extern fn napi_create_symbol(env: napi_env, description: napi_value, result: *napi_value) napi_status; -pub export fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status { - log("napi_create_error: \"{any}\"", .{msg.getZigString(env)}); - const system_error = JSC.SystemError{ - .code = if (!code.isEmptyOrUndefinedOrNull()) code.getZigString(env) else ZigString.Empty, - .message = msg.getZigString(env), - }; - result.* = system_error.toErrorInstance(env); - return .ok; -} - +pub extern fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_type_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_range_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_typeof(env: napi_env, value: napi_value, result: *napi_valuetype) napi_status; diff --git a/test/js/bun/util/error-gc-test.test.js b/test/js/bun/util/error-gc-test.test.js index 247bd68ef..4a45346b6 100644 --- a/test/js/bun/util/error-gc-test.test.js +++ b/test/js/bun/util/error-gc-test.test.js @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; - +import { readFileSync } from "fs"; // This test checks that printing stack traces increments and decrements // reference-counted strings test("error gc test", () => { @@ -34,7 +34,7 @@ test("error gc test #2", () => { } }); -test("error gc test #2", () => { +test("error gc test #3", () => { for (let i = 0; i < 1000; i++) { var err = new Error(); Error.captureStackTrace(err); @@ -42,3 +42,40 @@ test("error gc test #2", () => { Bun.gc(); } }); + +// This test fails if: +// - it crashes +// - The test failure message gets a non-sensical error +test("error gc test #4", () => { + for (let i = 0; i < 1000; i++) { + let path = + // Use a long-enough string for it to be obvious if we leak memory + "/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i"; + try { + readFileSync(path); + throw new Error("unreachable"); + } catch (e) { + if (e.message === "unreachable") { + throw e; + } + + const inspected = Bun.inspect(e); + Bun.gc(true); + + // Deliberately avoid using .toContain() directly to avoid + // BunString shenanigins. + // + // Only JSC builtin functions to operate on the string after inspecting it. + // + if (!inspected.includes(path)) { + expect(inspected).toContain(path); + } + + if (!inspected.includes("ENOENT")) { + expect(inspected).toContain("ENOENT"); + } + } finally { + Bun.gc(true); + } + } +}); -- cgit v1.2.3 From a7a01bd52f20e7908f06d4de9a1814902b838a4b Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 3 Jul 2023 16:19:50 -0300 Subject: [tls] add socket parameter, setServername and ALPNprotocols support (#3457) * add socket parameter support * refactor #socket * add test and more fixs * some fixes * bump uws * handlers fix * more fixes * fix node net and node tls tests * fix duplicate port * fix deinit on CallbackJobs * cleanup * add setImmediate repro * add test to setImmediate * this is necessary? * fix prependOnce on native listener * try to findout the error on nodemailer CI * show error message * Update bun.lockb * prettier * Use exact versions of packages * add alpnProtocol support * update * emit error when connect fails on net.Socket * format * fix _write and cleanup * fixup * fix connect, add alpn test * fix socket.io * add socket parameter to TLSSocket * add TLSSocket socket first parameter * fixup and _start * remove flask tests * fmt --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/bun.zig | 2 + src/bun.js/api/bun/socket.zig | 544 +++++++++++++++++++-- src/bun.js/api/server.zig | 48 +- src/bun.js/api/sockets.classes.ts | 16 + src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- src/bun.js/bindings/JSSinkLookupTable.h | 2 +- src/bun.js/bindings/ZigGeneratedClasses.cpp | 218 +++++++++ src/bun.js/bindings/generated_classes.zig | 26 + src/bun.js/bindings/webcore/JSEventEmitter.cpp | 2 +- src/deps/uws | 2 +- src/deps/uws.zig | 284 ++++++++++- src/js/node/net.js | 151 ++++-- src/js/node/tls.js | 351 ++++++++++++- src/js/out/modules/node/net.js | 118 +++-- src/js/out/modules/node/tls.js | 190 ++++++- test/bun.lockb | Bin 139814 -> 140524 bytes test/js/node/net/node-net-server.test.ts | 55 --- test/js/node/tls/node-tls-connect.test.ts | 32 ++ test/js/node/tls/node-tls-server.test.ts | 55 --- test/js/third_party/nodemailer/nodemailer.test.ts | 15 + test/js/third_party/nodemailer/package.json | 6 + .../nodemailer/process-nodemailer-fixture.js | 23 + test/js/web/timers/process-setImmediate-fixture.js | 9 + test/js/web/timers/setImmediate.test.js | 27 + test/package.json | 5 +- 26 files changed, 1888 insertions(+), 297 deletions(-) create mode 100644 test/js/node/tls/node-tls-connect.test.ts create mode 100644 test/js/third_party/nodemailer/nodemailer.test.ts create mode 100644 test/js/third_party/nodemailer/package.json create mode 100644 test/js/third_party/nodemailer/process-nodemailer-fixture.js create mode 100644 test/js/web/timers/process-setImmediate-fixture.js (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 2e6381c74..1e5a5e004 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -3794,6 +3794,8 @@ pub const Timer = struct { result.then(globalThis, this, CallbackJob__onResolve, CallbackJob__onReject); }, } + } else { + this.deinit(); } } }; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 69d6611cb..329cc40e4 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -69,6 +69,11 @@ fn normalizeHost(input: anytype) @TypeOf(input) { const BinaryType = JSC.BinaryType; +const WrappedType = enum { + none, + tls, + tcp, +}; const Handlers = struct { onOpen: JSC.JSValue = .zero, onClose: JSC.JSValue = .zero, @@ -97,8 +102,8 @@ const Handlers = struct { handlers: *Handlers, socket_context: *uws.SocketContext, - pub fn exit(this: *Scope, ssl: bool) void { - this.handlers.markInactive(ssl, this.socket_context); + pub fn exit(this: *Scope, ssl: bool, wrapped: WrappedType) void { + this.handlers.markInactive(ssl, this.socket_context, wrapped); } }; @@ -123,19 +128,24 @@ const Handlers = struct { return true; } - pub fn markInactive(this: *Handlers, ssl: bool, ctx: *uws.SocketContext) void { + pub fn markInactive(this: *Handlers, ssl: bool, ctx: *uws.SocketContext, wrapped: WrappedType) void { Listener.log("markInactive", .{}); this.active_connections -= 1; - if (this.active_connections == 0 and this.is_server) { - var listen_socket: *Listener = @fieldParentPtr(Listener, "handlers", this); - // allow it to be GC'd once the last connection is closed and it's not listening anymore - if (listen_socket.listener == null) { - listen_socket.strong_self.clear(); + if (this.active_connections == 0) { + if (this.is_server) { + var listen_socket: *Listener = @fieldParentPtr(Listener, "handlers", this); + // allow it to be GC'd once the last connection is closed and it's not listening anymore + if (listen_socket.listener == null) { + listen_socket.strong_self.clear(); + } + } else { + this.unprotect(); + // will deinit when is not wrapped or when is the TCP wrapped connection + if (wrapped != .tls) { + ctx.deinit(ssl); + } + bun.default_allocator.destroy(this); } - } else if (this.active_connections == 0 and !this.is_server) { - this.unprotect(); - ctx.deinit(ssl); - bun.default_allocator.destroy(this); } } @@ -364,6 +374,7 @@ pub const Listener = struct { connection: UnixOrHost, socket_context: ?*uws.SocketContext = null, ssl: bool = false, + protos: ?[]const u8 = null, strong_data: JSC.Strong = .{}, strong_self: JSC.Strong = .{}, @@ -395,6 +406,19 @@ pub const Listener = struct { port: u16, }, + pub fn clone(this: UnixOrHost) UnixOrHost { + switch (this) { + .unix => |u| { + return .{ + .unix = (bun.default_allocator.dupe(u8, u) catch unreachable), + }; + }, + .host => |h| { + return .{ .host = .{ .host = (bun.default_allocator.dupe(u8, h.host) catch unreachable), .port = this.host.port } }; + }, + } + } + pub fn deinit(this: UnixOrHost) void { switch (this) { .unix => |u| { @@ -455,10 +479,12 @@ pub const Listener = struct { var socket_config = SocketConfig.fromJS(opts, globalObject, exception) orelse { return .zero; }; + var hostname_or_unix = socket_config.hostname_or_unix; var port = socket_config.port; var ssl = socket_config.ssl; var handlers = socket_config.handlers; + var protos: ?[]const u8 = null; const exclusive = socket_config.exclusive; handlers.is_server = true; @@ -496,6 +522,10 @@ pub const Listener = struct { }; if (ssl_enabled) { + if (ssl.?.protos) |p| { + protos = p[0..ssl.?.protos_len]; + } + uws.NewSocketHandler(true).configure( socket_context, true, @@ -593,6 +623,7 @@ pub const Listener = struct { .ssl = ssl_enabled, .socket_context = socket_context, .listener = listen_socket, + .protos = if (protos) |p| (bun.default_allocator.dupe(u8, p) catch unreachable) else null, }; socket.handlers.protect(); @@ -649,6 +680,8 @@ pub const Listener = struct { .handlers = &listener.handlers, .this_value = .zero, .socket = socket, + .protos = listener.protos, + .owned_protos = false, }; if (listener.strong_data.get()) |default_data| { const globalObject = listener.handlers.globalObject; @@ -715,6 +748,10 @@ pub const Listener = struct { this.handlers.unprotect(); this.connection.deinit(); + if (this.protos) |protos| { + this.protos = null; + bun.default_allocator.destroy(protos); + } bun.default_allocator.destroy(this); } @@ -775,13 +812,16 @@ pub const Listener = struct { const socket_config = SocketConfig.fromJS(opts, globalObject, exception) orelse { return .zero; }; + var hostname_or_unix = socket_config.hostname_or_unix; var port = socket_config.port; var ssl = socket_config.ssl; var handlers = socket_config.handlers; var default_data = socket_config.default_data; + var protos: ?[]const u8 = null; const ssl_enabled = ssl != null; + defer if (ssl != null) ssl.?.deinit(); handlers.protect(); @@ -797,6 +837,9 @@ pub const Listener = struct { }; if (ssl_enabled) { + if (ssl.?.protos) |p| { + protos = p[0..ssl.?.protos_len]; + } uws.NewSocketHandler(true).configure( socket_context, true, @@ -848,6 +891,7 @@ pub const Listener = struct { .this_value = .zero, .socket = undefined, .connection = connection, + .protos = if (protos) |p| (bun.default_allocator.dupe(u8, p) catch unreachable) else null, }; TLSSocket.dataSetCached(tls.getThisValue(globalObject), globalObject, default_data); @@ -871,6 +915,7 @@ pub const Listener = struct { .this_value = .zero, .socket = undefined, .connection = null, + .protos = null, }; TCPSocket.dataSetCached(tcp.getThisValue(globalObject), globalObject, default_data); @@ -898,11 +943,41 @@ fn JSSocketType(comptime ssl: bool) type { } } +fn selectALPNCallback( + _: ?*BoringSSL.SSL, + out: [*c][*c]const u8, + outlen: [*c]u8, + in: [*c]const u8, + inlen: c_uint, + arg: ?*anyopaque, +) callconv(.C) c_int { + const this = bun.cast(*TLSSocket, arg); + if (this.protos) |protos| { + if (protos.len == 0) { + return BoringSSL.SSL_TLSEXT_ERR_NOACK; + } + + const status = BoringSSL.SSL_select_next_proto(bun.cast([*c][*c]u8, out), outlen, protos.ptr, @intCast(c_uint, protos.len), in, inlen); + + // Previous versions of Node.js returned SSL_TLSEXT_ERR_NOACK if no protocol + // match was found. This would neither cause a fatal alert nor would it result + // in a useful ALPN response as part of the Server Hello message. + // We now return SSL_TLSEXT_ERR_ALERT_FATAL in that case as per Section 3.2 + // of RFC 7301, which causes a fatal no_application_protocol alert. + const expected = if (comptime BoringSSL.OPENSSL_NPN_NEGOTIATED == 1) BoringSSL.SSL_TLSEXT_ERR_OK else BoringSSL.SSL_TLSEXT_ERR_ALERT_FATAL; + + return if (status == expected) 1 else 0; + } else { + return BoringSSL.SSL_TLSEXT_ERR_NOACK; + } +} + fn NewSocket(comptime ssl: bool) type { return struct { pub const Socket = uws.NewSocketHandler(ssl); socket: Socket, detached: bool = false, + wrapped: WrappedType = .none, handlers: *Handlers, this_value: JSC.JSValue = .zero, poll_ref: JSC.PollRef = JSC.PollRef.init(), @@ -910,6 +985,8 @@ fn NewSocket(comptime ssl: bool) type { last_4: [4]u8 = .{ 0, 0, 0, 0 }, authorized: bool = false, connection: ?Listener.UnixOrHost = null, + protos: ?[]const u8, + owned_protos: bool = true, // TODO: switch to something that uses `visitAggregate` and have the // `Listener` keep a list of all the sockets JSValue in there @@ -1079,7 +1156,7 @@ fn NewSocket(comptime ssl: bool) type { var vm = this.handlers.vm; this.reffer.unref(vm); - this.handlers.markInactive(ssl, this.socket.context()); + this.handlers.markInactive(ssl, this.socket.context(), this.wrapped); this.poll_ref.unref(vm); this.has_pending_activity.store(false, .Release); } @@ -1091,25 +1168,35 @@ fn NewSocket(comptime ssl: bool) type { // Add SNI support for TLS (mongodb and others requires this) if (comptime ssl) { - if (this.connection) |connection| { - if (connection == .host) { - const host = normalizeHost(connection.host.host); - if (host.len > 0) { - var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, socket.getNativeHandle()); - if (!ssl_ptr.isInitFinished()) { + var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, socket.getNativeHandle()); + if (!ssl_ptr.isInitFinished()) { + if (this.connection) |connection| { + if (connection == .host) { + const host = normalizeHost(connection.host.host); + if (host.len > 0) { var host__ = default_allocator.dupeZ(u8, host) catch unreachable; defer default_allocator.free(host__); ssl_ptr.setHostname(host__); } } } + if (this.protos) |protos| { + if (this.handlers.is_server) { + BoringSSL.SSL_CTX_set_alpn_select_cb(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), selectALPNCallback, bun.cast(*anyopaque, this)); + } else { + _ = BoringSSL.SSL_set_alpn_protos(ssl_ptr, protos.ptr, @intCast(c_uint, protos.len)); + } + } } } this.poll_ref.ref(this.handlers.vm); this.detached = false; this.socket = socket; - socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, this); + + if (this.wrapped == .none) { + socket.ext(**anyopaque).?.* = bun.cast(**anyopaque, this); + } const handlers = this.handlers; const callback = handlers.onOpen; @@ -1174,7 +1261,7 @@ fn NewSocket(comptime ssl: bool) type { // the handlers must be kept alive for the duration of the function call // that way if we need to call the error handler, we can var scope = handlers.enter(socket.context()); - defer scope.exit(ssl); + defer scope.exit(ssl, this.wrapped); const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); @@ -1211,7 +1298,7 @@ fn NewSocket(comptime ssl: bool) type { // the handlers must be kept alive for the duration of the function call // that way if we need to call the error handler, we can var scope = handlers.enter(socket.context()); - defer scope.exit(ssl); + defer scope.exit(ssl, this.wrapped); const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); @@ -1255,7 +1342,6 @@ fn NewSocket(comptime ssl: bool) type { log("onClose", .{}); this.detached = true; defer this.markInactive(); - const handlers = this.handlers; this.poll_ref.unref(handlers.vm); @@ -1265,7 +1351,7 @@ fn NewSocket(comptime ssl: bool) type { // the handlers must be kept alive for the duration of the function call // that way if we need to call the error handler, we can var scope = handlers.enter(socket.context()); - defer scope.exit(ssl); + defer scope.exit(ssl, this.wrapped); var globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); @@ -1295,7 +1381,7 @@ fn NewSocket(comptime ssl: bool) type { // the handlers must be kept alive for the duration of the function call // that way if we need to call the error handler, we can var scope = handlers.enter(socket.context()); - defer scope.exit(ssl); + defer scope.exit(ssl, this.wrapped); // const encoding = handlers.encoding; const result = callback.callWithThis(globalObject, this_value, &[_]JSValue{ @@ -1476,10 +1562,20 @@ fn NewSocket(comptime ssl: bool) type { } fn writeMaybeCorked(this: *This, buffer: []const u8, is_end: bool) i32 { - if (this.socket.isShutdown() or this.socket.isClosed()) { + if (this.detached or this.socket.isShutdown() or this.socket.isClosed()) { return -1; } // we don't cork yet but we might later + + if (comptime ssl) { + // TLS wrapped but in TCP mode + if (this.wrapped == .tcp) { + const res = this.socket.rawWrite(buffer, is_end); + log("write({d}, {any}) = {d}", .{ buffer.len, is_end, res }); + return res; + } + } + const res = this.socket.write(buffer, is_end); log("write({d}, {any}) = {d}", .{ buffer.len, is_end, res }); return res; @@ -1487,7 +1583,6 @@ fn NewSocket(comptime ssl: bool) type { fn writeOrEnd(this: *This, globalObject: *JSC.JSGlobalObject, args: []const JSC.JSValue, is_end: bool) WriteResult { if (args.len == 0) return .{ .success = .{} }; - if (args.ptr[0].asArrayBuffer(globalObject)) |array_buffer| { var slice = array_buffer.slice(); @@ -1681,9 +1776,6 @@ fn NewSocket(comptime ssl: bool) type { if (result.wrote == result.total) { this.socket.flush(); this.detached = true; - if (!this.socket.isClosed()) { - this.socket.close(0, null); - } this.markInactive(); } break :brk JSValue.jsNumber(result.wrote); @@ -1706,17 +1798,27 @@ fn NewSocket(comptime ssl: bool) type { pub fn finalize(this: *This) callconv(.C) void { log("finalize()", .{}); - if (this.detached) return; - this.detached = true; - if (!this.socket.isClosed()) { - this.socket.close(0, null); + if (!this.detached) { + this.detached = true; + if (!this.socket.isClosed()) { + this.socket.close(0, null); + } + this.markInactive(); + } + + this.poll_ref.unref(JSC.VirtualMachine.get()); + // need to deinit event without being attached + if (this.owned_protos) { + if (this.protos) |protos| { + this.protos = null; + default_allocator.free(protos); + } } + if (this.connection) |connection| { - connection.deinit(); this.connection = null; + connection.deinit(); } - this.markInactive(); - this.poll_ref.unref(JSC.VirtualMachine.get()); } pub fn reload(this: *This, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { @@ -1756,8 +1858,376 @@ fn NewSocket(comptime ssl: bool) type { return JSValue.jsUndefined(); } + + pub fn getALPNProtocol( + this: *This, + globalObject: *JSC.JSGlobalObject, + ) callconv(.C) JSValue { + if (comptime ssl == false) { + return JSValue.jsBoolean(false); + } + + if (this.detached) { + return JSValue.jsBoolean(false); + } + + var alpn_proto: [*c]const u8 = null; + var alpn_proto_len: u32 = 0; + + var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle()); + BoringSSL.SSL_get0_alpn_selected(ssl_ptr, &alpn_proto, &alpn_proto_len); + if (alpn_proto == null or alpn_proto_len == 0) { + return JSValue.jsBoolean(false); + } + + const slice = alpn_proto[0..alpn_proto_len]; + if (strings.eql(slice, "h2")) { + return ZigString.static("h2").toValue(globalObject); + } + if (strings.eql(slice, "http/1.1")) { + return ZigString.static("http/1.1").toValue(globalObject); + } + return ZigString.fromUTF8(slice).toValueGC(globalObject); + } + + pub fn setServername( + this: *This, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + if (comptime ssl == false) { + return JSValue.jsUndefined(); + } + if (this.detached) { + return JSValue.jsUndefined(); + } + + if (this.handlers.is_server) { + globalObject.throw("Cannot issue SNI from a TLS server-side socket", .{}); + return .zero; + } + + const args = callframe.arguments(1); + if (args.len < 1) { + globalObject.throw("Expected 1 argument", .{}); + return .zero; + } + + const server_name = args.ptr[0]; + if (!server_name.isString()) { + globalObject.throw("Expected \"serverName\" to be a string", .{}); + return .zero; + } + + const slice = server_name.getZigString(globalObject).toSlice(bun.default_allocator); + defer slice.deinit(); + const host = normalizeHost(slice.slice()); + if (host.len > 0) { + var ssl_ptr: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, this.socket.getNativeHandle()); + if (ssl_ptr.isInitFinished()) { + // match node.js exceptions + globalObject.throw("Already started.", .{}); + return .zero; + } + var host__ = default_allocator.dupeZ(u8, host) catch unreachable; + defer default_allocator.free(host__); + ssl_ptr.setHostname(host__); + } + + return JSValue.jsUndefined(); + } + + pub fn open( + this: *This, + _: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) JSValue { + JSC.markBinding(@src()); + this.socket.open(!this.handlers.is_server); + return JSValue.jsUndefined(); + } + + // this invalidates the current socket returning 2 new sockets + // one for non-TLS and another for TLS + // handlers for non-TLS are preserved + pub fn wrapTLS( + this: *This, + globalObject: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + JSC.markBinding(@src()); + if (comptime ssl) { + return JSValue.jsUndefined(); + } + + if (this.detached) { + return JSValue.jsUndefined(); + } + + const args = callframe.arguments(1); + + if (args.len < 1) { + globalObject.throw("Expected 1 arguments", .{}); + return .zero; + } + + var exception: JSC.C.JSValueRef = null; + + const opts = args.ptr[0]; + if (opts.isEmptyOrUndefinedOrNull() or opts.isBoolean() or !opts.isObject()) { + globalObject.throw("Expected options object", .{}); + return .zero; + } + + var socket_obj = opts.get(globalObject, "socket") orelse { + globalObject.throw("Expected \"socket\" option", .{}); + return .zero; + }; + + var handlers = Handlers.fromJS(globalObject, socket_obj, &exception) orelse { + globalObject.throwValue(exception.?.value()); + return .zero; + }; + + var ssl_opts: ?JSC.API.ServerConfig.SSLConfig = null; + + if (opts.getTruthy(globalObject, "tls")) |tls| { + if (tls.isBoolean()) { + if (tls.toBoolean()) { + ssl_opts = JSC.API.ServerConfig.SSLConfig.zero; + } + } else { + if (JSC.API.ServerConfig.SSLConfig.inJS(globalObject, tls, &exception)) |ssl_config| { + ssl_opts = ssl_config; + } else if (exception != null) { + return .zero; + } + } + } + + if (ssl_opts == null) { + globalObject.throw("Expected \"tls\" option", .{}); + return .zero; + } + + var default_data = JSValue.zero; + if (opts.getTruthy(globalObject, "data")) |default_data_value| { + default_data = default_data_value; + default_data.ensureStillAlive(); + } + + var socket_config = ssl_opts.?; + defer socket_config.deinit(); + const options = socket_config.asUSockets(); + + const protos = socket_config.protos; + const protos_len = socket_config.protos_len; + + const ext_size = @sizeOf(WrappedSocket); + + var tls = handlers.vm.allocator.create(TLSSocket) catch @panic("OOM"); + var handlers_ptr = handlers.vm.allocator.create(Handlers) catch @panic("OOM"); + handlers_ptr.* = handlers; + handlers_ptr.is_server = this.handlers.is_server; + handlers_ptr.protect(); + + tls.* = .{ + .handlers = handlers_ptr, + .this_value = .zero, + .socket = undefined, + .connection = if (this.connection) |c| c.clone() else null, + .wrapped = .tls, + .protos = if (protos) |p| (bun.default_allocator.dupe(u8, p[0..protos_len]) catch unreachable) else null, + }; + + var tls_js_value = tls.getThisValue(globalObject); + TLSSocket.dataSetCached(tls_js_value, globalObject, default_data); + + const TCPHandler = NewWrappedHandler(false); + + // reconfigure context to use the new wrapper handlers + Socket.unsafeConfigure(this.socket.context(), true, true, WrappedSocket, TCPHandler); + const old_context = this.socket.context(); + const TLSHandler = NewWrappedHandler(true); + const new_socket = this.socket.wrapTLS( + options, + ext_size, + true, + WrappedSocket, + TLSHandler, + ) orelse { + handlers_ptr.unprotect(); + handlers.vm.allocator.destroy(handlers_ptr); + bun.default_allocator.destroy(tls); + return JSValue.jsUndefined(); + }; + tls.socket = new_socket; + + var raw = handlers.vm.allocator.create(TLSSocket) catch @panic("OOM"); + var raw_handlers_ptr = handlers.vm.allocator.create(Handlers) catch @panic("OOM"); + this.handlers.unprotect(); + + var cloned_handlers: Handlers = .{ + .vm = globalObject.bunVM(), + .globalObject = globalObject, + .onOpen = this.handlers.onOpen, + .onClose = this.handlers.onClose, + .onData = this.handlers.onData, + .onWritable = this.handlers.onWritable, + .onTimeout = this.handlers.onTimeout, + .onConnectError = this.handlers.onConnectError, + .onEnd = this.handlers.onEnd, + .onError = this.handlers.onError, + .onHandshake = this.handlers.onHandshake, + .binary_type = this.handlers.binary_type, + }; + + raw_handlers_ptr.* = cloned_handlers; + raw_handlers_ptr.is_server = this.handlers.is_server; + raw_handlers_ptr.protect(); + raw.* = .{ + .handlers = raw_handlers_ptr, + .this_value = .zero, + .socket = new_socket, + .connection = if (this.connection) |c| c.clone() else null, + .wrapped = .tcp, + .protos = null, + }; + + var raw_js_value = raw.getThisValue(globalObject); + if (JSSocketType(ssl).dataGetCached(this.getThisValue(globalObject))) |raw_default_data| { + raw_default_data.ensureStillAlive(); + TLSSocket.dataSetCached(raw_js_value, globalObject, raw_default_data); + } + // marks both as active + raw.markActive(); + // this will keep tls alive until socket.open() is called to start TLS certificate and the handshake process + // open is not immediately called because we need to set bunSocketInternal + tls.markActive(); + + // mark both instances on socket data + new_socket.ext(WrappedSocket).?.* = .{ .tcp = raw, .tls = tls }; + + //detach and invalidate the old instance + this.detached = true; + if (this.reffer.has) { + var vm = this.handlers.vm; + this.reffer.unref(vm); + old_context.deinit(ssl); + bun.default_allocator.destroy(this.handlers); + this.poll_ref.unref(vm); + this.has_pending_activity.store(false, .Release); + } + + const array = JSC.JSValue.createEmptyArray(globalObject, 2); + array.putIndex(globalObject, 0, raw_js_value); + array.putIndex(globalObject, 1, tls_js_value); + return array; + } }; } pub const TCPSocket = NewSocket(false); pub const TLSSocket = NewSocket(true); + +pub const WrappedSocket = extern struct { + // both shares the same socket but one behaves as TLS and the other as TCP + tls: *TLSSocket, + tcp: *TLSSocket, +}; + +pub fn NewWrappedHandler(comptime tls: bool) type { + const Socket = uws.NewSocketHandler(true); + return struct { + pub fn onOpen( + this: WrappedSocket, + socket: Socket, + ) void { + // only TLS will call onOpen + if (comptime tls) { + TLSSocket.onOpen(this.tls, socket); + } + } + + pub fn onEnd( + this: WrappedSocket, + socket: Socket, + ) void { + if (comptime tls) { + TLSSocket.onEnd(this.tls, socket); + } else { + TLSSocket.onEnd(this.tcp, socket); + } + } + + pub fn onHandshake( + this: WrappedSocket, + socket: Socket, + success: i32, + ssl_error: uws.us_bun_verify_error_t, + ) void { + // only TLS will call onHandshake + if (comptime tls) { + TLSSocket.onHandshake(this.tls, socket, success, ssl_error); + } + } + + pub fn onClose( + this: WrappedSocket, + socket: Socket, + err: c_int, + data: ?*anyopaque, + ) void { + if (comptime tls) { + TLSSocket.onClose(this.tls, socket, err, data); + } else { + TLSSocket.onClose(this.tcp, socket, err, data); + } + } + + pub fn onData( + this: WrappedSocket, + socket: Socket, + data: []const u8, + ) void { + if (comptime tls) { + TLSSocket.onData(this.tls, socket, data); + } else { + TLSSocket.onData(this.tcp, socket, data); + } + } + + pub fn onWritable( + this: WrappedSocket, + socket: Socket, + ) void { + if (comptime tls) { + TLSSocket.onWritable(this.tls, socket); + } else { + TLSSocket.onWritable(this.tcp, socket); + } + } + pub fn onTimeout( + this: WrappedSocket, + socket: Socket, + ) void { + if (comptime tls) { + TLSSocket.onTimeout(this.tls, socket); + } else { + TLSSocket.onTimeout(this.tcp, socket); + } + } + + pub fn onConnectError( + this: WrappedSocket, + socket: Socket, + errno: c_int, + ) void { + if (comptime tls) { + TLSSocket.onConnectError(this.tls, socket, errno); + } else { + TLSSocket.onConnectError(this.tcp, socket, errno); + } + } + }; +} diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 140e62ce4..f52c08301 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -163,6 +163,8 @@ pub const ServerConfig = struct { request_cert: i32 = 0, reject_unauthorized: i32 = 0, ssl_ciphers: [*c]const u8 = null, + protos: [*c]const u8 = null, + protos_len: usize = 0, const log = Output.scoped(.SSLConfig, false); @@ -215,6 +217,7 @@ pub const ServerConfig = struct { "dh_params_file_name", "passphrase", "ssl_ciphers", + "protos", }; inline for (fields) |field| { @@ -270,6 +273,9 @@ pub const ServerConfig = struct { pub fn inJS(global: *JSC.JSGlobalObject, obj: JSC.JSValue, exception: JSC.C.ExceptionRef) ?SSLConfig { var result = zero; + var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + if (!obj.isObject()) { JSC.throwInvalidArguments("tls option expects an object", .{}, global, exception); return null; @@ -301,7 +307,6 @@ pub const ServerConfig = struct { var i: u32 = 0; var valid_count: u32 = 0; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { @@ -317,7 +322,6 @@ pub const ServerConfig = struct { valid_count += 1; any = true; } else { - arena.deinit(); // mark and free all CA's result.cert = native_array; result.deinit(); @@ -325,7 +329,6 @@ pub const ServerConfig = struct { } } else { global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - arena.deinit(); // mark and free all keys result.key = native_array; result.deinit(); @@ -333,8 +336,6 @@ pub const ServerConfig = struct { } } - arena.deinit(); - if (valid_count == 0) { bun.default_allocator.free(native_array); } else { @@ -356,7 +357,6 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { const sliced = sb.slice(); if (sliced.len > 0) { @@ -369,14 +369,11 @@ pub const ServerConfig = struct { } } else { global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - arena.deinit(); // mark and free all certs result.key = native_array; result.deinit(); return null; } - - arena.deinit(); } } @@ -394,6 +391,22 @@ pub const ServerConfig = struct { } } + if (obj.getTruthy(global, "ALPNProtocols")) |protocols| { + if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols, exception)) |sb| { + const sliced = sb.slice(); + if (sliced.len > 0) { + result.protos = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; + result.protos_len = sliced.len; + } + + any = true; + } else { + global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); + result.deinit(); + return null; + } + } + if (obj.getTruthy(global, "cert")) |js_obj| { if (js_obj.jsType().isArray()) { const count = js_obj.getLength(global); @@ -403,7 +416,6 @@ pub const ServerConfig = struct { var i: u32 = 0; var valid_count: u32 = 0; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { @@ -419,7 +431,6 @@ pub const ServerConfig = struct { valid_count += 1; any = true; } else { - arena.deinit(); // mark and free all CA's result.cert = native_array; result.deinit(); @@ -427,7 +438,6 @@ pub const ServerConfig = struct { } } else { global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - arena.deinit(); // mark and free all certs result.cert = native_array; result.deinit(); @@ -435,8 +445,6 @@ pub const ServerConfig = struct { } } - arena.deinit(); - if (valid_count == 0) { bun.default_allocator.free(native_array); } else { @@ -458,7 +466,6 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { const sliced = sb.slice(); if (sliced.len > 0) { @@ -471,14 +478,11 @@ pub const ServerConfig = struct { } } else { global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - arena.deinit(); // mark and free all certs result.cert = native_array; result.deinit(); return null; } - - arena.deinit(); } } @@ -518,7 +522,6 @@ pub const ServerConfig = struct { var i: u32 = 0; var valid_count: u32 = 0; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); while (i < count) : (i += 1) { const item = js_obj.getIndex(global, i); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), item, exception)) |sb| { @@ -534,7 +537,6 @@ pub const ServerConfig = struct { valid_count += 1; any = true; } else { - arena.deinit(); // mark and free all CA's result.cert = native_array; result.deinit(); @@ -542,7 +544,6 @@ pub const ServerConfig = struct { } } else { global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - arena.deinit(); // mark and free all CA's result.cert = native_array; result.deinit(); @@ -550,8 +551,6 @@ pub const ServerConfig = struct { } } - arena.deinit(); - if (valid_count == 0) { bun.default_allocator.free(native_array); } else { @@ -573,7 +572,6 @@ pub const ServerConfig = struct { } } else { const native_array = bun.default_allocator.alloc([*c]const u8, 1) catch unreachable; - var arena: @import("root").bun.ArenaAllocator = @import("root").bun.ArenaAllocator.init(bun.default_allocator); if (JSC.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj, exception)) |sb| { const sliced = sb.slice(); if (sliced.len > 0) { @@ -586,13 +584,11 @@ pub const ServerConfig = struct { } } else { JSC.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}, global, exception); - arena.deinit(); // mark and free all certs result.ca = native_array; result.deinit(); return null; } - arena.deinit(); } } diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index da07741a3..0c7847e19 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -15,10 +15,21 @@ function generate(ssl) { authorized: { getter: "getAuthorized", }, + alpnProtocol: { + getter: "getALPNProtocol", + }, write: { fn: "write", length: 3, }, + wrapTLS: { + fn: "wrapTLS", + length: 1, + }, + open: { + fn: "open", + length: 0, + }, end: { fn: "end", length: 3, @@ -82,6 +93,11 @@ function generate(ssl) { fn: "reload", length: 1, }, + + setServername: { + fn: "setServername", + length: 1, + }, }, finalize: true, construct: true, diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index 19bf05599..5f99d3792 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-25T17:34:54.187Z +// Generated by 'make generate-sink' at 2023-07-02T16:19:51.440Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 9bf5554c4..41d7065dc 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-25T17:34:54.186Z +// Generated by 'make generate-sink' at 2023-07-02T16:19:51.438Z // #pragma once diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h index f8518bc5e..e4ed81629 100644 --- a/src/bun.js/bindings/JSSinkLookupTable.h +++ b/src/bun.js/bindings/JSSinkLookupTable.h @@ -1,4 +1,4 @@ -// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/silas/Workspace/opensource/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! +// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index b7461b5f0..866970e4d 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -16872,6 +16872,9 @@ extern "C" void* TCPSocketClass__construct(JSC::JSGlobalObject*, JSC::CallFrame* JSC_DECLARE_CUSTOM_GETTER(jsTCPSocketConstructor); extern "C" void TCPSocketClass__finalize(void*); +extern "C" JSC::EncodedJSValue TCPSocketPrototype__getALPNProtocol(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__alpnProtocolGetterWrap); + extern "C" JSC::EncodedJSValue TCPSocketPrototype__getAuthorized(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__authorizedGetterWrap); @@ -16896,6 +16899,9 @@ JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__listenerGetterWrap); extern "C" JSC::EncodedJSValue TCPSocketPrototype__getLocalPort(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__localPortGetterWrap); +extern "C" EncodedJSValue TCPSocketPrototype__open(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__openCallback); + extern "C" JSC::EncodedJSValue TCPSocketPrototype__getReadyState(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__readyStateGetterWrap); @@ -16908,6 +16914,9 @@ JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__reloadCallback); extern "C" JSC::EncodedJSValue TCPSocketPrototype__getRemoteAddress(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TCPSocketPrototype__remoteAddressGetterWrap); +extern "C" EncodedJSValue TCPSocketPrototype__setServername(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__setServernameCallback); + extern "C" EncodedJSValue TCPSocketPrototype__shutdown(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__shutdownCallback); @@ -16917,12 +16926,16 @@ JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__timeoutCallback); extern "C" EncodedJSValue TCPSocketPrototype__unref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__unrefCallback); +extern "C" EncodedJSValue TCPSocketPrototype__wrapTLS(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__wrapTLSCallback); + extern "C" EncodedJSValue TCPSocketPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TCPSocketPrototype__writeCallback); STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTCPSocketPrototype, JSTCPSocketPrototype::Base); static const HashTableValue JSTCPSocketPrototypeTableValues[] = { + { "alpnProtocol"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__alpnProtocolGetterWrap, 0 } }, { "authorized"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__authorizedGetterWrap, 0 } }, { "data"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__dataGetterWrap, TCPSocketPrototype__dataSetterWrap } }, { "end"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__endCallback, 3 } }, @@ -16930,13 +16943,16 @@ static const HashTableValue JSTCPSocketPrototypeTableValues[] = { { "getAuthorizationError"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__getAuthorizationErrorCallback, 0 } }, { "listener"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__listenerGetterWrap, 0 } }, { "localPort"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__localPortGetterWrap, 0 } }, + { "open"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__openCallback, 0 } }, { "readyState"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__readyStateGetterWrap, 0 } }, { "ref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__refCallback, 0 } }, { "reload"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__reloadCallback, 1 } }, { "remoteAddress"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TCPSocketPrototype__remoteAddressGetterWrap, 0 } }, + { "setServername"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__setServernameCallback, 1 } }, { "shutdown"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__shutdownCallback, 1 } }, { "timeout"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__timeoutCallback, 1 } }, { "unref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__unrefCallback, 0 } }, + { "wrapTLS"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__wrapTLSCallback, 1 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TCPSocketPrototype__writeCallback, 3 } } }; @@ -16954,6 +16970,18 @@ JSC_DEFINE_CUSTOM_GETTER(jsTCPSocketConstructor, (JSGlobalObject * lexicalGlobal return JSValue::encode(globalObject->JSTCPSocketConstructor()); } +JSC_DEFINE_CUSTOM_GETTER(TCPSocketPrototype__alpnProtocolGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSTCPSocket* thisObject = jsCast(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + JSC::EncodedJSValue result = TCPSocketPrototype__getALPNProtocol(thisObject->wrapped(), globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, result); +} + JSC_DEFINE_CUSTOM_GETTER(TCPSocketPrototype__authorizedGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); @@ -17113,6 +17141,33 @@ JSC_DEFINE_CUSTOM_GETTER(TCPSocketPrototype__localPortGetterWrap, (JSGlobalObjec RELEASE_AND_RETURN(throwScope, result); } +JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__openCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTCPSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TCPSocketPrototype__open(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_CUSTOM_GETTER(TCPSocketPrototype__readyStateGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); @@ -17210,6 +17265,33 @@ extern "C" EncodedJSValue TCPSocketPrototype__remoteAddressGetCachedValue(JSC::E return JSValue::encode(thisObject->m_remoteAddress.get()); } +JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__setServernameCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTCPSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TCPSocketPrototype__setServername(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__shutdownCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -17291,6 +17373,33 @@ JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__unrefCallback, (JSGlobalObject * le return TCPSocketPrototype__unref(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__wrapTLSCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTCPSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TCPSocketPrototype__wrapTLS(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(TCPSocketPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -17479,6 +17588,9 @@ extern "C" void* TLSSocketClass__construct(JSC::JSGlobalObject*, JSC::CallFrame* JSC_DECLARE_CUSTOM_GETTER(jsTLSSocketConstructor); extern "C" void TLSSocketClass__finalize(void*); +extern "C" JSC::EncodedJSValue TLSSocketPrototype__getALPNProtocol(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__alpnProtocolGetterWrap); + extern "C" JSC::EncodedJSValue TLSSocketPrototype__getAuthorized(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__authorizedGetterWrap); @@ -17503,6 +17615,9 @@ JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__listenerGetterWrap); extern "C" JSC::EncodedJSValue TLSSocketPrototype__getLocalPort(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__localPortGetterWrap); +extern "C" EncodedJSValue TLSSocketPrototype__open(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__openCallback); + extern "C" JSC::EncodedJSValue TLSSocketPrototype__getReadyState(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__readyStateGetterWrap); @@ -17515,6 +17630,9 @@ JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__reloadCallback); extern "C" JSC::EncodedJSValue TLSSocketPrototype__getRemoteAddress(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); JSC_DECLARE_CUSTOM_GETTER(TLSSocketPrototype__remoteAddressGetterWrap); +extern "C" EncodedJSValue TLSSocketPrototype__setServername(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__setServernameCallback); + extern "C" EncodedJSValue TLSSocketPrototype__shutdown(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__shutdownCallback); @@ -17524,12 +17642,16 @@ JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__timeoutCallback); extern "C" EncodedJSValue TLSSocketPrototype__unref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__unrefCallback); +extern "C" EncodedJSValue TLSSocketPrototype__wrapTLS(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__wrapTLSCallback); + extern "C" EncodedJSValue TLSSocketPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(TLSSocketPrototype__writeCallback); STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTLSSocketPrototype, JSTLSSocketPrototype::Base); static const HashTableValue JSTLSSocketPrototypeTableValues[] = { + { "alpnProtocol"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__alpnProtocolGetterWrap, 0 } }, { "authorized"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__authorizedGetterWrap, 0 } }, { "data"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__dataGetterWrap, TLSSocketPrototype__dataSetterWrap } }, { "end"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__endCallback, 3 } }, @@ -17537,13 +17659,16 @@ static const HashTableValue JSTLSSocketPrototypeTableValues[] = { { "getAuthorizationError"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__getAuthorizationErrorCallback, 0 } }, { "listener"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__listenerGetterWrap, 0 } }, { "localPort"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__localPortGetterWrap, 0 } }, + { "open"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__openCallback, 0 } }, { "readyState"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__readyStateGetterWrap, 0 } }, { "ref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__refCallback, 0 } }, { "reload"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__reloadCallback, 1 } }, { "remoteAddress"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, TLSSocketPrototype__remoteAddressGetterWrap, 0 } }, + { "setServername"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__setServernameCallback, 1 } }, { "shutdown"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__shutdownCallback, 1 } }, { "timeout"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__timeoutCallback, 1 } }, { "unref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__unrefCallback, 0 } }, + { "wrapTLS"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__wrapTLSCallback, 1 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, TLSSocketPrototype__writeCallback, 3 } } }; @@ -17561,6 +17686,18 @@ JSC_DEFINE_CUSTOM_GETTER(jsTLSSocketConstructor, (JSGlobalObject * lexicalGlobal return JSValue::encode(globalObject->JSTLSSocketConstructor()); } +JSC_DEFINE_CUSTOM_GETTER(TLSSocketPrototype__alpnProtocolGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSTLSSocket* thisObject = jsCast(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + JSC::EncodedJSValue result = TLSSocketPrototype__getALPNProtocol(thisObject->wrapped(), globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, result); +} + JSC_DEFINE_CUSTOM_GETTER(TLSSocketPrototype__authorizedGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); @@ -17720,6 +17857,33 @@ JSC_DEFINE_CUSTOM_GETTER(TLSSocketPrototype__localPortGetterWrap, (JSGlobalObjec RELEASE_AND_RETURN(throwScope, result); } +JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__openCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTLSSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TLSSocketPrototype__open(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_CUSTOM_GETTER(TLSSocketPrototype__readyStateGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) { auto& vm = lexicalGlobalObject->vm(); @@ -17817,6 +17981,33 @@ extern "C" EncodedJSValue TLSSocketPrototype__remoteAddressGetCachedValue(JSC::E return JSValue::encode(thisObject->m_remoteAddress.get()); } +JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__setServernameCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTLSSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TLSSocketPrototype__setServername(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__shutdownCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -17898,6 +18089,33 @@ JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__unrefCallback, (JSGlobalObject * le return TLSSocketPrototype__unref(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__wrapTLSCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSTLSSocket* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return TLSSocketPrototype__wrapTLS(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(TLSSocketPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 04a72d7ed..a220b6814 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -4426,6 +4426,9 @@ pub const JSTCPSocket = struct { @compileLog("TCPSocket.finalize is not a finalizer"); } + if (@TypeOf(TCPSocket.getALPNProtocol) != GetterType) + @compileLog("Expected TCPSocket.getALPNProtocol to be a getter"); + if (@TypeOf(TCPSocket.getAuthorized) != GetterType) @compileLog("Expected TCPSocket.getAuthorized to be a getter"); @@ -4446,6 +4449,8 @@ pub const JSTCPSocket = struct { if (@TypeOf(TCPSocket.getLocalPort) != GetterType) @compileLog("Expected TCPSocket.getLocalPort to be a getter"); + if (@TypeOf(TCPSocket.open) != CallbackType) + @compileLog("Expected TCPSocket.open to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.open))); if (@TypeOf(TCPSocket.getReadyState) != GetterType) @compileLog("Expected TCPSocket.getReadyState to be a getter"); @@ -4456,18 +4461,23 @@ pub const JSTCPSocket = struct { if (@TypeOf(TCPSocket.getRemoteAddress) != GetterType) @compileLog("Expected TCPSocket.getRemoteAddress to be a getter"); + if (@TypeOf(TCPSocket.setServername) != CallbackType) + @compileLog("Expected TCPSocket.setServername to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.setServername))); if (@TypeOf(TCPSocket.shutdown) != CallbackType) @compileLog("Expected TCPSocket.shutdown to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.shutdown))); if (@TypeOf(TCPSocket.timeout) != CallbackType) @compileLog("Expected TCPSocket.timeout to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.timeout))); if (@TypeOf(TCPSocket.unref) != CallbackType) @compileLog("Expected TCPSocket.unref to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.unref))); + if (@TypeOf(TCPSocket.wrapTLS) != CallbackType) + @compileLog("Expected TCPSocket.wrapTLS to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.wrapTLS))); if (@TypeOf(TCPSocket.write) != CallbackType) @compileLog("Expected TCPSocket.write to be a callback but received " ++ @typeName(@TypeOf(TCPSocket.write))); if (!JSC.is_bindgen) { @export(TCPSocket.end, .{ .name = "TCPSocketPrototype__end" }); @export(TCPSocket.finalize, .{ .name = "TCPSocketClass__finalize" }); @export(TCPSocket.flush, .{ .name = "TCPSocketPrototype__flush" }); + @export(TCPSocket.getALPNProtocol, .{ .name = "TCPSocketPrototype__getALPNProtocol" }); @export(TCPSocket.getAuthorizationError, .{ .name = "TCPSocketPrototype__getAuthorizationError" }); @export(TCPSocket.getAuthorized, .{ .name = "TCPSocketPrototype__getAuthorized" }); @export(TCPSocket.getData, .{ .name = "TCPSocketPrototype__getData" }); @@ -4476,12 +4486,15 @@ pub const JSTCPSocket = struct { @export(TCPSocket.getReadyState, .{ .name = "TCPSocketPrototype__getReadyState" }); @export(TCPSocket.getRemoteAddress, .{ .name = "TCPSocketPrototype__getRemoteAddress" }); @export(TCPSocket.hasPendingActivity, .{ .name = "TCPSocket__hasPendingActivity" }); + @export(TCPSocket.open, .{ .name = "TCPSocketPrototype__open" }); @export(TCPSocket.ref, .{ .name = "TCPSocketPrototype__ref" }); @export(TCPSocket.reload, .{ .name = "TCPSocketPrototype__reload" }); @export(TCPSocket.setData, .{ .name = "TCPSocketPrototype__setData" }); + @export(TCPSocket.setServername, .{ .name = "TCPSocketPrototype__setServername" }); @export(TCPSocket.shutdown, .{ .name = "TCPSocketPrototype__shutdown" }); @export(TCPSocket.timeout, .{ .name = "TCPSocketPrototype__timeout" }); @export(TCPSocket.unref, .{ .name = "TCPSocketPrototype__unref" }); + @export(TCPSocket.wrapTLS, .{ .name = "TCPSocketPrototype__wrapTLS" }); @export(TCPSocket.write, .{ .name = "TCPSocketPrototype__write" }); } } @@ -4581,6 +4594,9 @@ pub const JSTLSSocket = struct { @compileLog("TLSSocket.finalize is not a finalizer"); } + if (@TypeOf(TLSSocket.getALPNProtocol) != GetterType) + @compileLog("Expected TLSSocket.getALPNProtocol to be a getter"); + if (@TypeOf(TLSSocket.getAuthorized) != GetterType) @compileLog("Expected TLSSocket.getAuthorized to be a getter"); @@ -4601,6 +4617,8 @@ pub const JSTLSSocket = struct { if (@TypeOf(TLSSocket.getLocalPort) != GetterType) @compileLog("Expected TLSSocket.getLocalPort to be a getter"); + if (@TypeOf(TLSSocket.open) != CallbackType) + @compileLog("Expected TLSSocket.open to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.open))); if (@TypeOf(TLSSocket.getReadyState) != GetterType) @compileLog("Expected TLSSocket.getReadyState to be a getter"); @@ -4611,18 +4629,23 @@ pub const JSTLSSocket = struct { if (@TypeOf(TLSSocket.getRemoteAddress) != GetterType) @compileLog("Expected TLSSocket.getRemoteAddress to be a getter"); + if (@TypeOf(TLSSocket.setServername) != CallbackType) + @compileLog("Expected TLSSocket.setServername to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.setServername))); if (@TypeOf(TLSSocket.shutdown) != CallbackType) @compileLog("Expected TLSSocket.shutdown to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.shutdown))); if (@TypeOf(TLSSocket.timeout) != CallbackType) @compileLog("Expected TLSSocket.timeout to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.timeout))); if (@TypeOf(TLSSocket.unref) != CallbackType) @compileLog("Expected TLSSocket.unref to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.unref))); + if (@TypeOf(TLSSocket.wrapTLS) != CallbackType) + @compileLog("Expected TLSSocket.wrapTLS to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.wrapTLS))); if (@TypeOf(TLSSocket.write) != CallbackType) @compileLog("Expected TLSSocket.write to be a callback but received " ++ @typeName(@TypeOf(TLSSocket.write))); if (!JSC.is_bindgen) { @export(TLSSocket.end, .{ .name = "TLSSocketPrototype__end" }); @export(TLSSocket.finalize, .{ .name = "TLSSocketClass__finalize" }); @export(TLSSocket.flush, .{ .name = "TLSSocketPrototype__flush" }); + @export(TLSSocket.getALPNProtocol, .{ .name = "TLSSocketPrototype__getALPNProtocol" }); @export(TLSSocket.getAuthorizationError, .{ .name = "TLSSocketPrototype__getAuthorizationError" }); @export(TLSSocket.getAuthorized, .{ .name = "TLSSocketPrototype__getAuthorized" }); @export(TLSSocket.getData, .{ .name = "TLSSocketPrototype__getData" }); @@ -4631,12 +4654,15 @@ pub const JSTLSSocket = struct { @export(TLSSocket.getReadyState, .{ .name = "TLSSocketPrototype__getReadyState" }); @export(TLSSocket.getRemoteAddress, .{ .name = "TLSSocketPrototype__getRemoteAddress" }); @export(TLSSocket.hasPendingActivity, .{ .name = "TLSSocket__hasPendingActivity" }); + @export(TLSSocket.open, .{ .name = "TLSSocketPrototype__open" }); @export(TLSSocket.ref, .{ .name = "TLSSocketPrototype__ref" }); @export(TLSSocket.reload, .{ .name = "TLSSocketPrototype__reload" }); @export(TLSSocket.setData, .{ .name = "TLSSocketPrototype__setData" }); + @export(TLSSocket.setServername, .{ .name = "TLSSocketPrototype__setServername" }); @export(TLSSocket.shutdown, .{ .name = "TLSSocketPrototype__shutdown" }); @export(TLSSocket.timeout, .{ .name = "TLSSocketPrototype__timeout" }); @export(TLSSocket.unref, .{ .name = "TLSSocketPrototype__unref" }); + @export(TLSSocket.wrapTLS, .{ .name = "TLSSocketPrototype__wrapTLS" }); @export(TLSSocket.write, .{ .name = "TLSSocketPrototype__write" }); } } diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp index 1957b404b..231ae0db4 100644 --- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp +++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp @@ -149,7 +149,7 @@ static const HashTableValue JSEventEmitterPrototypeTableValues[] = { { "on"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_addListener, 2 } }, { "once"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_addOnceListener, 2 } }, { "prependListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependListener, 2 } }, - { "prependOnce"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependOnceListener, 2 } }, + { "prependOnceListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_prependOnceListener, 2 } }, { "removeListener"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_removeListener, 2 } }, { "off"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_removeListener, 2 } }, { "removeAllListeners"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsEventEmitterPrototypeFunction_removeAllListeners, 1 } }, diff --git a/src/deps/uws b/src/deps/uws index d82c4a95d..875948226 160000 --- a/src/deps/uws +++ b/src/deps/uws @@ -1 +1 @@ -Subproject commit d82c4a95de3af01614ecb12bfff821611b4cc6b7 +Subproject commit 875948226eede72861a5170212ff6b43c4b7d7f9 diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 8ebe04ac0..5dbe4f5d8 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -40,6 +40,129 @@ pub fn NewSocketHandler(comptime ssl: bool) type { return us_socket_timeout(comptime ssl_int, this.socket, seconds); } + pub fn open(this: ThisSocket, is_client: bool) void { + _ = us_socket_open(comptime ssl_int, this.socket, @intFromBool(is_client), null, 0); + } + + // Note: this assumes that the socket is non-TLS and will be adopted and wrapped with a new TLS context + // context ext will not be copied to the new context, new context will contain us_wrapped_socket_context_t on ext + pub fn wrapTLS( + this: ThisSocket, + options: us_bun_socket_context_options_t, + socket_ext_size: i32, + comptime deref: bool, + comptime ContextType: type, + comptime Fields: anytype, + ) ?NewSocketHandler(true) { + const Type = comptime if (@TypeOf(Fields) != type) @TypeOf(Fields) else Fields; + const TLSSocket = NewSocketHandler(true); + const SocketHandler = struct { + const alignment = if (ContextType == anyopaque) + @sizeOf(usize) + else + std.meta.alignment(ContextType); + const deref_ = deref; + const ValueType = if (deref) ContextType else *ContextType; + fn getValue(socket: *Socket) ValueType { + if (comptime ContextType == anyopaque) { + return us_socket_ext(1, socket).?; + } + + if (comptime deref_) { + return (TLSSocket{ .socket = socket }).ext(ContextType).?.*; + } + + return (TLSSocket{ .socket = socket }).ext(ContextType).?; + } + + pub fn on_open(socket: *Socket, is_client: i32, _: [*c]u8, _: i32) callconv(.C) ?*Socket { + if (comptime @hasDecl(Fields, "onCreate")) { + if (is_client == 0) { + Fields.onCreate( + TLSSocket{ .socket = socket }, + ); + } + } + Fields.onOpen( + getValue(socket), + TLSSocket{ .socket = socket }, + ); + return socket; + } + pub fn on_close(socket: *Socket, code: i32, reason: ?*anyopaque) callconv(.C) ?*Socket { + Fields.onClose( + getValue(socket), + TLSSocket{ .socket = socket }, + code, + reason, + ); + return socket; + } + pub fn on_data(socket: *Socket, buf: ?[*]u8, len: i32) callconv(.C) ?*Socket { + Fields.onData( + getValue(socket), + TLSSocket{ .socket = socket }, + buf.?[0..@intCast(usize, len)], + ); + return socket; + } + pub fn on_writable(socket: *Socket) callconv(.C) ?*Socket { + Fields.onWritable( + getValue(socket), + TLSSocket{ .socket = socket }, + ); + return socket; + } + pub fn on_timeout(socket: *Socket) callconv(.C) ?*Socket { + Fields.onTimeout( + getValue(socket), + TLSSocket{ .socket = socket }, + ); + return socket; + } + pub fn on_connect_error(socket: *Socket, code: i32) callconv(.C) ?*Socket { + Fields.onConnectError( + getValue(socket), + TLSSocket{ .socket = socket }, + code, + ); + return socket; + } + pub fn on_end(socket: *Socket) callconv(.C) ?*Socket { + Fields.onEnd( + getValue(socket), + TLSSocket{ .socket = socket }, + ); + return socket; + } + pub fn on_handshake(socket: *Socket, success: i32, verify_error: us_bun_verify_error_t, _: ?*anyopaque) callconv(.C) void { + Fields.onHandshake(getValue(socket), TLSSocket{ .socket = socket }, success, verify_error); + } + }; + + var events: us_socket_events_t = .{}; + + if (comptime @hasDecl(Type, "onOpen") and @typeInfo(@TypeOf(Type.onOpen)) != .Null) + events.on_open = SocketHandler.on_open; + if (comptime @hasDecl(Type, "onClose") and @typeInfo(@TypeOf(Type.onClose)) != .Null) + events.on_close = SocketHandler.on_close; + if (comptime @hasDecl(Type, "onData") and @typeInfo(@TypeOf(Type.onData)) != .Null) + events.on_data = SocketHandler.on_data; + if (comptime @hasDecl(Type, "onWritable") and @typeInfo(@TypeOf(Type.onWritable)) != .Null) + events.on_writable = SocketHandler.on_writable; + if (comptime @hasDecl(Type, "onTimeout") and @typeInfo(@TypeOf(Type.onTimeout)) != .Null) + events.on_timeout = SocketHandler.on_timeout; + if (comptime @hasDecl(Type, "onConnectError") and @typeInfo(@TypeOf(Type.onConnectError)) != .Null) + events.on_connect_error = SocketHandler.on_connect_error; + if (comptime @hasDecl(Type, "onEnd") and @typeInfo(@TypeOf(Type.onEnd)) != .Null) + events.on_end = SocketHandler.on_end; + if (comptime @hasDecl(Type, "onHandshake") and @typeInfo(@TypeOf(Type.onHandshake)) != .Null) + events.on_handshake = SocketHandler.on_handshake; + + const socket = us_socket_wrap_with_tls(ssl_int, this.socket, options, events, socket_ext_size) orelse return null; + return NewSocketHandler(true).from(socket); + } + pub fn getNativeHandle(this: ThisSocket) *NativeSocketHandleType(ssl) { return @ptrCast(*NativeSocketHandleType(ssl), us_socket_get_native_handle(comptime ssl_int, this.socket).?); } @@ -95,6 +218,17 @@ pub fn NewSocketHandler(comptime ssl: bool) type { @as(i32, @intFromBool(msg_more)), ); } + + pub fn rawWrite(this: ThisSocket, data: []const u8, msg_more: bool) i32 { + return us_socket_raw_write( + comptime ssl_int, + this.socket, + data.ptr, + // truncate to 31 bits since sign bit exists + @intCast(i32, @truncate(u31, data.len)), + @as(i32, @intFromBool(msg_more)), + ); + } pub fn shutdown(this: ThisSocket) void { debug("us_socket_shutdown({d})", .{@intFromPtr(this.socket)}); return us_socket_shutdown( @@ -241,13 +375,126 @@ pub fn NewSocketHandler(comptime ssl: bool) type { return socket_; } + pub fn unsafeConfigure( + ctx: *SocketContext, + comptime ssl_type: bool, + comptime deref: bool, + comptime ContextType: type, + comptime Fields: anytype, + ) void { + const SocketHandlerType = NewSocketHandler(ssl_type); + const ssl_type_int: i32 = @intFromBool(ssl_type); + const Type = comptime if (@TypeOf(Fields) != type) @TypeOf(Fields) else Fields; + + const SocketHandler = struct { + const alignment = if (ContextType == anyopaque) + @sizeOf(usize) + else + std.meta.alignment(ContextType); + const deref_ = deref; + const ValueType = if (deref) ContextType else *ContextType; + fn getValue(socket: *Socket) ValueType { + if (comptime ContextType == anyopaque) { + return us_socket_ext(ssl_type_int, socket).?; + } + + if (comptime deref_) { + return (SocketHandlerType{ .socket = socket }).ext(ContextType).?.*; + } + + return (SocketHandlerType{ .socket = socket }).ext(ContextType).?; + } + + pub fn on_open(socket: *Socket, is_client: i32, _: [*c]u8, _: i32) callconv(.C) ?*Socket { + if (comptime @hasDecl(Fields, "onCreate")) { + if (is_client == 0) { + Fields.onCreate( + SocketHandlerType{ .socket = socket }, + ); + } + } + Fields.onOpen( + getValue(socket), + SocketHandlerType{ .socket = socket }, + ); + return socket; + } + pub fn on_close(socket: *Socket, code: i32, reason: ?*anyopaque) callconv(.C) ?*Socket { + Fields.onClose( + getValue(socket), + SocketHandlerType{ .socket = socket }, + code, + reason, + ); + return socket; + } + pub fn on_data(socket: *Socket, buf: ?[*]u8, len: i32) callconv(.C) ?*Socket { + Fields.onData( + getValue(socket), + SocketHandlerType{ .socket = socket }, + buf.?[0..@intCast(usize, len)], + ); + return socket; + } + pub fn on_writable(socket: *Socket) callconv(.C) ?*Socket { + Fields.onWritable( + getValue(socket), + SocketHandlerType{ .socket = socket }, + ); + return socket; + } + pub fn on_timeout(socket: *Socket) callconv(.C) ?*Socket { + Fields.onTimeout( + getValue(socket), + SocketHandlerType{ .socket = socket }, + ); + return socket; + } + pub fn on_connect_error(socket: *Socket, code: i32) callconv(.C) ?*Socket { + Fields.onConnectError( + getValue(socket), + SocketHandlerType{ .socket = socket }, + code, + ); + return socket; + } + pub fn on_end(socket: *Socket) callconv(.C) ?*Socket { + Fields.onEnd( + getValue(socket), + SocketHandlerType{ .socket = socket }, + ); + return socket; + } + pub fn on_handshake(socket: *Socket, success: i32, verify_error: us_bun_verify_error_t, _: ?*anyopaque) callconv(.C) void { + Fields.onHandshake(getValue(socket), SocketHandlerType{ .socket = socket }, success, verify_error); + } + }; + + if (comptime @hasDecl(Type, "onOpen") and @typeInfo(@TypeOf(Type.onOpen)) != .Null) + us_socket_context_on_open(ssl_int, ctx, SocketHandler.on_open); + if (comptime @hasDecl(Type, "onClose") and @typeInfo(@TypeOf(Type.onClose)) != .Null) + us_socket_context_on_close(ssl_int, ctx, SocketHandler.on_close); + if (comptime @hasDecl(Type, "onData") and @typeInfo(@TypeOf(Type.onData)) != .Null) + us_socket_context_on_data(ssl_int, ctx, SocketHandler.on_data); + if (comptime @hasDecl(Type, "onWritable") and @typeInfo(@TypeOf(Type.onWritable)) != .Null) + us_socket_context_on_writable(ssl_int, ctx, SocketHandler.on_writable); + if (comptime @hasDecl(Type, "onTimeout") and @typeInfo(@TypeOf(Type.onTimeout)) != .Null) + us_socket_context_on_timeout(ssl_int, ctx, SocketHandler.on_timeout); + if (comptime @hasDecl(Type, "onConnectError") and @typeInfo(@TypeOf(Type.onConnectError)) != .Null) + us_socket_context_on_connect_error(ssl_int, ctx, SocketHandler.on_connect_error); + if (comptime @hasDecl(Type, "onEnd") and @typeInfo(@TypeOf(Type.onEnd)) != .Null) + us_socket_context_on_end(ssl_int, ctx, SocketHandler.on_end); + if (comptime @hasDecl(Type, "onHandshake") and @typeInfo(@TypeOf(Type.onHandshake)) != .Null) + us_socket_context_on_handshake(ssl_int, ctx, SocketHandler.on_handshake, null); + } + pub fn configure( ctx: *SocketContext, comptime deref: bool, comptime ContextType: type, comptime Fields: anytype, ) void { - const @"type" = comptime if (@TypeOf(Fields) != type) @TypeOf(Fields) else Fields; + const Type = comptime if (@TypeOf(Fields) != type) @TypeOf(Fields) else Fields; const SocketHandler = struct { const alignment = if (ContextType == anyopaque) @@ -333,21 +580,21 @@ pub fn NewSocketHandler(comptime ssl: bool) type { } }; - if (comptime @hasDecl(@"type", "onOpen") and @typeInfo(@TypeOf(@"type".onOpen)) != .Null) + if (comptime @hasDecl(Type, "onOpen") and @typeInfo(@TypeOf(Type.onOpen)) != .Null) us_socket_context_on_open(ssl_int, ctx, SocketHandler.on_open); - if (comptime @hasDecl(@"type", "onClose") and @typeInfo(@TypeOf(@"type".onClose)) != .Null) + if (comptime @hasDecl(Type, "onClose") and @typeInfo(@TypeOf(Type.onClose)) != .Null) us_socket_context_on_close(ssl_int, ctx, SocketHandler.on_close); - if (comptime @hasDecl(@"type", "onData") and @typeInfo(@TypeOf(@"type".onData)) != .Null) + if (comptime @hasDecl(Type, "onData") and @typeInfo(@TypeOf(Type.onData)) != .Null) us_socket_context_on_data(ssl_int, ctx, SocketHandler.on_data); - if (comptime @hasDecl(@"type", "onWritable") and @typeInfo(@TypeOf(@"type".onWritable)) != .Null) + if (comptime @hasDecl(Type, "onWritable") and @typeInfo(@TypeOf(Type.onWritable)) != .Null) us_socket_context_on_writable(ssl_int, ctx, SocketHandler.on_writable); - if (comptime @hasDecl(@"type", "onTimeout") and @typeInfo(@TypeOf(@"type".onTimeout)) != .Null) + if (comptime @hasDecl(Type, "onTimeout") and @typeInfo(@TypeOf(Type.onTimeout)) != .Null) us_socket_context_on_timeout(ssl_int, ctx, SocketHandler.on_timeout); - if (comptime @hasDecl(@"type", "onConnectError") and @typeInfo(@TypeOf(@"type".onConnectError)) != .Null) + if (comptime @hasDecl(Type, "onConnectError") and @typeInfo(@TypeOf(Type.onConnectError)) != .Null) us_socket_context_on_connect_error(ssl_int, ctx, SocketHandler.on_connect_error); - if (comptime @hasDecl(@"type", "onEnd") and @typeInfo(@TypeOf(@"type".onEnd)) != .Null) + if (comptime @hasDecl(Type, "onEnd") and @typeInfo(@TypeOf(Type.onEnd)) != .Null) us_socket_context_on_end(ssl_int, ctx, SocketHandler.on_end); - if (comptime @hasDecl(@"type", "onHandshake") and @typeInfo(@TypeOf(@"type".onHandshake)) != .Null) + if (comptime @hasDecl(Type, "onHandshake") and @typeInfo(@TypeOf(Type.onHandshake)) != .Null) us_socket_context_on_handshake(ssl_int, ctx, SocketHandler.on_handshake, null); } @@ -659,6 +906,20 @@ pub const us_bun_verify_error_t = extern struct { reason: [*c]const u8 = null, }; +pub const us_socket_events_t = extern struct { + on_open: ?*const fn (*Socket, i32, [*c]u8, i32) callconv(.C) ?*Socket = null, + on_data: ?*const fn (*Socket, [*c]u8, i32) callconv(.C) ?*Socket = null, + on_writable: ?*const fn (*Socket) callconv(.C) ?*Socket = null, + on_close: ?*const fn (*Socket, i32, ?*anyopaque) callconv(.C) ?*Socket = null, + + on_timeout: ?*const fn (*Socket) callconv(.C) ?*Socket = null, + on_long_timeout: ?*const fn (*Socket) callconv(.C) ?*Socket = null, + on_end: ?*const fn (*Socket) callconv(.C) ?*Socket = null, + on_connect_error: ?*const fn (*Socket, i32) callconv(.C) ?*Socket = null, + on_handshake: ?*const fn (*Socket, i32, us_bun_verify_error_t, ?*anyopaque) callconv(.C) void = null, +}; + +pub extern fn us_socket_wrap_with_tls(ssl: i32, s: *Socket, options: us_bun_socket_context_options_t, events: us_socket_events_t, socket_ext_size: i32) ?*Socket; extern fn us_socket_verify_error(ssl: i32, context: *Socket) us_bun_verify_error_t; extern fn SocketContextimestamp(ssl: i32, context: ?*SocketContext) c_ushort; pub extern fn us_socket_context_add_server_name(ssl: i32, context: ?*SocketContext, hostname_pattern: [*c]const u8, options: us_socket_context_options_t, ?*anyopaque) void; @@ -777,11 +1038,16 @@ extern fn us_socket_ext(ssl: i32, s: ?*Socket) ?*anyopaque; extern fn us_socket_context(ssl: i32, s: ?*Socket) ?*SocketContext; extern fn us_socket_flush(ssl: i32, s: ?*Socket) void; extern fn us_socket_write(ssl: i32, s: ?*Socket, data: [*c]const u8, length: i32, msg_more: i32) i32; +extern fn us_socket_raw_write(ssl: i32, s: ?*Socket, data: [*c]const u8, length: i32, msg_more: i32) i32; extern fn us_socket_shutdown(ssl: i32, s: ?*Socket) void; extern fn us_socket_shutdown_read(ssl: i32, s: ?*Socket) void; extern fn us_socket_is_shut_down(ssl: i32, s: ?*Socket) i32; extern fn us_socket_is_closed(ssl: i32, s: ?*Socket) i32; extern fn us_socket_close(ssl: i32, s: ?*Socket, code: i32, reason: ?*anyopaque) ?*Socket; +// if a TLS socket calls this, it will start SSL instance and call open event will also do TLS handshake if required +// will have no effect if the socket is closed or is not TLS +extern fn us_socket_open(ssl: i32, s: ?*Socket, is_client: i32, ip: [*c]const u8, ip_length: i32) ?*Socket; + extern fn us_socket_local_port(ssl: i32, s: ?*Socket) i32; extern fn us_socket_remote_address(ssl: i32, s: ?*Socket, buf: [*c]u8, length: [*c]i32) void; pub const uws_app_s = opaque {}; diff --git a/src/js/node/net.js b/src/js/node/net.js index 430a0dfa2..1b7742dd1 100644 --- a/src/js/node/net.js +++ b/src/js/node/net.js @@ -64,6 +64,7 @@ const bunTlsSymbol = Symbol.for("::buntls::"); const bunSocketServerHandlers = Symbol.for("::bunsocket_serverhandlers::"); const bunSocketServerConnections = Symbol.for("::bunnetserverconnections::"); const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::"); +const bunSocketInternal = Symbol.for("::bunnetsocketinternal::"); var SocketClass; const Socket = (function (InternalSocket) { @@ -117,7 +118,7 @@ const Socket = (function (InternalSocket) { const self = socket.data; socket.timeout(self.timeout); socket.ref(); - self.#socket = socket; + self[bunSocketInternal] = socket; self.connecting = false; self.emit("connect", self); Socket.#Drain(socket); @@ -164,7 +165,7 @@ const Socket = (function (InternalSocket) { if (self.#closed) return; self.#closed = true; //socket cannot be used after close - self.#socket = null; + self[bunSocketInternal] = null; const queue = self.#readQueue; if (queue.isEmpty()) { if (self.push(null)) return; @@ -289,23 +290,33 @@ const Socket = (function (InternalSocket) { localAddress = "127.0.0.1"; #readQueue = createFIFO(); remotePort; - #socket; + [bunSocketInternal] = null; timeout = 0; #writeCallback; #writeChunk; #pendingRead; isServer = false; + _handle; + _parent; + _parentWrap; + #socket; constructor(options) { - const { signal, write, read, allowHalfOpen = false, ...opts } = options || {}; + const { socket, signal, write, read, allowHalfOpen = false, ...opts } = options || {}; super({ ...opts, allowHalfOpen, readable: true, writable: true, }); + this._handle = this; + this._parent = this; + this._parentWrap = this; this.#pendingRead = undefined; + if (socket instanceof Socket) { + this.#socket = socket; + } signal?.once("abort", () => this.destroy()); this.once("connect", () => this.emit("ready")); } @@ -327,7 +338,7 @@ const Socket = (function (InternalSocket) { socket.data = this; socket.timeout(this.timeout); socket.ref(); - this.#socket = socket; + this[bunSocketInternal] = socket; this.connecting = false; this.emit("connect", this); Socket.#Drain(socket); @@ -335,6 +346,7 @@ const Socket = (function (InternalSocket) { connect(port, host, connectListener) { var path; + var connection = this.#socket; if (typeof port === "string") { path = port; port = undefined; @@ -357,6 +369,7 @@ const Socket = (function (InternalSocket) { port, host, path, + socket, // TODOs localAddress, localPort, @@ -371,7 +384,11 @@ const Socket = (function (InternalSocket) { pauseOnConnect, servername, } = port; + this.servername = servername; + if (socket) { + connection = socket; + } } if (!pauseOnConnect) { @@ -399,41 +416,117 @@ const Socket = (function (InternalSocket) { } else { tls.rejectUnauthorized = rejectUnauthorized; tls.requestCert = true; + if (!connection && tls.socket) { + connection = tls.socket; + } + } + } + if (connection) { + if ( + typeof connection !== "object" || + !(connection instanceof Socket) || + typeof connection[bunTlsSymbol] === "function" + ) { + throw new TypeError("socket must be an instance of net.Socket"); } } - this.authorized = false; this.secureConnecting = true; this._secureEstablished = false; this._securePending = true; if (connectListener) this.on("secureConnect", connectListener); } else if (connectListener) this.on("connect", connectListener); - bunConnect( - path - ? { + // start using existing connection + + if (connection) { + const socket = connection[bunSocketInternal]; + + if (socket) { + const result = socket.wrapTLS({ + data: this, + tls, + socket: Socket.#Handlers, + }); + if (result) { + const [raw, tls] = result; + // replace socket + connection[bunSocketInternal] = raw; + raw.timeout(raw.timeout); + raw.connecting = false; + // set new socket + this[bunSocketInternal] = tls; + tls.timeout(tls.timeout); + tls.connecting = true; + this[bunSocketInternal] = socket; + // start tls + tls.open(); + } else { + this[bunSocketInternal] = null; + throw new Error("Invalid socket"); + } + } else { + // wait to be connected + connection.once("connect", () => { + const socket = connection[bunSocketInternal]; + if (!socket) return; + + const result = socket.wrapTLS({ data: this, - unix: path, - socket: Socket.#Handlers, tls, - } - : { - data: this, - hostname: host || "localhost", - port: port, socket: Socket.#Handlers, - tls, - }, - ); + }); + + if (result) { + const [raw, tls] = result; + // replace socket + connection[bunSocketInternal] = raw; + raw.timeout(raw.timeout); + raw.connecting = false; + // set new socket + this[bunSocketInternal] = tls; + tls.timeout(tls.timeout); + tls.connecting = true; + this[bunSocketInternal] = socket; + // start tls + tls.open(); + } else { + this[bunSocketInternal] = null; + throw new Error("Invalid socket"); + } + }); + } + } else if (path) { + // start using unix socket + bunConnect({ + data: this, + unix: path, + socket: Socket.#Handlers, + tls, + }).catch(error => { + this.emit("error", error); + }); + } else { + // default start + bunConnect({ + data: this, + hostname: host || "localhost", + port: port, + socket: Socket.#Handlers, + tls, + }).catch(error => { + this.emit("error", error); + }); + } return this; } _destroy(err, callback) { - this.#socket?.end(); + this[bunSocketInternal]?.end(); callback(err); } _final(callback) { - this.#socket?.end(); + this[bunSocketInternal]?.end(); callback(); } @@ -446,7 +539,7 @@ const Socket = (function (InternalSocket) { } get localPort() { - return this.#socket?.localPort; + return this[bunSocketInternal]?.localPort; } get pending() { @@ -472,11 +565,11 @@ const Socket = (function (InternalSocket) { } ref() { - this.#socket?.ref(); + this[bunSocketInternal]?.ref(); } get remoteAddress() { - return this.#socket?.remoteAddress; + return this[bunSocketInternal]?.remoteAddress; } get remoteFamily() { @@ -484,7 +577,7 @@ const Socket = (function (InternalSocket) { } resetAndDestroy() { - this.#socket?.end(); + this[bunSocketInternal]?.end(); } setKeepAlive(enable = false, initialDelay = 0) { @@ -498,19 +591,19 @@ const Socket = (function (InternalSocket) { } setTimeout(timeout, callback) { - this.#socket?.timeout(timeout); + this[bunSocketInternal]?.timeout(timeout); this.timeout = timeout; if (callback) this.once("timeout", callback); return this; } unref() { - this.#socket?.unref(); + this[bunSocketInternal]?.unref(); } _write(chunk, encoding, callback) { - if (typeof chunk == "string" && encoding !== "utf8") chunk = Buffer.from(chunk, encoding); - var written = this.#socket?.write(chunk); + if (typeof chunk == "string" && encoding !== "ascii") chunk = Buffer.from(chunk, encoding); + var written = this[bunSocketInternal]?.write(chunk); if (written == chunk.length) { callback(); } else if (this.#writeCallback) { diff --git a/src/js/node/tls.js b/src/js/node/tls.js index 356c25cbd..310a36620 100644 --- a/src/js/node/tls.js +++ b/src/js/node/tls.js @@ -1,9 +1,30 @@ // Hardcoded module "node:tls" -import { isTypedArray } from "util/types"; +import { isArrayBufferView, isTypedArray } from "util/types"; import net, { Server as NetServer } from "node:net"; const InternalTCPSocket = net[Symbol.for("::bunternal::")]; - +const bunSocketInternal = Symbol.for("::bunnetsocketinternal::"); + +const { RegExp, Array, String } = globalThis[Symbol.for("Bun.lazy")]("primordials"); +const SymbolReplace = Symbol.replace; +const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace]; +const RegExpPrototypeExec = RegExp.prototype.exec; + +const StringPrototypeStartsWith = String.prototype.startsWith; +const StringPrototypeSlice = String.prototype.slice; +const StringPrototypeIncludes = String.prototype.includes; +const StringPrototypeSplit = String.prototype.split; +const StringPrototypeIndexOf = String.prototype.indexOf; +const StringPrototypeSubstring = String.prototype.substring; +const StringPrototypeEndsWith = String.prototype.endsWith; + +const ArrayPrototypeIncludes = Array.prototype.includes; +const ArrayPrototypeJoin = Array.prototype.join; +const ArrayPrototypeForEach = Array.prototype.forEach; +const ArrayPrototypePush = Array.prototype.push; +const ArrayPrototypeSome = Array.prototype.some; +const ArrayPrototypeReduce = Array.prototype.reduce; function parseCertString() { + // Removed since JAN 2022 Node v18.0.0+ https://github.com/nodejs/node/pull/41479 throwNotImplemented("Not implemented"); } @@ -18,6 +39,164 @@ function isValidTLSArray(obj) { } } +function unfqdn(host) { + return RegExpPrototypeSymbolReplace(/[.]$/, host, ""); +} + +function splitHost(host) { + return StringPrototypeSplit.call(RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host), toLowerCase), "."); +} + +function check(hostParts, pattern, wildcards) { + // Empty strings, null, undefined, etc. never match. + if (!pattern) return false; + + const patternParts = splitHost(pattern); + + if (hostParts.length !== patternParts.length) return false; + + // Pattern has empty components, e.g. "bad..example.com". + if (ArrayPrototypeIncludes.call(patternParts, "")) return false; + + // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no + // good way to detect their encoding or normalize them so we simply + // reject them. Control characters and blanks are rejected as well + // because nothing good can come from accepting them. + const isBad = s => RegExpPrototypeExec.call(/[^\u0021-\u007F]/u, s) !== null; + if (ArrayPrototypeSome.call(patternParts, isBad)) return false; + + // Check host parts from right to left first. + for (let i = hostParts.length - 1; i > 0; i -= 1) { + if (hostParts[i] !== patternParts[i]) return false; + } + + const hostSubdomain = hostParts[0]; + const patternSubdomain = patternParts[0]; + const patternSubdomainParts = StringPrototypeSplit.call(patternSubdomain, "*"); + + // Short-circuit when the subdomain does not contain a wildcard. + // RFC 6125 does not allow wildcard substitution for components + // containing IDNA A-labels (Punycode) so match those verbatim. + if (patternSubdomainParts.length === 1 || StringPrototypeIncludes.call(patternSubdomain, "xn--")) + return hostSubdomain === patternSubdomain; + + if (!wildcards) return false; + + // More than one wildcard is always wrong. + if (patternSubdomainParts.length > 2) return false; + + // *.tld wildcards are not allowed. + if (patternParts.length <= 2) return false; + + const { 0: prefix, 1: suffix } = patternSubdomainParts; + + if (prefix.length + suffix.length > hostSubdomain.length) return false; + + if (!StringPrototypeStartsWith.call(hostSubdomain, prefix)) return false; + + if (!StringPrototypeEndsWith.call(hostSubdomain, suffix)) return false; + + return true; +} + +// This pattern is used to determine the length of escaped sequences within +// the subject alt names string. It allows any valid JSON string literal. +// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly. +const jsonStringPattern = + // eslint-disable-next-line no-control-regex + /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/; + +function splitEscapedAltNames(altNames) { + const result = []; + let currentToken = ""; + let offset = 0; + while (offset !== altNames.length) { + const nextSep = StringPrototypeIndexOf.call(altNames, ", ", offset); + const nextQuote = StringPrototypeIndexOf.call(altNames, '"', offset); + if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { + // There is a quote character and there is no separator before the quote. + currentToken += StringPrototypeSubstring.call(altNames, offset, nextQuote); + const match = RegExpPrototypeExec.call(jsonStringPattern, StringPrototypeSubstring.call(altNames, nextQuote)); + if (!match) { + let error = new SyntaxError("ERR_TLS_CERT_ALTNAME_FORMAT: Invalid subject alternative name string"); + error.name = ERR_TLS_CERT_ALTNAME_FORMAT; + throw error; + } + currentToken += JSON.parse(match[0]); + offset = nextQuote + match[0].length; + } else if (nextSep !== -1) { + // There is a separator and no quote before it. + currentToken += StringPrototypeSubstring.call(altNames, offset, nextSep); + ArrayPrototypePush.call(result, currentToken); + currentToken = ""; + offset = nextSep + 2; + } else { + currentToken += StringPrototypeSubstring.call(altNames, offset); + offset = altNames.length; + } + } + ArrayPrototypePush.call(result, currentToken); + return result; +} +function checkServerIdentity(hostname, cert) { + const subject = cert.subject; + const altNames = cert.subjectaltname; + const dnsNames = []; + const ips = []; + + hostname = "" + hostname; + + if (altNames) { + const splitAltNames = StringPrototypeIncludes.call(altNames, '"') + ? splitEscapedAltNames(altNames) + : StringPrototypeSplit.call(altNames, ", "); + ArrayPrototypeForEach.call(splitAltNames, name => { + if (StringPrototypeStartsWith.call(name, "DNS:")) { + ArrayPrototypePush.call(dnsNames, StringPrototypeSlice.call(name, 4)); + } else if (StringPrototypeStartsWith.call(name, "IP Address:")) { + ArrayPrototypePush.call(ips, canonicalizeIP(StringPrototypeSlice.call(name, 11))); + } + }); + } + + let valid = false; + let reason = "Unknown reason"; + + hostname = unfqdn(hostname); // Remove trailing dot for error messages. + + if (net.isIP(hostname)) { + valid = ArrayPrototypeIncludes.call(ips, canonicalizeIP(hostname)); + if (!valid) reason = `IP: ${hostname} is not in the cert's list: ` + ArrayPrototypeJoin.call(ips, ", "); + } else if (dnsNames.length > 0 || subject?.CN) { + const hostParts = splitHost(hostname); + const wildcard = pattern => check(hostParts, pattern, true); + + if (dnsNames.length > 0) { + valid = ArrayPrototypeSome.call(dnsNames, wildcard); + if (!valid) reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; + } else { + // Match against Common Name only if no supported identifiers exist. + const cn = subject.CN; + + if (ArrayIsArray(cn)) valid = ArrayPrototypeSome.call(cn, wildcard); + else if (cn) valid = wildcard(cn); + + if (!valid) reason = `Host: ${hostname}. is not cert's CN: ${cn}`; + } + } else { + reason = "Cert does not contain a DNS name"; + } + + if (!valid) { + let error = new Error(`ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames: ${reason}`); + error.name = "ERR_TLS_CERT_ALTNAME_INVALID"; + error.reason = reason; + error.host = host; + error.cert = cert; + return error; + } +} + var InternalSecureContext = class SecureContext { context; @@ -83,6 +262,36 @@ function createSecureContext(options) { return new SecureContext(options); } +// Translate some fields from the handle's C-friendly format into more idiomatic +// javascript object representations before passing them back to the user. Can +// be used on any cert object, but changing the name would be semver-major. +function translatePeerCertificate(c) { + if (!c) return null; + + if (c.issuerCertificate != null && c.issuerCertificate !== c) { + c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); + } + if (c.infoAccess != null) { + const info = c.infoAccess; + c.infoAccess = { __proto__: null }; + + // XXX: More key validation? + RegExpPrototypeSymbolReplace(/([^\n:]*):([^\n]*)(?:\n|$)/g, info, (all, key, val) => { + if (val.charCodeAt(0) === 0x22) { + // The translatePeerCertificate function is only + // used on internally created legacy certificate + // objects, and any value that contains a quote + // will always be a valid JSON string literal, + // so this should never throw. + val = JSONParse(val); + } + if (key in c.infoAccess) ArrayPrototypePush.call(c.infoAccess[key], val); + else c.infoAccess[key] = [val]; + }); + } + return c; +} + const buntls = Symbol.for("::buntls::"); var SocketClass; @@ -107,8 +316,22 @@ const TLSSocket = (function (InternalTLSSocket) { })( class TLSSocket extends InternalTCPSocket { #secureContext; - constructor(options) { - super(options); + ALPNProtocols; + #socket; + + constructor(socket, options) { + super(socket instanceof InternalTCPSocket ? options : options || socket); + options = options || socket || {}; + if (typeof options === "object") { + const { ALPNProtocols } = options; + if (ALPNProtocols) { + convertALPNProtocols(ALPNProtocols, this); + } + if (socket instanceof InternalTCPSocket) { + this.#socket = socket; + } + } + this.#secureContext = options.secureContext || createSecureContext(options); this.authorized = false; this.secureConnecting = true; @@ -123,28 +346,52 @@ const TLSSocket = (function (InternalTLSSocket) { secureConnecting = false; _SNICallback; servername; - alpnProtocol; authorized = false; authorizationError; encrypted = true; - exportKeyingMaterial() { - throw Error("Not implented in Bun yet"); + _start() { + // some frameworks uses this _start internal implementation is suposed to start TLS handshake + // on Bun we auto start this after on_open callback and when wrapping we start it after the socket is attached to the net.Socket/tls.Socket } - setMaxSendFragment() { + + exportKeyingMaterial(length, label, context) { + //SSL_export_keying_material throw Error("Not implented in Bun yet"); } - setServername() { + setMaxSendFragment(size) { + // SSL_set_max_send_fragment throw Error("Not implented in Bun yet"); } + setServername(name) { + if (this.isServer) { + let error = new Error("ERR_TLS_SNI_FROM_SERVER: Cannot issue SNI from a TLS server-side socket"); + error.name = "ERR_TLS_SNI_FROM_SERVER"; + throw error; + } + // if the socket is detached we can't set the servername but we set this property so when open will auto set to it + this.servername = name; + this[bunSocketInternal]?.setServername(name); + } setSession() { throw Error("Not implented in Bun yet"); } getPeerCertificate() { + // need to implement peerCertificate on socket.zig + // const cert = this[bunSocketInternal]?.peerCertificate; + // if(cert) { + // return translatePeerCertificate(cert); + // } throw Error("Not implented in Bun yet"); } getCertificate() { + // need to implement certificate on socket.zig + // const cert = this[bunSocketInternal]?.certificate; + // if(cert) { + // It's not a peer cert, but the formatting is identical. + // return translatePeerCertificate(cert); + // } throw Error("Not implented in Bun yet"); } getPeerX509Certificate() { @@ -154,16 +401,17 @@ const TLSSocket = (function (InternalTLSSocket) { throw Error("Not implented in Bun yet"); } - [buntls](port, host) { - var { servername } = this; - if (servername) { - return { - serverName: typeof servername === "string" ? servername : host, - ...this.#secureContext, - }; - } + get alpnProtocol() { + return this[bunSocketInternal]?.alpnProtocol; + } - return true; + [buntls](port, host) { + return { + socket: this.#socket, + ALPNProtocols: this.ALPNProtocols, + serverName: this.servername || host || "localhost", + ...this.#secureContext, + }; } }, ); @@ -177,9 +425,12 @@ class Server extends NetServer { _rejectUnauthorized; _requestCert; servername; + ALPNProtocols; + #checkServerIdentity; constructor(options, secureConnectionListener) { super(options, secureConnectionListener); + this.#checkServerIdentity = options?.checkServerIdentity || checkServerIdentity; this.setSecureContext(options); } emit(event, args) { @@ -197,6 +448,12 @@ class Server extends NetServer { options = options.context; } if (options) { + const { ALPNProtocols } = options; + + if (ALPNProtocols) { + convertALPNProtocols(ALPNProtocols, this); + } + let key = options.key; if (key) { if (!isValidTLSArray(key)) { @@ -277,6 +534,8 @@ class Server extends NetServer { // Client always is NONE on set_verify rejectUnauthorized: isClient ? false : this._rejectUnauthorized, requestCert: isClient ? false : this._requestCert, + ALPNProtocols: this.ALPNProtocols, + checkServerIdentity: this.#checkServerIdentity, }, SocketClass, ]; @@ -296,6 +555,11 @@ const CLIENT_RENEG_LIMIT = 3, DEFAULT_MAX_VERSION = "TLSv1.3", createConnection = (port, host, connectListener) => { if (typeof port === "object") { + port.checkServerIdentity || checkServerIdentity; + const { ALPNProtocols } = port; + if (ALPNProtocols) { + convertALPNProtocols(ALPNProtocols, port); + } // port is option pass Socket options and let connect handle connection options return new TLSSocket(port).connect(port, host, connectListener); } @@ -312,7 +576,55 @@ function getCurves() { return; } -function convertALPNProtocols(protocols, out) {} +// Convert protocols array into valid OpenSSL protocols list +// ("\x06spdy/2\x08http/1.1\x08http/1.0") +function convertProtocols(protocols) { + const lens = new Array(protocols.length); + const buff = Buffer.allocUnsafe( + ArrayPrototypeReduce.call( + protocols, + (p, c, i) => { + const len = Buffer.byteLength(c); + if (len > 255) { + throw new RangeError( + "The byte length of the protocol at index " + `${i} exceeds the maximum length.`, + "<= 255", + len, + true, + ); + } + lens[i] = len; + return p + 1 + len; + }, + 0, + ), + ); + + let offset = 0; + for (let i = 0, c = protocols.length; i < c; i++) { + buff[offset++] = lens[i]; + buff.write(protocols[i], offset); + offset += lens[i]; + } + + return buff; +} + +function convertALPNProtocols(protocols, out) { + // If protocols is Array - translate it into buffer + if (Array.isArray(protocols)) { + out.ALPNProtocols = convertProtocols(protocols); + } else if (isTypedArray(protocols)) { + // Copy new buffer not to be modified by user. + out.ALPNProtocols = Buffer.from(protocols); + } else if (isArrayBufferView(protocols)) { + out.ALPNProtocols = Buffer.from( + protocols.buffer.slice(protocols.byteOffset, protocols.byteOffset + protocols.byteLength), + ); + } else if (Buffer.isBuffer(protocols)) { + out.ALPNProtocols = protocols; + } +} var exports = { [Symbol.for("CommonJS")]: 0, @@ -351,6 +663,7 @@ export { getCurves, parseCertString, SecureContext, + checkServerIdentity, Server, TLSSocket, exports as default, diff --git a/src/js/out/modules/node/net.js b/src/js/out/modules/node/net.js index 164ec6677..c34f86b04 100644 --- a/src/js/out/modules/node/net.js +++ b/src/js/out/modules/node/net.js @@ -26,7 +26,7 @@ var isIPv4 = function(s) { self.emit("listening"); }, createServer = function(options, connectionListener) { return new Server(options, connectionListener); -}, v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])", v4Str = `(${v4Seg}[.]){3}${v4Seg}`, IPv4Reg = new RegExp(`^${v4Str}$`), v6Seg = "(?:[0-9a-fA-F]{1,4})", IPv6Reg = new RegExp("^(" + `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + ")(%[0-9a-zA-Z-.:]{1,})?$"), { Bun, createFIFO, Object } = globalThis[Symbol.for("Bun.lazy")]("primordials"), { connect: bunConnect } = Bun, { setTimeout } = globalThis, bunTlsSymbol = Symbol.for("::buntls::"), bunSocketServerHandlers = Symbol.for("::bunsocket_serverhandlers::"), bunSocketServerConnections = Symbol.for("::bunnetserverconnections::"), bunSocketServerOptions = Symbol.for("::bunnetserveroptions::"), SocketClass, Socket = function(InternalSocket) { +}, v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])", v4Str = `(${v4Seg}[.]){3}${v4Seg}`, IPv4Reg = new RegExp(`^${v4Str}$`), v6Seg = "(?:[0-9a-fA-F]{1,4})", IPv6Reg = new RegExp("^(" + `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + ")(%[0-9a-zA-Z-.:]{1,})?$"), { Bun, createFIFO, Object } = globalThis[Symbol.for("Bun.lazy")]("primordials"), { connect: bunConnect } = Bun, { setTimeout } = globalThis, bunTlsSymbol = Symbol.for("::buntls::"), bunSocketServerHandlers = Symbol.for("::bunsocket_serverhandlers::"), bunSocketServerConnections = Symbol.for("::bunnetserverconnections::"), bunSocketServerOptions = Symbol.for("::bunnetserveroptions::"), bunSocketInternal = Symbol.for("::bunnetsocketinternal::"), SocketClass, Socket = function(InternalSocket) { return SocketClass = InternalSocket, Object.defineProperty(SocketClass.prototype, Symbol.toStringTag, { value: "Socket", enumerable: !1 @@ -62,7 +62,7 @@ var isIPv4 = function(s) { }, open(socket) { const self = socket.data; - socket.timeout(self.timeout), socket.ref(), self.#socket = socket, self.connecting = !1, self.emit("connect", self), Socket2.#Drain(socket); + socket.timeout(self.timeout), socket.ref(), self[bunSocketInternal] = socket, self.connecting = !1, self.emit("connect", self), Socket2.#Drain(socket); }, handshake(socket, success, verifyError) { const { data: self } = socket; @@ -87,7 +87,7 @@ var isIPv4 = function(s) { const self = socket.data; if (self.#closed) return; - self.#closed = !0, self.#socket = null; + self.#closed = !0, self[bunSocketInternal] = null; const queue = self.#readQueue; if (queue.isEmpty()) { if (self.push(null)) @@ -163,21 +163,27 @@ var isIPv4 = function(s) { localAddress = "127.0.0.1"; #readQueue = createFIFO(); remotePort; - #socket; + [bunSocketInternal] = null; timeout = 0; #writeCallback; #writeChunk; #pendingRead; isServer = !1; + _handle; + _parent; + _parentWrap; + #socket; constructor(options) { - const { signal, write, read, allowHalfOpen = !1, ...opts } = options || {}; + const { socket, signal, write, read, allowHalfOpen = !1, ...opts } = options || {}; super({ ...opts, allowHalfOpen, readable: !0, writable: !0 }); - this.#pendingRead = void 0, signal?.once("abort", () => this.destroy()), this.once("connect", () => this.emit("ready")); + if (this._handle = this, this._parent = this, this._parentWrap = this, this.#pendingRead = void 0, socket instanceof Socket2) + this.#socket = socket; + signal?.once("abort", () => this.destroy()), this.once("connect", () => this.emit("ready")); } address() { return { @@ -190,10 +196,10 @@ var isIPv4 = function(s) { return this.writableLength; } #attach(port, socket) { - this.remotePort = port, socket.data = this, socket.timeout(this.timeout), socket.ref(), this.#socket = socket, this.connecting = !1, this.emit("connect", this), Socket2.#Drain(socket); + this.remotePort = port, socket.data = this, socket.timeout(this.timeout), socket.ref(), this[bunSocketInternal] = socket, this.connecting = !1, this.emit("connect", this), Socket2.#Drain(socket); } connect(port, host, connectListener) { - var path; + var path, connection = this.#socket; if (typeof port === "string") { if (path = port, port = void 0, typeof host === "function") connectListener = host, host = void 0; @@ -207,6 +213,7 @@ var isIPv4 = function(s) { port, host, path, + socket, localAddress, localPort, family, @@ -220,7 +227,8 @@ var isIPv4 = function(s) { pauseOnConnect, servername } = port; - this.servername = servername; + if (this.servername = servername, socket) + connection = socket; } if (!pauseOnConnect) this.resume(); @@ -228,36 +236,78 @@ var isIPv4 = function(s) { const bunTLS = this[bunTlsSymbol]; var tls = void 0; if (typeof bunTLS === "function") { - if (tls = bunTLS.call(this, port, host, !0), this._requestCert = !0, this._rejectUnauthorized = rejectUnauthorized, tls) + if (tls = bunTLS.call(this, port, host, !0), this._requestCert = !0, this._rejectUnauthorized = rejectUnauthorized, tls) { if (typeof tls !== "object") tls = { rejectUnauthorized, requestCert: !0 }; - else - tls.rejectUnauthorized = rejectUnauthorized, tls.requestCert = !0; + else if (tls.rejectUnauthorized = rejectUnauthorized, tls.requestCert = !0, !connection && tls.socket) + connection = tls.socket; + } + if (connection) { + if (typeof connection !== "object" || !(connection instanceof Socket2) || typeof connection[bunTlsSymbol] === "function") + throw new TypeError("socket must be an instance of net.Socket"); + } if (this.authorized = !1, this.secureConnecting = !0, this._secureEstablished = !1, this._securePending = !0, connectListener) this.on("secureConnect", connectListener); } else if (connectListener) this.on("connect", connectListener); - return bunConnect(path ? { - data: this, - unix: path, - socket: Socket2.#Handlers, - tls - } : { - data: this, - hostname: host || "localhost", - port, - socket: Socket2.#Handlers, - tls - }), this; + if (connection) { + const socket2 = connection[bunSocketInternal]; + if (socket2) { + const result = socket2.wrapTLS({ + data: this, + tls, + socket: Socket2.#Handlers + }); + if (result) { + const [raw, tls2] = result; + connection[bunSocketInternal] = raw, raw.timeout(raw.timeout), raw.connecting = !1, this[bunSocketInternal] = tls2, tls2.timeout(tls2.timeout), tls2.connecting = !0, this[bunSocketInternal] = socket2, tls2.open(); + } else + throw this[bunSocketInternal] = null, new Error("Invalid socket"); + } else + connection.once("connect", () => { + const socket3 = connection[bunSocketInternal]; + if (!socket3) + return; + const result = socket3.wrapTLS({ + data: this, + tls, + socket: Socket2.#Handlers + }); + if (result) { + const [raw, tls2] = result; + connection[bunSocketInternal] = raw, raw.timeout(raw.timeout), raw.connecting = !1, this[bunSocketInternal] = tls2, tls2.timeout(tls2.timeout), tls2.connecting = !0, this[bunSocketInternal] = socket3, tls2.open(); + } else + throw this[bunSocketInternal] = null, new Error("Invalid socket"); + }); + } else if (path) + bunConnect({ + data: this, + unix: path, + socket: Socket2.#Handlers, + tls + }).catch((error) => { + this.emit("error", error); + }); + else + bunConnect({ + data: this, + hostname: host || "localhost", + port, + socket: Socket2.#Handlers, + tls + }).catch((error) => { + this.emit("error", error); + }); + return this; } _destroy(err, callback) { - this.#socket?.end(), callback(err); + this[bunSocketInternal]?.end(), callback(err); } _final(callback) { - this.#socket?.end(), callback(); + this[bunSocketInternal]?.end(), callback(); } get localAddress() { return "127.0.0.1"; @@ -266,7 +316,7 @@ var isIPv4 = function(s) { return "IPv4"; } get localPort() { - return this.#socket?.localPort; + return this[bunSocketInternal]?.localPort; } get pending() { return this.connecting; @@ -289,16 +339,16 @@ var isIPv4 = function(s) { return this.writable ? "writeOnly" : "closed"; } ref() { - this.#socket?.ref(); + this[bunSocketInternal]?.ref(); } get remoteAddress() { - return this.#socket?.remoteAddress; + return this[bunSocketInternal]?.remoteAddress; } get remoteFamily() { return "IPv4"; } resetAndDestroy() { - this.#socket?.end(); + this[bunSocketInternal]?.end(); } setKeepAlive(enable = !1, initialDelay = 0) { return this; @@ -307,17 +357,17 @@ var isIPv4 = function(s) { return this; } setTimeout(timeout, callback) { - if (this.#socket?.timeout(timeout), this.timeout = timeout, callback) + if (this[bunSocketInternal]?.timeout(timeout), this.timeout = timeout, callback) this.once("timeout", callback); return this; } unref() { - this.#socket?.unref(); + this[bunSocketInternal]?.unref(); } _write(chunk, encoding, callback) { - if (typeof chunk == "string" && encoding !== "utf8") + if (typeof chunk == "string" && encoding !== "ascii") chunk = Buffer.from(chunk, encoding); - var written = this.#socket?.write(chunk); + var written = this[bunSocketInternal]?.write(chunk); if (written == chunk.length) callback(); else if (this.#writeCallback) diff --git a/src/js/out/modules/node/tls.js b/src/js/out/modules/node/tls.js index 4cceadc7f..ca8a13270 100644 --- a/src/js/out/modules/node/tls.js +++ b/src/js/out/modules/node/tls.js @@ -1,4 +1,4 @@ -import {isTypedArray} from "node:util/types"; +import {isArrayBufferView, isTypedArray} from "node:util/types"; import net, {Server as NetServer} from "node:net"; var parseCertString = function() { throwNotImplemented("Not implemented"); @@ -11,18 +11,127 @@ var parseCertString = function() { return !1; return !0; } +}, unfqdn = function(host2) { + return RegExpPrototypeSymbolReplace(/[.]$/, host2, ""); +}, splitHost = function(host2) { + return StringPrototypeSplit.call(RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host2), toLowerCase), "."); +}, check = function(hostParts, pattern, wildcards) { + if (!pattern) + return !1; + const patternParts = splitHost(pattern); + if (hostParts.length !== patternParts.length) + return !1; + if (ArrayPrototypeIncludes.call(patternParts, "")) + return !1; + const isBad = (s) => RegExpPrototypeExec.call(/[^\u0021-\u007F]/u, s) !== null; + if (ArrayPrototypeSome.call(patternParts, isBad)) + return !1; + for (let i = hostParts.length - 1;i > 0; i -= 1) + if (hostParts[i] !== patternParts[i]) + return !1; + const hostSubdomain = hostParts[0], patternSubdomain = patternParts[0], patternSubdomainParts = StringPrototypeSplit.call(patternSubdomain, "*"); + if (patternSubdomainParts.length === 1 || StringPrototypeIncludes.call(patternSubdomain, "xn--")) + return hostSubdomain === patternSubdomain; + if (!wildcards) + return !1; + if (patternSubdomainParts.length > 2) + return !1; + if (patternParts.length <= 2) + return !1; + const { 0: prefix, 1: suffix } = patternSubdomainParts; + if (prefix.length + suffix.length > hostSubdomain.length) + return !1; + if (!StringPrototypeStartsWith.call(hostSubdomain, prefix)) + return !1; + if (!StringPrototypeEndsWith.call(hostSubdomain, suffix)) + return !1; + return !0; +}, splitEscapedAltNames = function(altNames) { + const result = []; + let currentToken = "", offset = 0; + while (offset !== altNames.length) { + const nextSep = StringPrototypeIndexOf.call(altNames, ", ", offset), nextQuote = StringPrototypeIndexOf.call(altNames, '"', offset); + if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { + currentToken += StringPrototypeSubstring.call(altNames, offset, nextQuote); + const match = RegExpPrototypeExec.call(jsonStringPattern, StringPrototypeSubstring.call(altNames, nextQuote)); + if (!match) { + let error = new SyntaxError("ERR_TLS_CERT_ALTNAME_FORMAT: Invalid subject alternative name string"); + throw error.name = ERR_TLS_CERT_ALTNAME_FORMAT, error; + } + currentToken += JSON.parse(match[0]), offset = nextQuote + match[0].length; + } else if (nextSep !== -1) + currentToken += StringPrototypeSubstring.call(altNames, offset, nextSep), ArrayPrototypePush.call(result, currentToken), currentToken = "", offset = nextSep + 2; + else + currentToken += StringPrototypeSubstring.call(altNames, offset), offset = altNames.length; + } + return ArrayPrototypePush.call(result, currentToken), result; +}, checkServerIdentity = function(hostname, cert) { + const { subject, subjectaltname: altNames } = cert, dnsNames = [], ips = []; + if (hostname = "" + hostname, altNames) { + const splitAltNames = StringPrototypeIncludes.call(altNames, '"') ? splitEscapedAltNames(altNames) : StringPrototypeSplit.call(altNames, ", "); + ArrayPrototypeForEach.call(splitAltNames, (name) => { + if (StringPrototypeStartsWith.call(name, "DNS:")) + ArrayPrototypePush.call(dnsNames, StringPrototypeSlice.call(name, 4)); + else if (StringPrototypeStartsWith.call(name, "IP Address:")) + ArrayPrototypePush.call(ips, canonicalizeIP(StringPrototypeSlice.call(name, 11))); + }); + } + let valid = !1, reason = "Unknown reason"; + if (hostname = unfqdn(hostname), net.isIP(hostname)) { + if (valid = ArrayPrototypeIncludes.call(ips, canonicalizeIP(hostname)), !valid) + reason = `IP: ${hostname} is not in the cert's list: ` + ArrayPrototypeJoin.call(ips, ", "); + } else if (dnsNames.length > 0 || subject?.CN) { + const hostParts = splitHost(hostname), wildcard = (pattern) => check(hostParts, pattern, !0); + if (dnsNames.length > 0) { + if (valid = ArrayPrototypeSome.call(dnsNames, wildcard), !valid) + reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; + } else { + const cn = subject.CN; + if (ArrayIsArray(cn)) + valid = ArrayPrototypeSome.call(cn, wildcard); + else if (cn) + valid = wildcard(cn); + if (!valid) + reason = `Host: ${hostname}. is not cert's CN: ${cn}`; + } + } else + reason = "Cert does not contain a DNS name"; + if (!valid) { + let error = new Error(`ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames: ${reason}`); + return error.name = "ERR_TLS_CERT_ALTNAME_INVALID", error.reason = reason, error.host = host, error.cert = cert, error; + } }, SecureContext = function(options) { return new InternalSecureContext(options); }, createSecureContext = function(options) { return new SecureContext(options); -}, createServer = function(options, connectionListener) { +}; +var createServer = function(options, connectionListener) { return new Server(options, connectionListener); }, getCiphers = function() { return DEFAULT_CIPHERS.split(":"); }, getCurves = function() { return; +}, convertProtocols = function(protocols) { + const lens = new Array(protocols.length), buff = Buffer.allocUnsafe(ArrayPrototypeReduce.call(protocols, (p, c, i) => { + const len = Buffer.byteLength(c); + if (len > 255) + throw new RangeError("The byte length of the protocol at index " + `${i} exceeds the maximum length.`, "<= 255", len, !0); + return lens[i] = len, p + 1 + len; + }, 0)); + let offset = 0; + for (let i = 0, c = protocols.length;i < c; i++) + buff[offset++] = lens[i], buff.write(protocols[i], offset), offset += lens[i]; + return buff; }, convertALPNProtocols = function(protocols, out) { -}, InternalTCPSocket = net[Symbol.for("::bunternal::")], InternalSecureContext = class SecureContext2 { + if (Array.isArray(protocols)) + out.ALPNProtocols = convertProtocols(protocols); + else if (isTypedArray(protocols)) + out.ALPNProtocols = Buffer.from(protocols); + else if (isArrayBufferView(protocols)) + out.ALPNProtocols = Buffer.from(protocols.buffer.slice(protocols.byteOffset, protocols.byteOffset + protocols.byteLength)); + else if (Buffer.isBuffer(protocols)) + out.ALPNProtocols = protocols; +}, InternalTCPSocket = net[Symbol.for("::bunternal::")], bunSocketInternal = Symbol.for("::bunnetsocketinternal::"), { RegExp, Array, String } = globalThis[Symbol.for("Bun.lazy")]("primordials"), SymbolReplace = Symbol.replace, RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace], RegExpPrototypeExec = RegExp.prototype.exec, StringPrototypeStartsWith = String.prototype.startsWith, StringPrototypeSlice = String.prototype.slice, StringPrototypeIncludes = String.prototype.includes, StringPrototypeSplit = String.prototype.split, StringPrototypeIndexOf = String.prototype.indexOf, StringPrototypeSubstring = String.prototype.substring, StringPrototypeEndsWith = String.prototype.endsWith, ArrayPrototypeIncludes = Array.prototype.includes, ArrayPrototypeJoin = Array.prototype.join, ArrayPrototypeForEach = Array.prototype.forEach, ArrayPrototypePush = Array.prototype.push, ArrayPrototypeSome = Array.prototype.some, ArrayPrototypeReduce = Array.prototype.reduce, jsonStringPattern = /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/, InternalSecureContext = class SecureContext2 { context; constructor(options) { const context = {}; @@ -73,8 +182,17 @@ var parseCertString = function() { }); }(class TLSSocket2 extends InternalTCPSocket { #secureContext; - constructor(options) { - super(options); + ALPNProtocols; + #socket; + constructor(socket, options) { + super(socket instanceof InternalTCPSocket ? options : options || socket); + if (options = options || socket || {}, typeof options === "object") { + const { ALPNProtocols } = options; + if (ALPNProtocols) + convertALPNProtocols(ALPNProtocols, this); + if (socket instanceof InternalTCPSocket) + this.#socket = socket; + } this.#secureContext = options.secureContext || createSecureContext(options), this.authorized = !1, this.secureConnecting = !0, this._secureEstablished = !1, this._securePending = !0; } _secureEstablished = !1; @@ -84,19 +202,24 @@ var parseCertString = function() { secureConnecting = !1; _SNICallback; servername; - alpnProtocol; authorized = !1; authorizationError; encrypted = !0; - exportKeyingMaterial() { - throw Error("Not implented in Bun yet"); + _start() { } - setMaxSendFragment() { + exportKeyingMaterial(length, label, context) { throw Error("Not implented in Bun yet"); } - setServername() { + setMaxSendFragment(size) { throw Error("Not implented in Bun yet"); } + setServername(name) { + if (this.isServer) { + let error = new Error("ERR_TLS_SNI_FROM_SERVER: Cannot issue SNI from a TLS server-side socket"); + throw error.name = "ERR_TLS_SNI_FROM_SERVER", error; + } + this.servername = name, this[bunSocketInternal]?.setServername(name); + } setSession() { throw Error("Not implented in Bun yet"); } @@ -112,14 +235,16 @@ var parseCertString = function() { getX509Certificate() { throw Error("Not implented in Bun yet"); } - [buntls](port, host) { - var { servername } = this; - if (servername) - return { - serverName: typeof servername === "string" ? servername : host, - ...this.#secureContext - }; - return !0; + get alpnProtocol() { + return this[bunSocketInternal]?.alpnProtocol; + } + [buntls](port, host2) { + return { + socket: this.#socket, + ALPNProtocols: this.ALPNProtocols, + serverName: this.servername || host2 || "localhost", + ...this.#secureContext + }; } }); @@ -132,9 +257,11 @@ class Server extends NetServer { _rejectUnauthorized; _requestCert; servername; + ALPNProtocols; + #checkServerIdentity; constructor(options, secureConnectionListener) { super(options, secureConnectionListener); - this.setSecureContext(options); + this.#checkServerIdentity = options?.checkServerIdentity || checkServerIdentity, this.setSecureContext(options); } emit(event, args) { if (super.emit(event, args), event === "connection") @@ -146,6 +273,9 @@ class Server extends NetServer { if (options instanceof InternalSecureContext) options = options.context; if (options) { + const { ALPNProtocols } = options; + if (ALPNProtocols) + convertALPNProtocols(ALPNProtocols, this); let key = options.key; if (key) { if (!isValidTLSArray(key)) @@ -194,26 +324,33 @@ class Server extends NetServer { setTicketKeys() { throw Error("Not implented in Bun yet"); } - [buntls](port, host, isClient) { + [buntls](port, host2, isClient) { return [ { - serverName: this.servername || host || "localhost", + serverName: this.servername || host2 || "localhost", key: this.key, cert: this.cert, ca: this.ca, passphrase: this.passphrase, secureOptions: this.secureOptions, rejectUnauthorized: isClient ? !1 : this._rejectUnauthorized, - requestCert: isClient ? !1 : this._requestCert + requestCert: isClient ? !1 : this._requestCert, + ALPNProtocols: this.ALPNProtocols, + checkServerIdentity: this.#checkServerIdentity }, SocketClass ]; } } -var CLIENT_RENEG_LIMIT = 3, CLIENT_RENEG_WINDOW = 600, DEFAULT_ECDH_CURVE = "auto", DEFAULT_CIPHERS = "DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", DEFAULT_MIN_VERSION = "TLSv1.2", DEFAULT_MAX_VERSION = "TLSv1.3", createConnection = (port, host, connectListener) => { - if (typeof port === "object") - return new TLSSocket(port).connect(port, host, connectListener); - return new TLSSocket().connect(port, host, connectListener); +var CLIENT_RENEG_LIMIT = 3, CLIENT_RENEG_WINDOW = 600, DEFAULT_ECDH_CURVE = "auto", DEFAULT_CIPHERS = "DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", DEFAULT_MIN_VERSION = "TLSv1.2", DEFAULT_MAX_VERSION = "TLSv1.3", createConnection = (port, host2, connectListener) => { + if (typeof port === "object") { + port.checkServerIdentity; + const { ALPNProtocols } = port; + if (ALPNProtocols) + convertALPNProtocols(ALPNProtocols, port); + return new TLSSocket(port).connect(port, host2, connectListener); + } + return new TLSSocket().connect(port, host2, connectListener); }, connect = createConnection, exports = { [Symbol.for("CommonJS")]: 0, CLIENT_RENEG_LIMIT, @@ -244,6 +381,7 @@ export { createConnection, convertALPNProtocols, connect, + checkServerIdentity, TLSSocket, Server, SecureContext, diff --git a/test/bun.lockb b/test/bun.lockb index f30ca197a..e3a2abdfa 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 398959bd6..3cdaa17e1 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -181,61 +181,6 @@ describe("net.createServer listen", () => { ); }); - it("should listen on the correct port", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49027, - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("::"); - expect(address.port).toStrictEqual(49027); - expect(address.family).toStrictEqual("IPv6"); - server.close(); - done(); - }), - ); - }); - - it("should listen on the correct port with IPV4", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49026, - "0.0.0.0", - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("0.0.0.0"); - expect(address.port).toStrictEqual(49026); - expect(address.family).toStrictEqual("IPv4"); - server.close(); - done(); - }), - ); - }); - it("should listen on unix domain socket", done => { const { mustCall, mustNotCall } = createCallCheckCtx(done); diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts new file mode 100644 index 000000000..791dba88a --- /dev/null +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -0,0 +1,32 @@ +import { TLSSocket, connect } from "tls"; + +it("should work with alpnProtocols", done => { + try { + let socket: TLSSocket | null = connect({ + ALPNProtocols: ["http/1.1"], + host: "bun.sh", + servername: "bun.sh", + port: 443, + rejectUnauthorized: false, + }); + + const timeout = setTimeout(() => { + socket?.end(); + done("timeout"); + }, 3000); + + socket.on("error", err => { + clearTimeout(timeout); + done(err); + }); + + socket.on("secureConnect", () => { + clearTimeout(timeout); + done(socket?.alpnProtocol === "http/1.1" ? undefined : "alpnProtocol is not http/1.1"); + socket?.end(); + socket = null; + }); + } catch (err) { + done(err); + } +}); diff --git a/test/js/node/tls/node-tls-server.test.ts b/test/js/node/tls/node-tls-server.test.ts index 6879d0927..2a6101b9f 100644 --- a/test/js/node/tls/node-tls-server.test.ts +++ b/test/js/node/tls/node-tls-server.test.ts @@ -195,61 +195,6 @@ describe("tls.createServer listen", () => { ); }); - it("should listen on the correct port", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(COMMON_CERT); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49027, - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("::"); - expect(address.port).toStrictEqual(49027); - expect(address.family).toStrictEqual("IPv6"); - server.close(); - done(); - }), - ); - }); - - it("should listen on the correct port with IPV4", done => { - const { mustCall, mustNotCall } = createCallCheckCtx(done); - - const server: Server = createServer(COMMON_CERT); - - let timeout: Timer; - const closeAndFail = () => { - clearTimeout(timeout); - server.close(); - mustNotCall()(); - }; - server.on("error", closeAndFail); - timeout = setTimeout(closeAndFail, 100); - - server.listen( - 49026, - "0.0.0.0", - mustCall(() => { - const address = server.address() as AddressInfo; - expect(address.address).toStrictEqual("0.0.0.0"); - expect(address.port).toStrictEqual(49026); - expect(address.family).toStrictEqual("IPv4"); - server.close(); - done(); - }), - ); - }); - it("should listen on unix domain socket", done => { const { mustCall, mustNotCall } = createCallCheckCtx(done); diff --git a/test/js/third_party/nodemailer/nodemailer.test.ts b/test/js/third_party/nodemailer/nodemailer.test.ts new file mode 100644 index 000000000..265112608 --- /dev/null +++ b/test/js/third_party/nodemailer/nodemailer.test.ts @@ -0,0 +1,15 @@ +import { test, expect, describe } from "bun:test"; +import { bunRun } from "harness"; +import path from "path"; + +describe("nodemailer", () => { + test("basic smtp", async () => { + try { + const info = bunRun(path.join(import.meta.dir, "process-nodemailer-fixture.js")); + expect(info.stdout).toBe("true"); + expect(info.stderr || "").toBe(""); + } catch (err: any) { + expect(err?.message || err).toBe(""); + } + }, 10000); +}); diff --git a/test/js/third_party/nodemailer/package.json b/test/js/third_party/nodemailer/package.json new file mode 100644 index 000000000..08e98074f --- /dev/null +++ b/test/js/third_party/nodemailer/package.json @@ -0,0 +1,6 @@ +{ + "name": "nodemailer", + "dependencies": { + "nodemailer": "6.9.3" + } +} diff --git a/test/js/third_party/nodemailer/process-nodemailer-fixture.js b/test/js/third_party/nodemailer/process-nodemailer-fixture.js new file mode 100644 index 000000000..a54735f26 --- /dev/null +++ b/test/js/third_party/nodemailer/process-nodemailer-fixture.js @@ -0,0 +1,23 @@ +import nodemailer from "nodemailer"; +const account = await nodemailer.createTestAccount(); +const transporter = nodemailer.createTransport({ + host: account.smtp.host, + port: account.smtp.port, + secure: account.smtp.secure, + auth: { + user: account.user, // generated ethereal user + pass: account.pass, // generated ethereal password + }, +}); + +// send mail with defined transport object +let info = await transporter.sendMail({ + from: '"Fred Foo 👻" ', // sender address + to: "example@gmail.com", // list of receivers + subject: "Hello ✔", // Subject line + text: "Hello world?", // plain text body + html: "Hello world?", // html body +}); +const url = nodemailer.getTestMessageUrl(info); +console.log(typeof url === "string" && url.length > 0); +transporter.close(); diff --git a/test/js/web/timers/process-setImmediate-fixture.js b/test/js/web/timers/process-setImmediate-fixture.js new file mode 100644 index 000000000..6ffd91c8d --- /dev/null +++ b/test/js/web/timers/process-setImmediate-fixture.js @@ -0,0 +1,9 @@ +setImmediate(() => { + console.log("setImmediate"); + return { + a: 1, + b: 2, + c: 3, + d: 4, + }; +}); diff --git a/test/js/web/timers/setImmediate.test.js b/test/js/web/timers/setImmediate.test.js index 9cd6fa1c9..d00224e0f 100644 --- a/test/js/web/timers/setImmediate.test.js +++ b/test/js/web/timers/setImmediate.test.js @@ -1,4 +1,6 @@ import { it, expect } from "bun:test"; +import { bunExe, bunEnv } from "harness"; +import path from "path"; it("setImmediate", async () => { var lastID = -1; @@ -45,3 +47,28 @@ it("clearImmediate", async () => { }); expect(called).toBe(false); }); + +it("setImmediate should not keep the process alive forever", async () => { + let process = null; + const success = async () => { + process = Bun.spawn({ + cmd: [bunExe(), "run", path.join(import.meta.dir, "process-setImmediate-fixture.js")], + stdout: "ignore", + env: { + ...bunEnv, + NODE_ENV: undefined, + }, + }); + await process.exited; + process = null; + return true; + }; + + const fail = async () => { + await Bun.sleep(500); + process?.kill(); + return false; + }; + + expect(await Promise.race([success(), fail()])).toBe(true); +}); diff --git a/test/package.json b/test/package.json index 116571879..db0053874 100644 --- a/test/package.json +++ b/test/package.json @@ -16,9 +16,10 @@ "iconv-lite": "0.6.3", "jest-extended": "4.0.0", "lodash": "4.17.21", + "nodemailer": "6.9.3", "prisma": "4.15.0", - "socket.io": "4.6.1", - "socket.io-client": "4.6.1", + "socket.io": "4.7.1", + "socket.io-client": "4.7.1", "supertest": "6.1.6", "svelte": "3.55.1", "typescript": "5.0.2", -- cgit v1.2.3 From 3345a7fc3c81f914000fd5a3c5a24f920a70386a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 3 Jul 2023 20:53:41 -0700 Subject: Allow zero length WebSocket client & server messages (#3488) * Allow zero length WebSocket client & server messages * Add test * Clean this up a little * Clean up these tests a little * Hopefully fix the test failure in release build * Don't copy into the receive buffer * Less flaky --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/server.zig | 53 ----- src/bun.js/bindings/ZigGlobalObject.cpp | 26 +++ src/bun.js/bindings/ZigGlobalObject.h | 2 + src/bun.js/bindings/bindings.zig | 17 ++ src/bun.js/bindings/webcore/WebSocket.cpp | 27 ++- src/bun.js/bindings/webcore/WebSocket.h | 28 +-- src/http/websocket_http_client.zig | 172 +++++++++------ test/js/bun/websocket/websocket-server.test.ts | 292 +++++++++++++++---------- test/js/web/websocket/websocket.test.js | 34 ++- 9 files changed, 391 insertions(+), 260 deletions(-) (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index f52c08301..9625ff693 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -3560,11 +3560,6 @@ pub const ServerWebSocket = struct { if (message_value.asArrayBuffer(globalThis)) |array_buffer| { const buffer = array_buffer.slice(); - if (buffer.len == 0) { - globalThis.throw("publish requires a non-empty message", .{}); - return .zero; - } - const result = if (!publish_to_self) this.websocket.publish(topic_slice.slice(), buffer, .binary, compress) else @@ -3580,9 +3575,6 @@ pub const ServerWebSocket = struct { { var string_slice = message_value.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); @@ -3634,10 +3626,6 @@ pub const ServerWebSocket = struct { var topic_slice = topic_value.toSlice(globalThis, bun.default_allocator); defer topic_slice.deinit(); - if (topic_slice.len == 0) { - globalThis.throw("publishText requires a non-empty topic", .{}); - return .zero; - } const compress = args.len > 1 and compress_value.toBoolean(); @@ -3648,9 +3636,6 @@ pub const ServerWebSocket = struct { var string_slice = message_value.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); @@ -3715,10 +3700,6 @@ pub const ServerWebSocket = struct { }; const buffer = array_buffer.slice(); - if (buffer.len == 0) { - return JSC.JSValue.jsNumber(0); - } - const result = if (!publish_to_self) this.websocket.publish(topic_slice.slice(), buffer, .binary, compress) else @@ -3883,10 +3864,6 @@ pub const ServerWebSocket = struct { } if (message_value.asArrayBuffer(globalThis)) |buffer| { - if (buffer.len == 0) { - return JSValue.jsNumber(0); - } - switch (this.websocket.send(buffer.slice(), .binary, compress, true)) { .backpressure => { log("send() backpressure ({d} bytes)", .{buffer.len}); @@ -3906,9 +3883,6 @@ pub const ServerWebSocket = struct { { var string_slice = message_value.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); switch (this.websocket.send(buffer, .text, compress, true)) { @@ -3960,9 +3934,6 @@ pub const ServerWebSocket = struct { var string_slice = message_value.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); switch (this.websocket.send(buffer, .text, compress, true)) { @@ -3994,9 +3965,6 @@ pub const ServerWebSocket = struct { var string_slice = message_str.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); switch (this.websocket.send(buffer, .text, compress, true)) { @@ -4043,10 +4011,6 @@ pub const ServerWebSocket = struct { return .zero; }; - if (buffer.len == 0) { - return JSValue.jsNumber(0); - } - switch (this.websocket.send(buffer.slice(), .binary, compress, true)) { .backpressure => { log("sendBinary() backpressure ({d} bytes)", .{buffer.len}); @@ -4076,10 +4040,6 @@ pub const ServerWebSocket = struct { const buffer = array_buffer.slice(); - if (buffer.len == 0) { - return JSValue.jsNumber(0); - } - switch (this.websocket.send(buffer, .binary, compress, true)) { .backpressure => { log("sendBinary() backpressure ({d} bytes)", .{buffer.len}); @@ -4416,17 +4376,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { const compress = (compress_value orelse JSValue.jsBoolean(true)).toBoolean(); - if (message_value.isEmptyOrUndefinedOrNull()) { - JSC.JSError(this.vm.allocator, "publish requires a non-empty message", .{}, globalThis, exception); - return .zero; - } - if (message_value.asArrayBuffer(globalThis)) |buffer| { - if (buffer.len == 0) { - JSC.JSError(this.vm.allocator, "publish requires a non-empty message", .{}, globalThis, exception); - return .zero; - } - return JSValue.jsNumber( // if 0, return 0 // else return number of bytes sent @@ -4437,9 +4387,6 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { { var string_slice = message_value.toSlice(globalThis, bun.default_allocator); defer string_slice.deinit(); - if (string_slice.len == 0) { - return JSValue.jsNumber(0); - } const buffer = string_slice.slice(); return JSValue.jsNumber( diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index c00670289..b3236a4a2 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1021,6 +1021,20 @@ JSC_DEFINE_HOST_FUNCTION(functionBunSleepThenCallback, return JSC::JSValue::encode(promise); } +using MicrotaskCallback = void (*)(void*); + +JSC_DEFINE_HOST_FUNCTION(functionNativeMicrotaskTrampoline, + (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSCell* cellPtr = callFrame->uncheckedArgument(0).asCell(); + JSCell* callbackPtr = callFrame->uncheckedArgument(1).asCell(); + + void* cell = reinterpret_cast(cellPtr); + auto* callback = reinterpret_cast(callbackPtr); + callback(cell); + return JSValue::encode(jsUndefined()); +} + JSC_DEFINE_HOST_FUNCTION(functionBunSleep, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -3027,6 +3041,11 @@ void GlobalObject::finishCreation(VM& vm) init.set(JSFunction::create(init.vm, init.owner, 4, "performMicrotaskVariadic"_s, jsFunctionPerformMicrotaskVariadic, ImplementationVisibility::Public)); }); + m_nativeMicrotaskTrampoline.initLater( + [](const Initializer& init) { + init.set(JSFunction::create(init.vm, init.owner, 2, ""_s, functionNativeMicrotaskTrampoline, ImplementationVisibility::Public)); + }); + m_navigatorObject.initLater( [](const Initializer& init) { int cpuCount = 0; @@ -4225,6 +4244,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSFileSinkControllerPrototype.visit(visitor); thisObject->m_JSHTTPSResponseControllerPrototype.visit(visitor); thisObject->m_navigatorObject.visit(visitor); + thisObject->m_nativeMicrotaskTrampoline.visit(visitor); thisObject->m_performanceObject.visit(visitor); thisObject->m_primordialsObject.visit(visitor); thisObject->m_processEnvObject.visit(visitor); @@ -4387,6 +4407,12 @@ extern "C" void JSC__JSGlobalObject__reload(JSC__JSGlobalObject* arg0) globalObject->reload(); } +extern "C" void JSC__JSGlobalObject__queueMicrotaskCallback(Zig::GlobalObject* globalObject, void* ptr, MicrotaskCallback callback) +{ + JSFunction* function = globalObject->nativeMicrotaskTrampoline(); + globalObject->queueMicrotask(function, JSValue(reinterpret_cast(ptr)), JSValue(reinterpret_cast(callback)), jsUndefined(), jsUndefined()); +} + JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* globalObject, JSModuleLoader* loader, JSValue key, JSValue referrer, JSValue origin) diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index a5b802ced..da6ba92a0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -369,6 +369,7 @@ public: mutable WriteBarrier m_thenables[promiseFunctionsSize + 1]; JSObject* navigatorObject(); + JSFunction* nativeMicrotaskTrampoline() { return m_nativeMicrotaskTrampoline.getInitializedOnMainThread(this); } void trackFFIFunction(JSC::JSFunction* function) { @@ -466,6 +467,7 @@ private: */ LazyProperty m_pendingVirtualModuleResultStructure; LazyProperty m_performMicrotaskFunction; + LazyProperty m_nativeMicrotaskTrampoline; LazyProperty m_performMicrotaskVariadicFunction; LazyProperty m_emitReadableNextTickFunction; LazyProperty m_lazyReadableStreamPrototypeMap; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 277172b81..07882d857 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2732,6 +2732,23 @@ pub const JSGlobalObject = extern struct { this.vm().throwError(this, this.createErrorInstance(Output.prettyFmt(fmt, false), args)); } } + extern fn JSC__JSGlobalObject__queueMicrotaskCallback(*JSGlobalObject, *anyopaque, Function: *const (fn (*anyopaque) callconv(.C) void)) void; + pub fn queueMicrotaskCallback( + this: *JSGlobalObject, + ctx_val: anytype, + comptime Function: fn (ctx: @TypeOf(ctx_val)) void, + ) void { + JSC.markBinding(@src()); + const Fn = Function; + const ContextType = @TypeOf(ctx_val); + const Wrapper = struct { + pub fn call(p: *anyopaque) callconv(.C) void { + Fn(bun.cast(ContextType, p)); + } + }; + + JSC__JSGlobalObject__queueMicrotaskCallback(this, ctx_val, &Wrapper.call); + } pub fn queueMicrotask( this: *JSGlobalObject, diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index a346175df..1d6392f44 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -458,8 +458,8 @@ ExceptionOr WebSocket::send(const String& message) return {}; } - if (message.length() > 0) - this->sendWebSocketString(message); + // 0-length is allowed + this->sendWebSocketString(message); return {}; } @@ -477,8 +477,8 @@ ExceptionOr WebSocket::send(ArrayBuffer& binaryData) } char* data = static_cast(binaryData.data()); size_t length = binaryData.byteLength(); - if (length > 0) - this->sendWebSocketData(data, length); + // 0-length is allowed + this->sendWebSocketData(data, length); return {}; } @@ -498,8 +498,8 @@ ExceptionOr WebSocket::send(ArrayBufferView& arrayBufferView) auto buffer = arrayBufferView.unsharedBuffer().get(); char* baseAddress = reinterpret_cast(buffer->data()) + arrayBufferView.byteOffset(); size_t length = arrayBufferView.byteLength(); - if (length > 0) - this->sendWebSocketData(baseAddress, length); + // 0-length is allowed + this->sendWebSocketData(baseAddress, length); return {}; } @@ -1232,14 +1232,19 @@ extern "C" void WebSocket__didCloseWithErrorCode(WebCore::WebSocket* webSocket, extern "C" void WebSocket__didReceiveText(WebCore::WebSocket* webSocket, bool clone, const ZigString* str) { - WTF::String wtf_str = Zig::toString(*str); - if (clone) { - wtf_str = wtf_str.isolatedCopy(); - } - + WTF::String wtf_str = clone ? Zig::toStringCopy(*str) : Zig::toString(*str); webSocket->didReceiveMessage(WTFMove(wtf_str)); } extern "C" void WebSocket__didReceiveBytes(WebCore::WebSocket* webSocket, uint8_t* bytes, size_t len) { webSocket->didReceiveBinaryData({ bytes, len }); } + +extern "C" void WebSocket__incrementPendingActivity(WebCore::WebSocket* webSocket) +{ + webSocket->incPendingActivityCount(); +} +extern "C" void WebSocket__decrementPendingActivity(WebCore::WebSocket* webSocket) +{ + webSocket->decPendingActivityCount(); +} \ No newline at end of file diff --git a/src/bun.js/bindings/webcore/WebSocket.h b/src/bun.js/bindings/webcore/WebSocket.h index 42261cfc4..846bd186b 100644 --- a/src/bun.js/bindings/webcore/WebSocket.h +++ b/src/bun.js/bindings/webcore/WebSocket.h @@ -111,6 +111,20 @@ public: return m_hasPendingActivity.load(); } + void incPendingActivityCount() + { + m_pendingActivityCount++; + ref(); + updateHasPendingActivity(); + } + + void decPendingActivityCount() + { + m_pendingActivityCount--; + deref(); + updateHasPendingActivity(); + } + private: typedef union AnyWebSocket { WebSocketClient* client; @@ -147,20 +161,6 @@ private: void sendWebSocketString(const String& message); void sendWebSocketData(const char* data, size_t length); - void incPendingActivityCount() - { - m_pendingActivityCount++; - ref(); - updateHasPendingActivity(); - } - - void decPendingActivityCount() - { - m_pendingActivityCount--; - deref(); - updateHasPendingActivity(); - } - void failAsynchronously(); enum class BinaryType { Blob, diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index ee0fb9c77..a3ae8c3ba 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -145,6 +145,15 @@ const CppWebSocket = opaque { pub const didCloseWithErrorCode = WebSocket__didCloseWithErrorCode; pub const didReceiveText = WebSocket__didReceiveText; pub const didReceiveBytes = WebSocket__didReceiveBytes; + extern fn WebSocket__incrementPendingActivity(websocket_context: *CppWebSocket) void; + extern fn WebSocket__decrementPendingActivity(websocket_context: *CppWebSocket) void; + pub fn ref(this: *CppWebSocket) void { + WebSocket__incrementPendingActivity(this); + } + + pub fn unref(this: *CppWebSocket) void { + WebSocket__decrementPendingActivity(this); + } }; const body_buf_len = 16384 - 16; @@ -163,8 +172,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { to_send: []const u8 = "", read_length: usize = 0, headers_buf: [128]PicoHTTP.Header = undefined, - body_buf: ?*BodyBuf = null, - body_written: usize = 0, + body: std.ArrayListUnmanaged(u8) = .{}, websocket_protocol: u64 = 0, hostname: [:0]const u8 = "", poll_ref: JSC.PollRef = .{}, @@ -280,10 +288,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.poll_ref.unrefOnNextTick(JSC.VirtualMachine.get()); this.clearInput(); - if (this.body_buf) |buf| { - this.body_buf = null; - buf.release(); - } + this.body.clearAndFree(bun.default_allocator); } pub fn cancel(this: *HTTPClient) callconv(.C) void { this.clearData(); @@ -355,14 +360,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.to_send = this.input_body_buf[@intCast(usize, wrote)..]; } - fn getBody(this: *HTTPClient) *BodyBufBytes { - if (this.body_buf == null) { - this.body_buf = BodyBufPool.get(bun.default_allocator); - } - - return &this.body_buf.?.data; - } - pub fn handleData(this: *HTTPClient, socket: Socket, data: []const u8) void { log("onData", .{}); std.debug.assert(socket.socket == this.tcp.socket); @@ -374,43 +371,37 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { if (comptime Environment.allow_assert) std.debug.assert(!socket.isShutdown()); - var body = this.getBody(); - var remain = body[this.body_written..]; - const is_first = this.body_written == 0; + var body = data; + if (this.body.items.len > 0) { + this.body.appendSlice(bun.default_allocator, data) catch @panic("out of memory"); + body = this.body.items; + } + + const is_first = this.body.items.len == 0; if (is_first) { // fail early if we receive a non-101 status code - if (!strings.hasPrefixComptime(data, "HTTP/1.1 101 ")) { + if (!strings.hasPrefixComptime(body, "HTTP/1.1 101 ")) { this.terminate(ErrorCode.expected_101_status_code); return; } } - const to_write = remain[0..@min(remain.len, data.len)]; - if (data.len > 0 and to_write.len > 0) { - @memcpy(remain[0..to_write.len], data[0..to_write.len]); - this.body_written += to_write.len; - } - - const overflow = data[to_write.len..]; - - const available_to_read = body[0..this.body_written]; - const response = PicoHTTP.Response.parse(available_to_read, &this.headers_buf) catch |err| { + const response = PicoHTTP.Response.parse(body, &this.headers_buf) catch |err| { switch (err) { error.Malformed_HTTP_Response => { this.terminate(ErrorCode.invalid_response); return; }, error.ShortRead => { - if (overflow.len > 0) { - this.terminate(ErrorCode.headers_too_large); - return; + if (this.body.items.len == 0) { + this.body.appendSlice(bun.default_allocator, data) catch @panic("out of memory"); } return; }, } }; - this.processResponse(response, available_to_read[@intCast(usize, response.bytes_read)..]); + this.processResponse(response, body[@intCast(usize, response.bytes_read)..]); } pub fn handleEnd(this: *HTTPClient, socket: Socket) void { @@ -420,8 +411,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { } pub fn processResponse(this: *HTTPClient, response: PicoHTTP.Response, remain_buf: []const u8) void { - std.debug.assert(this.body_written > 0); - var upgrade_header = PicoHTTP.Header{ .name = "", .value = "" }; var connection_header = PicoHTTP.Header{ .name = "", .value = "" }; var websocket_accept_header = PicoHTTP.Header{ .name = "", .value = "" }; @@ -524,7 +513,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.terminate(ErrorCode.invalid_response); return; }; - if (remain_buf.len > 0) @memcpy(overflow[0..remain_buf.len], remain_buf); + @memcpy(overflow, remain_buf); } this.clearData(); @@ -866,6 +855,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { globalThis: *JSC.JSGlobalObject, poll_ref: JSC.PollRef = JSC.PollRef.init(), + initial_data_handler: ?*InitialDataHandler = null, + pub const name = if (ssl) "WebSocketClientTLS" else "WebSocketClient"; pub const shim = JSC.Shimmer("Bun", name, @This()); @@ -927,6 +918,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { JSC.markBinding(@src()); if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; + log("fail ({s})", .{@tagName(code)}); + ws.didCloseWithErrorCode(code); } @@ -937,7 +930,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { _ = socket; _ = ssl_error; JSC.markBinding(@src()); - log("WebSocket.onHandshake({d})", .{success}); + log("onHandshake({d})", .{success}); JSC.markBinding(@src()); if (success == 0) { if (this.outgoing_websocket) |ws| { @@ -1044,6 +1037,24 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { } pub fn handleData(this: *WebSocket, socket: Socket, data_: []const u8) void { + // Due to scheduling, it is possible for the websocket onData + // handler to run with additional data before the microtask queue is + // drained. + if (this.initial_data_handler) |initial_handler| { + // This calls `handleData` + // We deliberately do not set this.initial_data_handler to null here, that's done in handleWithoutDeinit. + // We do not free the memory here since the lifetime is managed by the microtask queue (it should free when called from there) + initial_handler.handleWithoutDeinit(); + + // handleWithoutDeinit is supposed to clear the handler from WebSocket* + // to prevent an infinite loop + std.debug.assert(this.initial_data_handler == null); + + // If we disconnected for any reason in the re-entrant case, we should just ignore the data + if (this.outgoing_websocket == null or this.tcp.isShutdown() or this.tcp.isClosed()) + return; + } + var data = data_; var receive_state = this.receive_state; var terminated = false; @@ -1141,6 +1152,30 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { terminated = true; break; } + + // Handle when the payload length is 0, but it is a message + // + // This should become + // + // - ArrayBuffer(0) + // - "" + // - Buffer(0) (etc) + // + if (receive_body_remain == 0 and receive_state == .need_body and is_final) { + _ = this.consume( + "", + receive_body_remain, + last_receive_data_type, + is_final, + ); + + // Return to the header state to read the next frame + receive_state = .need_header; + is_fragmented = false; + + // Bail out if there's nothing left to read + if (data.len == 0) break; + } }, .need_mask => { this.terminate(.unexpected_mask_from_server); @@ -1201,6 +1236,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (data.len == 0) break; }, .need_body => { + // Empty messages are valid, but we handle that earlier in the flow. if (receive_body_remain == 0 and data.len > 0) { this.terminate(ErrorCode.expected_control_frame); terminated = true; @@ -1434,9 +1470,6 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { return; } - if (len == 0) - return; - const slice = ptr[0..len]; const bytes = Copy{ .bytes = slice }; // fast path: small frame, no backpressure, attempt to send without allocating @@ -1460,9 +1493,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { return; } - if (str.len == 0) { - return; - } + // Note: 0 is valid { var inline_buf: [stack_frame_size]u8 = undefined; @@ -1525,6 +1556,33 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { this.sendCloseWithBody(this.tcp, code, null, 0); } + const InitialDataHandler = struct { + adopted: ?*WebSocket, + ws: *CppWebSocket, + slice: []u8, + + pub const Handle = JSC.AnyTask.New(@This(), handle); + + pub fn handleWithoutDeinit(this: *@This()) void { + var this_socket = this.adopted orelse return; + this.adopted = null; + this_socket.initial_data_handler = null; + var ws = this.ws; + defer ws.unref(); + + if (this_socket.outgoing_websocket != null) + this_socket.handleData(this_socket.tcp, this.slice); + } + + pub fn handle(this: *@This()) void { + defer { + bun.default_allocator.free(this.slice); + bun.default_allocator.destroy(this); + } + this.handleWithoutDeinit(); + } + }; + pub fn init( outgoing: *CppWebSocket, input_socket: *anyopaque, @@ -1554,33 +1612,19 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { var buffered_slice: []u8 = buffered_data[0..buffered_data_len]; if (buffered_slice.len > 0) { - const InitialDataHandler = struct { - adopted: *WebSocket, - slice: []u8, - task: JSC.AnyTask = undefined, - - pub const Handle = JSC.AnyTask.New(@This(), handle); - - pub fn handle(this: *@This()) void { - defer { - bun.default_allocator.free(this.slice); - bun.default_allocator.destroy(this); - } - - this.adopted.receive_buffer.ensureUnusedCapacity(this.slice.len) catch return; - var writable = this.adopted.receive_buffer.writableSlice(0); - @memcpy(writable[0..this.slice.len], this.slice); - - this.adopted.handleData(this.adopted.tcp, writable); - } - }; var initial_data = bun.default_allocator.create(InitialDataHandler) catch unreachable; initial_data.* = .{ .adopted = adopted, .slice = buffered_slice, + .ws = outgoing, }; - initial_data.task = InitialDataHandler.Handle.init(initial_data); - globalThis.bunVM().eventLoop().enqueueTask(JSC.Task.init(&initial_data.task)); + + // Use a higher-priority callback for the initial onData handler + globalThis.queueMicrotaskCallback(initial_data, InitialDataHandler.handle); + + // We need to ref the outgoing websocket so that it doesn't get finalized + // before the initial data handler is called + outgoing.ref(); } return @ptrCast( *anyopaque, diff --git a/test/js/bun/websocket/websocket-server.test.ts b/test/js/bun/websocket/websocket-server.test.ts index 2c2352f91..7913147f9 100644 --- a/test/js/bun/websocket/websocket-server.test.ts +++ b/test/js/bun/websocket/websocket-server.test.ts @@ -3,6 +3,77 @@ import { gcTick } from "harness"; import { serve, ServerWebSocket } from "bun"; describe("websocket server", () => { + it("send & receive empty messages", done => { + const serverReceived: any[] = []; + const clientReceived: any[] = []; + var clientDone = false; + var serverDone = false; + + let server = Bun.serve({ + websocket: { + open(ws) { + ws.send(""); + ws.send(new ArrayBuffer(0)); + }, + message(ws, data) { + serverReceived.push(data); + + if (serverReceived.length === 2) { + if (serverReceived.find(d => d === "") === undefined) { + done(new Error("expected empty string")); + } + + if (!serverReceived.find(d => d.byteLength === 0)) { + done(new Error("expected empty Buffer")); + } + + serverDone = true; + + if (clientDone && serverDone) { + z.close(); + server.stop(true); + done(); + } + } + }, + close() {}, + }, + fetch(req, server) { + if (!server.upgrade(req)) { + return new Response(null, { status: 404 }); + } + }, + port: 0, + }); + + let z = new WebSocket(`ws://${server.hostname}:${server.port}`); + z.onmessage = e => { + clientReceived.push(e.data); + + if (clientReceived.length === 2) { + if (clientReceived.find(d => d === "") === undefined) { + done(new Error("expected empty string")); + } + + if (!clientReceived.find(d => d.byteLength === 0)) { + done(new Error("expected empty Buffer")); + } + + clientDone = true; + if (clientDone && serverDone) { + server.stop(true); + z.close(); + + done(); + } + } + }; + z.addEventListener("open", () => { + z.send(""); + z.send(new Buffer(0)); + }); + }); + it("remoteAddress works", done => { let server = Bun.serve({ websocket: { @@ -859,16 +930,13 @@ describe("websocket server", () => { const server = serve({ port: 0, websocket: { - open(ws) { - server.stop(); - }, + open(ws) {}, message(ws, msg) { ws.send(sendQueue[serverCounter++] + " "); - gcTick(); + serverCounter % 10 === 0 && gcTick(); }, }, fetch(req, server) { - server.stop(); if ( server.upgrade(req, { data: { count: 0 }, @@ -879,32 +947,39 @@ describe("websocket server", () => { return new Response("noooooo hello world"); }, }); + try { + await new Promise((resolve, reject) => { + const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); + websocket.onerror = e => { + reject(e); + }; - await new Promise((resolve, reject) => { - const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); - websocket.onerror = e => { - reject(e); - }; + websocket.onopen = () => { + server.stop(); + websocket.send("first"); + }; - var counter = 0; - websocket.onopen = () => websocket.send("first"); - websocket.onmessage = e => { - try { - const expected = sendQueue[clientCounter++] + " "; - expect(e.data).toBe(expected); - websocket.send("next"); - if (clientCounter === sendQueue.length) { + websocket.onmessage = e => { + try { + const expected = sendQueue[clientCounter++] + " "; + expect(e.data).toBe(expected); + websocket.send("next"); + if (clientCounter === sendQueue.length) { + websocket.close(); + resolve(); + } + } catch (r) { + reject(r); + console.error(r); websocket.close(); - resolve(); } - } catch (r) { - reject(r); - console.error(r); - websocket.close(); - } - }; - }); - server.stop(true); + }; + }); + } catch (e) { + throw e; + } finally { + server.stop(true); + } }); // this test sends 100 messages to 10 connected clients via pubsub @@ -913,20 +988,15 @@ describe("websocket server", () => { var sendQueue: any[] = []; for (var i = 0; i < 100; i++) { sendQueue.push(ropey + " " + i); - gcTick(); } + var serverCounter = 0; var clientCount = 0; const server = serve({ port: 0, websocket: { - // FIXME: update this test to not rely on publishToSelf: true, - publishToSelf: true, - open(ws) { - server.stop(); ws.subscribe("test"); - gcTick(); if (!ws.isSubscribed("test")) { throw new Error("not subscribed"); } @@ -936,15 +1006,15 @@ describe("websocket server", () => { } ws.subscribe("test"); clientCount++; - if (clientCount === 10) setTimeout(() => ws.publish("test", "hello world"), 1); + if (clientCount === 10) { + setTimeout(() => server.publish("test", "hello world"), 1); + } }, message(ws, msg) { - if (serverCounter < sendQueue.length) ws.publish("test", sendQueue[serverCounter++] + " "); + if (serverCounter < sendQueue.length) server.publish("test", sendQueue[serverCounter++] + " "); }, }, fetch(req) { - gcTick(); - server.stop(); if ( server.upgrade(req, { data: { count: 0 }, @@ -954,89 +1024,89 @@ describe("websocket server", () => { return new Response("noooooo hello world"); }, }); + try { + const connections = new Array(10); + const websockets = new Array(connections.length); + var doneCounter = 0; + await new Promise(done => { + for (var i = 0; i < connections.length; i++) { + var j = i; + var resolve: (_?: unknown) => void, + reject: (_?: unknown) => void, + resolveConnection: (_?: unknown) => void, + rejectConnection: (_?: unknown) => void; + connections[j] = new Promise((res, rej) => { + resolveConnection = res; + rejectConnection = rej; + }); + websockets[j] = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); + websocket.onerror = e => { + reject(e); + }; + websocket.onclose = () => { + doneCounter++; + if (doneCounter === connections.length) { + done(); + } + }; + var hasOpened = false; + websocket.onopen = () => { + if (!hasOpened) { + hasOpened = true; + resolve(websocket); + } + }; - const connections = new Array(10); - const websockets = new Array(connections.length); - var doneCounter = 0; - await new Promise(done => { - for (var i = 0; i < connections.length; i++) { - var j = i; - var resolve: (_?: unknown) => void, - reject: (_?: unknown) => void, - resolveConnection: (_?: unknown) => void, - rejectConnection: (_?: unknown) => void; - connections[j] = new Promise((res, rej) => { - resolveConnection = res; - rejectConnection = rej; - }); - websockets[j] = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - gcTick(); - const websocket = new WebSocket(`ws://${server.hostname}:${server.port}`); - websocket.onerror = e => { - reject(e); - }; - websocket.onclose = () => { - doneCounter++; - if (doneCounter === connections.length) { - done(); - } - }; - var hasOpened = false; - websocket.onopen = () => { - if (!hasOpened) { - hasOpened = true; - resolve(websocket); - } - }; - - let clientCounter = -1; - var hasSentThisTick = false; - - websocket.onmessage = e => { - gcTick(); - - if (!hasOpened) { - hasOpened = true; - resolve(websocket); - } + let clientCounter = -1; + var hasSentThisTick = false; - if (e.data === "hello world") { - clientCounter = 0; - websocket.send("first"); - return; - } + websocket.onmessage = e => { + if (!hasOpened) { + hasOpened = true; + resolve(websocket); + } - try { - expect(!!sendQueue.find(a => a + " " === e.data)).toBe(true); - - if (!hasSentThisTick) { - websocket.send("second"); - hasSentThisTick = true; - queueMicrotask(() => { - hasSentThisTick = false; - }); + if (e.data === "hello world") { + clientCounter = 0; + websocket.send("first"); + return; } - gcTick(); + try { + expect(!!sendQueue.find(a => a + " " === e.data)).toBe(true); + + if (!hasSentThisTick) { + websocket.send("second"); + hasSentThisTick = true; + queueMicrotask(() => { + hasSentThisTick = false; + }); + } - if (clientCounter++ === sendQueue.length - 1) { + if (clientCounter++ === sendQueue.length - 1) { + websocket.close(); + resolveConnection(); + } + } catch (r) { + console.error(r); websocket.close(); - resolveConnection(); + rejectConnection(r); } - } catch (r) { - console.error(r); - websocket.close(); - rejectConnection(r); - gcTick(); - } - }; - } - }); + }; + } + }); + } catch (e) { + throw e; + } finally { + server.stop(true); + gcTick(); + } + expect(serverCounter).toBe(sendQueue.length); - server.stop(true); }, 30_000); it("can close with reason and code #2631", done => { let timeout: any; diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index 867b86123..76ff16ecb 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -6,16 +6,33 @@ const TEST_WEBSOCKET_HOST = process.env.TEST_WEBSOCKET_HOST || "wss://ws.postman describe("WebSocket", () => { it("should connect", async () => { - const ws = new WebSocket(TEST_WEBSOCKET_HOST); - await new Promise((resolve, reject) => { + const server = Bun.serve({ + port: 0, + fetch(req, server) { + if (server.upgrade(req)) { + server.stop(); + return; + } + + return new Response(); + }, + websocket: { + open(ws) {}, + message(ws) { + ws.close(); + }, + }, + }); + const ws = new WebSocket(`ws://${server.hostname}:${server.port}`, {}); + await new Promise(resolve => { ws.onopen = resolve; - ws.onerror = reject; }); - var closed = new Promise((resolve, reject) => { + var closed = new Promise(resolve => { ws.onclose = resolve; }); ws.close(); await closed; + server.stop(true); }); it("should connect over https", async () => { @@ -59,17 +76,18 @@ describe("WebSocket", () => { const server = Bun.serve({ port: 0, fetch(req, server) { - server.stop(); done(); + server.stop(); return new Response(); }, websocket: { - open(ws) { + open(ws) {}, + message(ws) { ws.close(); }, }, }); - const ws = new WebSocket(`http://${server.hostname}:${server.port}`, {}); + new WebSocket(`http://${server.hostname}:${server.port}`, {}); }); describe("nodebuffer", () => { it("should support 'nodebuffer' binaryType", done => { @@ -93,6 +111,7 @@ describe("WebSocket", () => { expect(ws.binaryType).toBe("nodebuffer"); Bun.gc(true); ws.onmessage = ({ data }) => { + ws.close(); expect(Buffer.isBuffer(data)).toBe(true); expect(data).toEqual(new Uint8Array([1, 2, 3])); server.stop(true); @@ -117,6 +136,7 @@ describe("WebSocket", () => { ws.sendBinary(new Uint8Array([1, 2, 3])); setTimeout(() => { client.onmessage = ({ data }) => { + client.close(); expect(Buffer.isBuffer(data)).toBe(true); expect(data).toEqual(new Uint8Array([1, 2, 3])); server.stop(true); -- cgit v1.2.3 From 2f5e4fffe9554fcc7afa6980b3af6b33bc3a3a5e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 9 Jul 2023 21:50:19 -0700 Subject: Implement process.memoryUsage() and process.cpuUsage() (#3586) * Implement process.memoryUsage() and process.cpuUsage() * Avoid mi_process_info * Update bench * Update Process.cpp * fixup * More tests + linux fixup * Skip it for now since it seems less accurate --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- bench/snippets/process-info.mjs | 33 ++ src/bun.js/bindings/Process.cpp | 388 ++++++++++++++++++++- src/bun.js/bindings/Process.h | 18 + src/bun.js/bindings/Process.lut.h | 97 +++--- .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + test/js/node/process/process.test.js | 142 +++++++- 7 files changed, 620 insertions(+), 60 deletions(-) create mode 100644 bench/snippets/process-info.mjs (limited to 'src/bun.js/bindings/webcore') diff --git a/bench/snippets/process-info.mjs b/bench/snippets/process-info.mjs new file mode 100644 index 000000000..0366472e5 --- /dev/null +++ b/bench/snippets/process-info.mjs @@ -0,0 +1,33 @@ +import { bench, run } from "./runner.mjs"; +import { performance } from "perf_hooks"; + +bench("process.memoryUsage()", () => { + process.memoryUsage(); +}); + +bench("process.memoryUsage.rss()", () => { + process.memoryUsage.rss(); +}); + +bench("process.cpuUsage()", () => { + process.cpuUsage(); +}); + +const init = process.cpuUsage(); +bench("process.cpuUsage(delta)", () => { + process.cpuUsage(init); +}); + +bench("performance.now()", () => { + performance.now(); +}); + +bench("process.hrtime()", () => { + process.hrtime(); +}); + +bench("process.hrtime.bigint()", () => { + process.hrtime.bigint(); +}); + +await run(); diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 8d94594cc..7d7bdd982 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -12,8 +12,24 @@ #include "ZigConsoleClient.h" #include #include +#include +#include +#include + #pragma mark - Node.js Process +#if defined(__APPLE__) +#include +#include +#endif + +#if defined(__linux__) +#include +#include +#include +#include +#endif + #if !defined(_MSC_VER) #include // setuid, getuid #endif @@ -335,9 +351,12 @@ extern "C" uint64_t Bun__readOriginTimer(void*); JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { + Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); auto& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + uint64_t time = Bun__readOriginTimer(globalObject->bunVM()); int64_t seconds = static_cast(time / 1000000000); int64_t nanoseconds = time % 1000000000; @@ -346,7 +365,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, JSC::JSValue arg0 = callFrame->uncheckedArgument(0); if (!arg0.isUndefinedOrNull()) { JSArray* relativeArray = JSC::jsDynamicCast(arg0); - auto throwScope = DECLARE_THROW_SCOPE(vm); if ((!relativeArray && !arg0.isUndefinedOrNull()) || relativeArray->length() < 2) { JSC::throwTypeError(globalObject, throwScope, "hrtime() argument must be an array or undefined"_s); return JSC::JSValue::encode(JSC::JSValue {}); @@ -366,14 +384,28 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, seconds--; nanoseconds += 1000000000; } - throwScope.release(); } } - auto* array = JSArray::create(vm, globalObject->originalArrayStructureForIndexingType(ArrayWithContiguous), 2); - array->setIndexQuickly(vm, 0, JSC::jsNumber(seconds)); - array->setIndexQuickly(vm, 1, JSC::jsNumber(nanoseconds)); - return JSC::JSValue::encode(JSC::JSValue(array)); + JSC::JSArray* array = nullptr; + { + JSC::ObjectInitializationScope initializationScope(vm); + if ((array = JSC::JSArray::tryCreateUninitializedRestricted( + initializationScope, nullptr, + globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + 2))) { + + array->initializeIndex(initializationScope, 0, JSC::jsNumber(seconds)); + array->initializeIndex(initializationScope, 1, JSC::jsNumber(nanoseconds)); + } + } + + if (UNLIKELY(!array)) { + JSC::throwOutOfMemoryError(globalObject, throwScope); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(array)); } JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, @@ -680,10 +712,10 @@ static JSValue constructProcessHrtimeObject(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, - MAKE_STATIC_STRING_IMPL("hrtime"), Process_functionHRTime, ImplementationVisibility::Public); + String("hrtime"_s), Process_functionHRTime, ImplementationVisibility::Public); JSC::JSFunction* hrtimeBigInt = JSC::JSFunction::create(vm, globalObject, 0, - MAKE_STATIC_STRING_IMPL("bigint"), Process_functionHRTimeBigInt, ImplementationVisibility::Public); + String("bigint"_s), Process_functionHRTimeBigInt, ImplementationVisibility::Public); hrtime->putDirect(vm, JSC::Identifier::fromString(vm, "bigint"_s), hrtimeBigInt); @@ -945,6 +977,317 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyExit, (JSGlobalObject * globalObj __builtin_unreachable(); } +template +void Process::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + Process* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + thisObject->cpuUsageStructure.visit(visitor); + thisObject->memoryUsageStructure.visit(visitor); +} + +DEFINE_VISIT_CHILDREN(Process); + +static Structure* constructCPUUsageStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + JSC::Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 2); + PropertyOffset offset; + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "user"_s), + 0, + offset); + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "system"_s), + 0, + offset); + return structure; +} +static Structure* constructMemoryUsageStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + JSC::Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 5); + PropertyOffset offset; + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "rss"_s), + 0, + offset); + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "heapTotal"_s), + 0, + offset); + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "heapUsed"_s), + 0, + offset); + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "external"_s), + 0, + offset); + structure = structure->addPropertyTransition( + vm, + structure, + JSC::Identifier::fromString(vm, "arrayBuffers"_s), + 0, + offset); + + return structure; +} + +static Process* getProcessObject(JSC::JSGlobalObject* lexicalGlobalObject, JSValue thisValue) +{ + Process* process = jsDynamicCast(thisValue); + + // Handle "var memoryUsage = process.memoryUsage; memoryUsage()" + if (UNLIKELY(!process)) { + // Handle calling this function from inside a node:vm + Zig::GlobalObject* zigGlobalObject = jsDynamicCast(lexicalGlobalObject); + + if (UNLIKELY(!zigGlobalObject)) { + zigGlobalObject = Bun__getDefaultGlobal(); + } + + return jsCast(zigGlobalObject->processObject()); + } + + return process; +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionCpuUsage, + (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + struct rusage rusage; + if (getrusage(RUSAGE_SELF, &rusage) != 0) { + SystemError error; + error.errno_ = errno; + error.syscall = Bun::toString("getrusage"_s); + error.message = Bun::toString("Failed to get CPU usage"_s); + throwException(globalObject, throwScope, JSValue::decode(SystemError__toErrorInstance(&error, globalObject))); + return JSValue::encode(jsUndefined()); + } + + auto* process = getProcessObject(globalObject, callFrame->thisValue()); + + Structure* cpuUsageStructure = process->cpuUsageStructure.getInitializedOnMainThread(process); + + constexpr double MICROS_PER_SEC = 1000000.0; + + double user = MICROS_PER_SEC * rusage.ru_utime.tv_sec + rusage.ru_utime.tv_usec; + double system = MICROS_PER_SEC * rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec; + + if (callFrame->argumentCount() > 0) { + JSValue comparatorValue = callFrame->argument(0); + if (!comparatorValue.isUndefined()) { + if (UNLIKELY(!comparatorValue.isObject())) { + throwTypeError(globalObject, throwScope, "Expected an object as the first argument"_s); + return JSC::JSValue::encode(JSC::jsUndefined()); + } + + JSC::JSObject* comparator = comparatorValue.getObject(); + JSValue userValue; + JSValue systemValue; + + if (LIKELY(comparator->structureID() == cpuUsageStructure->id())) { + userValue = comparator->getDirect(0); + systemValue = comparator->getDirect(1); + } else { + userValue = comparator->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "user"_s)); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); + + systemValue = comparator->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "system"_s)); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); + } + + if (UNLIKELY(!userValue || !userValue.isNumber())) { + throwTypeError(globalObject, throwScope, "Expected a number for the user property"_s); + return JSC::JSValue::encode(JSC::jsUndefined()); + } + + if (UNLIKELY(!systemValue || !systemValue.isNumber())) { + throwTypeError(globalObject, throwScope, "Expected a number for the system property"_s); + return JSC::JSValue::encode(JSC::jsUndefined()); + } + + double userComparator = userValue.asNumber(); + double systemComparator = systemValue.asNumber(); + + user -= userComparator; + system -= systemComparator; + } + } + + JSC::JSObject* result = JSC::constructEmptyObject(vm, cpuUsageStructure); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::jsUndefined())); + + result->putDirectOffset(vm, 0, JSC::jsNumber(user)); + result->putDirectOffset(vm, 1, JSC::jsNumber(system)); + + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(result)); +} + +static int getRSS(size_t* rss) +{ +#if defined(__APPLE__) + mach_msg_type_number_t count; + task_basic_info_data_t info; + kern_return_t err; + + count = TASK_BASIC_INFO_COUNT; + err = task_info(mach_task_self(), + TASK_BASIC_INFO, + reinterpret_cast(&info), + &count); + + if (err == KERN_SUCCESS) { + *rss = (size_t)info.resident_size; + return 0; + } + + return -1; +#elif defined(__linux__) + // Taken from libuv. + char buf[1024]; + const char* s; + ssize_t n; + long val; + int fd; + int i; + + do + fd = open("/proc/self/stat", O_RDONLY); + while (fd == -1 && errno == EINTR); + + if (fd == -1) + return errno; + + do + n = read(fd, buf, sizeof(buf) - 1); + while (n == -1 && errno == EINTR); + + int closeErrno = 0; + do { + closeErrno = close(fd); + } while (closeErrno == -1 && errno == EINTR); + + if (n == -1) + return errno; + buf[n] = '\0'; + + s = strchr(buf, ' '); + if (s == NULL) + goto err; + + s += 1; + if (*s != '(') + goto err; + + s = strchr(s, ')'); + if (s == NULL) + goto err; + + for (i = 1; i <= 22; i++) { + s = strchr(s + 1, ' '); + if (s == NULL) + goto err; + } + + errno = 0; + val = strtol(s, NULL, 10); + if (errno != 0) + goto err; + if (val < 0) + goto err; + + *rss = val * getpagesize(); + return 0; + +err: + return EINVAL; +#else +#error "Unsupported platform" +#endif +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, + (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* process = getProcessObject(globalObject, callFrame->thisValue()); + + size_t current_rss = 0; + if (getRSS(¤t_rss) != 0) { + SystemError error; + error.errno_ = errno; + error.syscall = Bun::toString("memoryUsage"_s); + error.message = Bun::toString("Failed to get memory usage"_s); + throwException(globalObject, throwScope, JSValue::decode(SystemError__toErrorInstance(&error, globalObject))); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + JSC::JSObject* result = JSC::constructEmptyObject(vm, process->memoryUsageStructure.getInitializedOnMainThread(process)); + if (UNLIKELY(throwScope.exception())) { + return JSC::JSValue::encode(JSC::JSValue {}); + } + + // Node.js: + // { + // rss: 4935680, + // heapTotal: 1826816, + // heapUsed: 650472, + // external: 49879, + // arrayBuffers: 9386 + // } + + result->putDirectOffset(vm, 0, JSC::jsNumber(current_rss)); + result->putDirectOffset(vm, 1, JSC::jsNumber(vm.heap.blockBytesAllocated())); + + // heap.size() loops through every cell... + // TODO: add a binding for heap.sizeAfterLastCollection() + result->putDirectOffset(vm, 2, JSC::jsNumber(vm.heap.sizeAfterLastEdenCollection())); + + result->putDirectOffset(vm, 3, JSC::jsNumber(vm.heap.externalMemorySize())); + + // We report 0 for this because m_arrayBuffers in JSC::Heap is private and we need to add a binding + // If we use objectTypeCounts(), it's hideously slow because it loops through every single object in the heap + // TODO: add a binding for m_arrayBuffers, registerWrapper() in TypedArrayController doesn't work + result->putDirectOffset(vm, 4, JSC::jsNumber(0)); + + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(result)); +} + +JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsageRSS, + (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + size_t current_rss = 0; + if (getRSS(¤t_rss) != 0) { + SystemError error; + error.errno_ = errno; + error.syscall = Bun::toString("memoryUsage"_s); + error.message = Bun::toString("Failed to get memory usage"_s); + throwException(globalObject, throwScope, JSValue::decode(SystemError__toErrorInstance(&error, globalObject))); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsNumber(current_rss))); +} + JSC_DEFINE_HOST_FUNCTION(Process_functionOpenStdin, (JSGlobalObject * globalObject, CallFrame* callFrame)) { auto& vm = globalObject->vm(); @@ -1010,6 +1353,19 @@ static JSValue Process_stubEmptySet(VM& vm, JSObject* processObject) return JSSet::create(vm, globalObject->setStructure()); } +static JSValue constructMemoryUsage(VM& vm, JSObject* processObject) +{ + auto* globalObject = processObject->globalObject(); + JSC::JSFunction* memoryUsage = JSC::JSFunction::create(vm, globalObject, 0, + String("memoryUsage"_s), Process_functionMemoryUsage, ImplementationVisibility::Public); + + JSC::JSFunction* rss = JSC::JSFunction::create(vm, globalObject, 0, + String("rss"_s), Process_functionMemoryUsageRSS, ImplementationVisibility::Public); + + memoryUsage->putDirect(vm, JSC::Identifier::fromString(vm, "rss"_s), rss, JSC::PropertyAttribute::Function | 0); + return memoryUsage; +} + static JSValue constructFeatures(VM& vm, JSObject* processObject) { // { @@ -1133,16 +1489,16 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, browser constructBrowser PropertyCallback chdir Process_functionChdir Function 1 config constructProcessConfigObject PropertyCallback - debugPort processDebugPort CustomAccessor - exitCode processExitCode CustomAccessor - title processTitle CustomAccessor + cpuUsage Process_functionCpuUsage Function 1 cwd Process_functionCwd Function 1 + debugPort processDebugPort CustomAccessor dlopen Process_functionDlopen Function 1 emitWarning Process_emitWarning Function 1 env constructEnv PropertyCallback execArgv constructExecArgv PropertyCallback execPath constructExecPath PropertyCallback exit Process_functionExit Function 1 + exitCode processExitCode CustomAccessor features constructFeatures PropertyCallback getActiveResourcesInfo Process_stubFunctionReturningArray Function 0 getegid Process_functiongetegid Function 0 @@ -1153,6 +1509,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, hrtime constructProcessHrtimeObject PropertyCallback isBun constructIsBun PropertyCallback mainModule JSBuiltin ReadOnly|Builtin|Accessor|Function 0 + memoryUsage constructMemoryUsage PropertyCallback moduleLoadList Process_stubEmptyArray PropertyCallback nextTick Process_functionNextTick Function 1 openStdin Process_functionOpenStdin Function 0 @@ -1166,6 +1523,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, stderr constructStderr PropertyCallback stdin constructStdin PropertyCallback stdout constructStdout PropertyCallback + title processTitle CustomAccessor umask Process_functionUmask Function 1 uptime Process_functionUptime Function 1 version constructVersion PropertyCallback @@ -1192,6 +1550,14 @@ void Process::finishCreation(JSC::VM& vm) { Base::finishCreation(vm); + this->cpuUsageStructure.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(constructCPUUsageStructure(init.vm, init.owner->globalObject())); + }); + + this->memoryUsageStructure.initLater([](const JSC::LazyProperty::Initializer& init) { + init.set(constructMemoryUsageStructure(init.vm, init.owner->globalObject())); + }); + this->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsString(vm, String("process"_s)), 0); } diff --git a/src/bun.js/bindings/Process.h b/src/bun.js/bindings/Process.h index 8b703b8b1..fbad9b1ff 100644 --- a/src/bun.js/bindings/Process.h +++ b/src/bun.js/bindings/Process.h @@ -45,6 +45,24 @@ public: return accessor; } + LazyProperty cpuUsageStructure; + LazyProperty memoryUsageStructure; + + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForProcessObject.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForProcessObject = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForProcessObject.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForProcessObject = std::forward(space); }); + } + void finishCreation(JSC::VM& vm); }; diff --git a/src/bun.js/bindings/Process.lut.h b/src/bun.js/bindings/Process.lut.h index 5c0bd2c5a..81eb1b7b9 100644 --- a/src/bun.js/bindings/Process.lut.h +++ b/src/bun.js/bindings/Process.lut.h @@ -1,6 +1,6 @@ // File generated via `make generate-builtins` -static const struct CompactHashIndex processObjectTableIndex[141] = { - { 42, -1 }, +static const struct CompactHashIndex processObjectTableIndex[142] = { + { 43, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, @@ -11,12 +11,12 @@ static const struct CompactHashIndex processObjectTableIndex[141] = { { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 16, 130 }, + { 15, 129 }, { -1, -1 }, { -1, -1 }, - { 19, 137 }, + { 18, 138 }, { -1, -1 }, - { 43, -1 }, + { 45, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, @@ -33,37 +33,37 @@ static const struct CompactHashIndex processObjectTableIndex[141] = { { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 3, 140 }, + { 3, 141 }, { 1, 128 }, { -1, -1 }, - { 57, -1 }, - { -1, -1 }, + { 59, -1 }, { -1, -1 }, + { 10, -1 }, { -1, -1 }, { -1, -1 }, - { 30, -1 }, + { 31, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 50, -1 }, + { 52, -1 }, { 27, -1 }, - { 10, -1 }, + { 12, -1 }, { -1, -1 }, - { 11, -1 }, + { 19, -1 }, { -1, -1 }, - { 15, 136 }, + { 14, 137 }, { -1, -1 }, - { 35, -1 }, + { 36, -1 }, { -1, -1 }, - { 37, -1 }, - { 53, -1 }, - { 34, -1 }, - { 6, 138 }, + { 38, -1 }, + { 55, -1 }, + { 35, -1 }, + { 6, 139 }, { -1, -1 }, - { 49, -1 }, + { 51, -1 }, { 4, -1 }, - { 45, -1 }, + { 47, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, @@ -75,23 +75,23 @@ static const struct CompactHashIndex processObjectTableIndex[141] = { { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 39, -1 }, + { 40, -1 }, { -1, -1 }, - { 36, -1 }, + { 37, -1 }, { -1, -1 }, { 0, -1 }, - { 12, 129 }, - { 17, 131 }, - { 38, -1 }, + { 26, 135 }, + { 16, 130 }, + { 39, -1 }, { -1, -1 }, { 23, -1 }, - { 13, -1 }, + { 11, -1 }, { -1, -1 }, { -1, -1 }, - { 56, -1 }, + { 58, -1 }, { -1, -1 }, { -1, -1 }, - { 47, -1 }, + { 30, 136 }, { -1, -1 }, { 29, -1 }, { 22, -1 }, @@ -105,45 +105,46 @@ static const struct CompactHashIndex processObjectTableIndex[141] = { { 5, -1 }, { -1, -1 }, { -1, -1 }, - { 46, -1 }, + { 48, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 14, 132 }, + { 13, 131 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, { 9, -1 }, - { 25, 134 }, + { 25, 133 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 21, 135 }, + { 21, 134 }, { -1, -1 }, { -1, -1 }, { -1, -1 }, - { 44, 139 }, + { 46, 140 }, { -1, -1 }, - { 18, -1 }, + { 17, -1 }, { 8, -1 }, - { 26, -1 }, { 28, -1 }, - { 31, 133 }, - { 32, -1 }, + { 32, 132 }, { 33, -1 }, - { 40, -1 }, + { 34, -1 }, { 41, -1 }, - { 48, -1 }, - { 51, -1 }, - { 52, -1 }, + { 42, -1 }, + { 44, -1 }, + { 49, -1 }, + { 50, -1 }, + { 53, -1 }, { 54, -1 }, - { 55, -1 }, + { 56, -1 }, + { 57, -1 }, }; -static const struct HashTableValue processObjectTableValues[58] = { +static const struct HashTableValue processObjectTableValues[60] = { { "abort"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionAbort, 1 } }, { "allowedNodeEnvironmentFlags"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, Process_stubEmptySet } }, { "arch"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructArch } }, @@ -154,16 +155,16 @@ static const struct HashTableValue processObjectTableValues[58] = { { "browser"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructBrowser } }, { "chdir"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionChdir, 1 } }, { "config"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructProcessConfigObject } }, - { "debugPort"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processDebugPort, setProcessDebugPort } }, - { "exitCode"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processExitCode, setProcessExitCode } }, - { "title"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processTitle, setProcessTitle } }, + { "cpuUsage"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionCpuUsage, 1 } }, { "cwd"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionCwd, 1 } }, + { "debugPort"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processDebugPort, setProcessDebugPort } }, { "dlopen"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionDlopen, 1 } }, { "emitWarning"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_emitWarning, 1 } }, { "env"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructEnv } }, { "execArgv"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructExecArgv } }, { "execPath"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructExecPath } }, { "exit"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionExit, 1 } }, + { "exitCode"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processExitCode, setProcessExitCode } }, { "features"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructFeatures } }, { "getActiveResourcesInfo"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_stubFunctionReturningArray, 0 } }, { "getegid"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functiongetegid, 0 } }, @@ -174,6 +175,7 @@ static const struct HashTableValue processObjectTableValues[58] = { { "hrtime"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructProcessHrtimeObject } }, { "isBun"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructIsBun } }, { "mainModule"_s, ((static_cast(PropertyAttribute::ReadOnly|PropertyAttribute::Builtin|PropertyAttribute::Accessor|PropertyAttribute::Function)) & ~PropertyAttribute::Function) | PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinGeneratorType, processObjectMainModuleCodeGenerator, 0 } }, + { "memoryUsage"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructMemoryUsage } }, { "moduleLoadList"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, Process_stubEmptyArray } }, { "nextTick"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionNextTick, 1 } }, { "openStdin"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionOpenStdin, 0 } }, @@ -187,6 +189,7 @@ static const struct HashTableValue processObjectTableValues[58] = { { "stderr"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructStderr } }, { "stdin"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructStdin } }, { "stdout"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructStdout } }, + { "title"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, processTitle, setProcessTitle } }, { "umask"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionUmask, 1 } }, { "uptime"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Process_functionUptime, 1 } }, { "version"_s, static_cast(PropertyAttribute::PropertyCallback), NoIntrinsic, { HashTableValue::LazyPropertyType, constructVersion } }, @@ -205,4 +208,4 @@ static const struct HashTableValue processObjectTableValues[58] = { }; static const struct HashTable processObjectTable = - { 58, 127, true, nullptr, processObjectTableValues, processObjectTableIndex }; + { 60, 127, true, nullptr, processObjectTableValues, processObjectTableIndex }; diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 65875d091..82a2c6a24 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -38,6 +38,7 @@ public: std::unique_ptr m_clientSubspaceForJSMockImplementation; std::unique_ptr m_clientSubspaceForJSMockFunction; std::unique_ptr m_clientSubspaceForMockWithImplementationCleanupData; + std::unique_ptr m_clientSubspaceForProcessObject; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" /* --- bun --- */ diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 433832450..f1b290d25 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -38,6 +38,7 @@ public: std::unique_ptr m_subspaceForJSMockImplementation; std::unique_ptr m_subspaceForJSMockFunction; std::unique_ptr m_subspaceForMockWithImplementationCleanupData; + std::unique_ptr m_subspaceForProcessObject; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" /*-- BUN --*/ diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index c4701f664..e038383de 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -162,7 +162,8 @@ it("process.umask()", () => { expect(process.umask()).toBe(orig); }); -const versions = existsSync(import.meta.dir + "/../../src/generated_versions_list.zig"); +const generated_versions_list = join(import.meta.dir, "../../../../src/generated_versions_list.zig"); +const versions = existsSync(generated_versions_list); (versions ? it : it.skip)("process.versions", () => { // Generate a list of all the versions in the versions object // example: @@ -178,7 +179,7 @@ const versions = existsSync(import.meta.dir + "/../../src/generated_versions_lis // pub const c_ares = "0e7a5dee0fbb04080750cf6eabbe89d8bae87faa"; // pub const usockets = "fafc241e8664243fc0c51d69684d5d02b9805134"; const versions = Object.fromEntries( - readFileSync(import.meta.dir + "/../../src/generated_versions_list.zig", "utf8") + readFileSync(generated_versions_list, "utf8") .split("\n") .filter(line => line.startsWith("pub const") && !line.includes("zig") && line.includes(' = "')) .map(line => line.split(" = ")) @@ -291,3 +292,140 @@ describe("process.onBeforeExit", () => { expect(stdout.toString().trim()).toBe("beforeExit: 0\nbeforeExit: 1\nexit: 2"); }); }); + +it("process.memoryUsage", () => { + expect(process.memoryUsage()).toEqual({ + rss: expect.any(Number), + heapTotal: expect.any(Number), + heapUsed: expect.any(Number), + external: expect.any(Number), + arrayBuffers: expect.any(Number), + }); +}); + +it("process.memoryUsage.rss", () => { + expect(process.memoryUsage.rss()).toEqual(expect.any(Number)); +}); + +describe("process.cpuUsage", () => { + it("works", () => { + expect(process.cpuUsage()).toEqual({ + user: expect.any(Number), + system: expect.any(Number), + }); + }); + + it("works with diff", () => { + const init = process.cpuUsage(); + for (let i = 0; i < 1000; i++) {} + const delta = process.cpuUsage(init); + expect(delta.user).toBeGreaterThan(0); + expect(delta.system).toBeGreaterThan(0); + }); + + it("works with diff of different structure", () => { + const init = { + user: 0, + system: 0, + }; + for (let i = 0; i < 1000; i++) {} + const delta = process.cpuUsage(init); + expect(delta.user).toBeGreaterThan(0); + expect(delta.system).toBeGreaterThan(0); + }); + + it("throws on invalid property", () => { + const fixtures = [ + {}, + { user: null }, + { user: {} }, + { user: "potato" }, + + { user: 123 }, + { user: 123, system: null }, + { user: 123, system: "potato" }, + ]; + for (const fixture of fixtures) { + expect(() => process.cpuUsage(fixture)).toThrow(); + } + }); + + // Skipped on Linux because it seems to not change as often as on macOS + it.skipIf(process.platform === "linux")("increases monotonically", () => { + const init = process.cpuUsage(); + for (let i = 0; i < 10000; i++) {} + const another = process.cpuUsage(); + expect(another.user).toBeGreaterThan(init.user); + expect(another.system).toBeGreaterThan(init.system); + }); +}); + +it("process.getegid", () => { + expect(typeof process.getegid()).toBe("number"); +}); +it("process.geteuid", () => { + expect(typeof process.geteuid()).toBe("number"); +}); +it("process.getgid", () => { + expect(typeof process.getgid()).toBe("number"); +}); +it("process.getgroups", () => { + expect(process.getgroups()).toBeInstanceOf(Array); + expect(process.getgroups().length).toBeGreaterThan(0); +}); +it("process.getuid", () => { + expect(typeof process.getuid()).toBe("number"); +}); + +it("process.getuid", () => { + expect(typeof process.getuid()).toBe("number"); +}); + +const undefinedStubs = [ + "_debugEnd", + "_debugProcess", + "_fatalException", + "_linkedBinding", + "_rawDebug", + "_startProfilerIdleNotifier", + "_stopProfilerIdleNotifier", + "_tickCallback", +]; + +for (const stub of undefinedStubs) { + it(`process.${stub}`, () => { + expect(process[stub]()).toBeUndefined(); + }); +} + +const arrayStubs = ["getActiveResourcesInfo", "_getActiveRequests", "_getActiveHandles"]; + +for (const stub of arrayStubs) { + it(`process.${stub}`, () => { + expect(process[stub]()).toBeInstanceOf(Array); + }); +} + +const emptyObjectStubs = ["_preload_modules"]; +const emptySetStubs = ["allowedNodeEnvironmentFlags"]; +const emptyArrayStubs = ["moduleLoadList"]; + +for (const stub of emptyObjectStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toEqual({}); + }); +} + +for (const stub of emptySetStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toBeInstanceOf(Set); + expect(process[stub].size).toBe(0); + }); +} + +for (const stub of emptyArrayStubs) { + it(`process.${stub}`, () => { + expect(process[stub]).toBeInstanceOf(Array); + expect(process[stub]).toHaveLength(0); + }); +} -- cgit v1.2.3 From 5c8726d602fe73e49d027194fef65b9432872c8b Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:48:46 -0700 Subject: process signal events (#3569) * signal events * simple tests * ignore SIGSTOP * better tests * use `EventEmitter` * use `Bun__getDefaultGlobal` * progress * don't use 'Bun__getDefaultGlobal` * fix tests * remove signals from map * update tests * don't overwrite event emitter methods * avoid two lookups * use `std::once` * releaseEarly() * Remove signal handler after use * Update call-raise.js * Create process-signal-handler.fixture.js * Don't register duplicates * Add missing lock * another test * update test * revert some changes --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/JSBundlerPlugin.cpp | 3 +- src/bun.js/bindings/Process.cpp | 290 +++++++++++++++------ src/bun.js/bindings/Process.h | 2 + src/bun.js/bindings/ScriptExecutionContext.cpp | 6 + src/bun.js/bindings/ScriptExecutionContext.h | 8 +- src/bun.js/bindings/webcore/EventEmitter.cpp | 7 + src/bun.js/bindings/webcore/EventEmitter.h | 6 +- src/bun.js/bindings/webcore/JSEventEmitter.cpp | 15 +- src/bun.js/bindings/webcore/JSEventEmitter.h | 3 + test/js/node/process/call-raise.js | 15 ++ .../node/process/process-signal-handler.fixture.js | 63 +++++ test/js/node/process/process.test.js | 26 +- 12 files changed, 352 insertions(+), 92 deletions(-) create mode 100644 test/js/node/process/call-raise.js create mode 100644 test/js/node/process/process-signal-handler.fixture.js (limited to 'src/bun.js/bindings/webcore') diff --git a/src/bun.js/bindings/JSBundlerPlugin.cpp b/src/bun.js/bindings/JSBundlerPlugin.cpp index d55c5fc2e..ec3933574 100644 --- a/src/bun.js/bindings/JSBundlerPlugin.cpp +++ b/src/bun.js/bindings/JSBundlerPlugin.cpp @@ -157,7 +157,8 @@ public: JSC::LazyProperty setupFunction; private: - JSBundlerPlugin(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure, void* config, BunPluginTarget target, JSBundlerPluginAddErrorCallback addError, JSBundlerPluginOnLoadAsyncCallback onLoadAsync, JSBundlerPluginOnResolveAsyncCallback onResolveAsync) + JSBundlerPlugin(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure, void* config, BunPluginTarget target, + JSBundlerPluginAddErrorCallback addError, JSBundlerPluginOnLoadAsyncCallback onLoadAsync, JSBundlerPluginOnResolveAsyncCallback onResolveAsync) : JSC::JSNonFinalObject(vm, structure) , plugin(BundlerPlugin(config, target, addError, onLoadAsync, onResolveAsync)) { diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 7d7bdd982..f9fb85b95 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -437,91 +437,215 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, return JSC::JSValue::encode(result); } -// static const NeverDestroyed signalNames[] = { -// MAKE_STATIC_STRING_IMPL("SIGHUP"), -// MAKE_STATIC_STRING_IMPL("SIGINT"), -// MAKE_STATIC_STRING_IMPL("SIGQUIT"), -// MAKE_STATIC_STRING_IMPL("SIGILL"), -// MAKE_STATIC_STRING_IMPL("SIGTRAP"), -// MAKE_STATIC_STRING_IMPL("SIGABRT"), -// MAKE_STATIC_STRING_IMPL("SIGIOT"), -// MAKE_STATIC_STRING_IMPL("SIGBUS"), -// MAKE_STATIC_STRING_IMPL("SIGFPE"), -// MAKE_STATIC_STRING_IMPL("SIGKILL"), -// MAKE_STATIC_STRING_IMPL("SIGUSR1"), -// MAKE_STATIC_STRING_IMPL("SIGSEGV"), -// MAKE_STATIC_STRING_IMPL("SIGUSR2"), -// MAKE_STATIC_STRING_IMPL("SIGPIPE"), -// MAKE_STATIC_STRING_IMPL("SIGALRM"), -// MAKE_STATIC_STRING_IMPL("SIGTERM"), -// MAKE_STATIC_STRING_IMPL("SIGCHLD"), -// MAKE_STATIC_STRING_IMPL("SIGCONT"), -// MAKE_STATIC_STRING_IMPL("SIGSTOP"), -// MAKE_STATIC_STRING_IMPL("SIGTSTP"), -// MAKE_STATIC_STRING_IMPL("SIGTTIN"), -// MAKE_STATIC_STRING_IMPL("SIGTTOU"), -// MAKE_STATIC_STRING_IMPL("SIGURG"), -// MAKE_STATIC_STRING_IMPL("SIGXCPU"), -// MAKE_STATIC_STRING_IMPL("SIGXFSZ"), -// MAKE_STATIC_STRING_IMPL("SIGVTALRM"), -// MAKE_STATIC_STRING_IMPL("SIGPROF"), -// MAKE_STATIC_STRING_IMPL("SIGWINCH"), -// MAKE_STATIC_STRING_IMPL("SIGIO"), -// MAKE_STATIC_STRING_IMPL("SIGINFO"), -// MAKE_STATIC_STRING_IMPL("SIGSYS"), -// }; -// static const int signalNumbers[] = { -// SIGHUP, -// SIGINT, -// SIGQUIT, -// SIGILL, -// SIGTRAP, -// SIGABRT, -// SIGIOT, -// SIGBUS, -// SIGFPE, -// SIGKILL, -// SIGUSR1, -// SIGSEGV, -// SIGUSR2, -// SIGPIPE, -// SIGALRM, -// SIGTERM, -// SIGCHLD, -// SIGCONT, -// SIGSTOP, -// SIGTSTP, -// SIGTTIN, -// SIGTTOU, -// SIGURG, -// SIGXCPU, -// SIGXFSZ, -// SIGVTALRM, -// SIGPROF, -// SIGWINCH, -// SIGIO, -// SIGINFO, -// SIGSYS, -// }; - -// JSC_DEFINE_HOST_FUNCTION(jsFunctionProcessOn, (JSGlobalObject * globalObject, CallFrame* callFrame)) -// { -// VM& vm = globalObject->vm(); -// auto scope = DECLARE_THROW_SCOPE(vm); - -// if (callFrame->argumentCount() < 2) { -// throwVMError(globalObject, scope, "Not enough arguments"_s); -// return JSValue::encode(jsUndefined()); -// } - -// String eventName = callFrame->uncheckedArgument(0).toWTFString(globalObject); -// RETURN_IF_EXCEPTION(scope, encodedJSValue()); -// } +static HashMap* signalNameToNumberMap = nullptr; +static HashMap* signalNumberToNameMap = nullptr; + +// signal number to array of script execution context ids that care about the signal +static HashMap>* signalToContextIdsMap = nullptr; +static Lock signalToContextIdsMapLock; + +static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& eventName, bool isAdded) +{ + + static const NeverDestroyed signalNames[] = { + MAKE_STATIC_STRING_IMPL("SIGHUP"), + MAKE_STATIC_STRING_IMPL("SIGINT"), + MAKE_STATIC_STRING_IMPL("SIGQUIT"), + MAKE_STATIC_STRING_IMPL("SIGILL"), + MAKE_STATIC_STRING_IMPL("SIGTRAP"), + MAKE_STATIC_STRING_IMPL("SIGABRT"), + MAKE_STATIC_STRING_IMPL("SIGIOT"), + MAKE_STATIC_STRING_IMPL("SIGBUS"), + MAKE_STATIC_STRING_IMPL("SIGFPE"), + MAKE_STATIC_STRING_IMPL("SIGKILL"), + MAKE_STATIC_STRING_IMPL("SIGUSR1"), + MAKE_STATIC_STRING_IMPL("SIGSEGV"), + MAKE_STATIC_STRING_IMPL("SIGUSR2"), + MAKE_STATIC_STRING_IMPL("SIGPIPE"), + MAKE_STATIC_STRING_IMPL("SIGALRM"), + MAKE_STATIC_STRING_IMPL("SIGTERM"), + MAKE_STATIC_STRING_IMPL("SIGCHLD"), + MAKE_STATIC_STRING_IMPL("SIGCONT"), + MAKE_STATIC_STRING_IMPL("SIGSTOP"), + MAKE_STATIC_STRING_IMPL("SIGTSTP"), + MAKE_STATIC_STRING_IMPL("SIGTTIN"), + MAKE_STATIC_STRING_IMPL("SIGTTOU"), + MAKE_STATIC_STRING_IMPL("SIGURG"), + MAKE_STATIC_STRING_IMPL("SIGXCPU"), + MAKE_STATIC_STRING_IMPL("SIGXFSZ"), + MAKE_STATIC_STRING_IMPL("SIGVTALRM"), + MAKE_STATIC_STRING_IMPL("SIGPROF"), + MAKE_STATIC_STRING_IMPL("SIGWINCH"), + MAKE_STATIC_STRING_IMPL("SIGIO"), + MAKE_STATIC_STRING_IMPL("SIGINFO"), + MAKE_STATIC_STRING_IMPL("SIGSYS"), + }; + + static std::once_flag signalNameToNumberMapOnceFlag; + std::call_once(signalNameToNumberMapOnceFlag, [] { + signalNameToNumberMap = new HashMap(); + signalNameToNumberMap->reserveInitialCapacity(31); + signalNameToNumberMap->add(signalNames[0], SIGHUP); + signalNameToNumberMap->add(signalNames[1], SIGINT); + signalNameToNumberMap->add(signalNames[2], SIGQUIT); + signalNameToNumberMap->add(signalNames[3], SIGILL); + signalNameToNumberMap->add(signalNames[4], SIGTRAP); + signalNameToNumberMap->add(signalNames[5], SIGABRT); + signalNameToNumberMap->add(signalNames[6], SIGIOT); + signalNameToNumberMap->add(signalNames[7], SIGBUS); + signalNameToNumberMap->add(signalNames[8], SIGFPE); + // signalNameToNumberMap->add(signalNames[9], SIGKILL); + signalNameToNumberMap->add(signalNames[10], SIGUSR1); + signalNameToNumberMap->add(signalNames[11], SIGSEGV); + signalNameToNumberMap->add(signalNames[12], SIGUSR2); + signalNameToNumberMap->add(signalNames[13], SIGPIPE); + signalNameToNumberMap->add(signalNames[14], SIGALRM); + signalNameToNumberMap->add(signalNames[15], SIGTERM); + signalNameToNumberMap->add(signalNames[16], SIGCHLD); + signalNameToNumberMap->add(signalNames[17], SIGCONT); + // signalNameToNumberMap->add(signalNames[18], SIGSTOP); + signalNameToNumberMap->add(signalNames[19], SIGTSTP); + signalNameToNumberMap->add(signalNames[20], SIGTTIN); + signalNameToNumberMap->add(signalNames[21], SIGTTOU); + signalNameToNumberMap->add(signalNames[22], SIGURG); + signalNameToNumberMap->add(signalNames[23], SIGXCPU); + signalNameToNumberMap->add(signalNames[24], SIGXFSZ); + signalNameToNumberMap->add(signalNames[25], SIGVTALRM); + signalNameToNumberMap->add(signalNames[26], SIGPROF); + signalNameToNumberMap->add(signalNames[27], SIGWINCH); + signalNameToNumberMap->add(signalNames[28], SIGIO); +#ifdef SIGINFO + signalNameToNumberMap->add(signalNames[29], SIGINFO); +#endif + +#ifndef SIGINFO + signalNameToNumberMap->add(signalNames[29], 255); +#endif + signalNameToNumberMap->add(signalNames[30], SIGSYS); + }); + + static std::once_flag signalNumberToNameMapOnceFlag; + std::call_once(signalNumberToNameMapOnceFlag, [] { + signalNumberToNameMap = new HashMap(); + signalNumberToNameMap->reserveInitialCapacity(31); + signalNumberToNameMap->add(SIGHUP, signalNames[0]); + signalNumberToNameMap->add(SIGINT, signalNames[1]); + signalNumberToNameMap->add(SIGQUIT, signalNames[2]); + signalNumberToNameMap->add(SIGILL, signalNames[3]); + signalNumberToNameMap->add(SIGTRAP, signalNames[4]); + signalNumberToNameMap->add(SIGABRT, signalNames[5]); + signalNumberToNameMap->add(SIGIOT, signalNames[6]); + signalNumberToNameMap->add(SIGBUS, signalNames[7]); + signalNumberToNameMap->add(SIGFPE, signalNames[8]); + // signalNumberToNameMap->add(SIGKILL, signalNames[9]); + signalNumberToNameMap->add(SIGUSR1, signalNames[10]); + signalNumberToNameMap->add(SIGSEGV, signalNames[11]); + signalNumberToNameMap->add(SIGUSR2, signalNames[12]); + signalNumberToNameMap->add(SIGPIPE, signalNames[13]); + signalNumberToNameMap->add(SIGALRM, signalNames[14]); + signalNumberToNameMap->add(SIGTERM, signalNames[15]); + signalNumberToNameMap->add(SIGCHLD, signalNames[16]); + signalNumberToNameMap->add(SIGCONT, signalNames[17]); + // signalNumberToNameMap->add(SIGSTOP, signalNames[18]); + signalNumberToNameMap->add(SIGTSTP, signalNames[19]); + signalNumberToNameMap->add(SIGTTIN, signalNames[20]); + signalNumberToNameMap->add(SIGTTOU, signalNames[21]); + signalNumberToNameMap->add(SIGURG, signalNames[22]); + signalNumberToNameMap->add(SIGXCPU, signalNames[23]); + signalNumberToNameMap->add(SIGXFSZ, signalNames[24]); + signalNumberToNameMap->add(SIGVTALRM, signalNames[25]); + signalNumberToNameMap->add(SIGPROF, signalNames[26]); + signalNumberToNameMap->add(SIGWINCH, signalNames[27]); + signalNumberToNameMap->add(SIGIO, signalNames[28]); +#ifdef SIGINFO + signalNameToNumberMap->add(signalNames[29], SIGINFO); +#endif + signalNumberToNameMap->add(SIGSYS, signalNames[30]); + }); + + if (!signalToContextIdsMap) { + signalToContextIdsMap = new HashMap>(); + } + + if (isAdded) { + if (auto signalNumber = signalNameToNumberMap->get(eventName.string())) { + uint32_t contextId = eventEmitter.scriptExecutionContext()->identifier(); + Locker lock { signalToContextIdsMapLock }; + if (!signalToContextIdsMap->contains(signalNumber)) { + HashSet contextIds; + contextIds.add(contextId); + signalToContextIdsMap->set(signalNumber, contextIds); + + lock.unlockEarly(); + + struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + + // Set the handler in the action struct + action.sa_handler = [](int signalNumber) { + if (UNLIKELY(signalNumberToNameMap->find(signalNumber) == signalNumberToNameMap->end())) + return; + + Locker lock { signalToContextIdsMapLock }; + if (UNLIKELY(signalToContextIdsMap->find(signalNumber) == signalToContextIdsMap->end())) + return; + auto contextIds = signalToContextIdsMap->get(signalNumber); + + for (int contextId : contextIds) { + auto* context = ScriptExecutionContext::getScriptExecutionContext(contextId); + if (UNLIKELY(!context)) + continue; + + JSGlobalObject* lexicalGlobalObject = context->jsGlobalObject(); + Zig::GlobalObject* globalObject = static_cast(lexicalGlobalObject); + + Process* process = jsCast(globalObject->processObject()); + + context->postCrossThreadTask(*process, &Process::emitSignalEvent, signalNumber); + } + }; + + // Clear the sa_mask + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, signalNumber); + action.sa_flags = SA_RESTART; + + sigaction(signalNumber, &action, nullptr); + } else { + auto contextIds = signalToContextIdsMap->get(signalNumber); + contextIds.add(contextId); + signalToContextIdsMap->set(signalNumber, contextIds); + } + } + } else { + if (auto signalNumber = signalNameToNumberMap->get(eventName.string())) { + uint32_t contextId = eventEmitter.scriptExecutionContext()->identifier(); + Locker lock { signalToContextIdsMapLock }; + if (signalToContextIdsMap->find(signalNumber) != signalToContextIdsMap->end()) { + HashSet contextIds = signalToContextIdsMap->get(signalNumber); + contextIds.remove(contextId); + if (contextIds.isEmpty()) { + signal(signalNumber, SIG_DFL); + signalToContextIdsMap->remove(signalNumber); + } else { + signalToContextIdsMap->set(signalNumber, contextIds); + } + } + } + } +} + +void Process::emitSignalEvent(int signalNumber) +{ + String signalName = signalNumberToNameMap->get(signalNumber); + Identifier signalNameIdentifier = Identifier::fromString(vm(), signalName); + MarkedArgumentBuffer args; + args.append(jsNumber(signalNumber)); + wrapped().emitForBindings(signalNameIdentifier, args); +} Process::~Process() { - for (auto& listener : this->wrapped().eventListenerMap().entries()) { - } } JSC_DEFINE_HOST_FUNCTION(Process_functionAbort, (JSGlobalObject * globalObject, CallFrame*)) @@ -1550,6 +1674,8 @@ void Process::finishCreation(JSC::VM& vm) { Base::finishCreation(vm); + this->wrapped().onDidChangeListener = &onDidChangeListeners; + this->cpuUsageStructure.initLater([](const JSC::LazyProperty::Initializer& init) { init.set(constructCPUUsageStructure(init.vm, init.owner->globalObject())); }); diff --git a/src/bun.js/bindings/Process.h b/src/bun.js/bindings/Process.h index fbad9b1ff..0ee6f4243 100644 --- a/src/bun.js/bindings/Process.h +++ b/src/bun.js/bindings/Process.h @@ -19,6 +19,8 @@ public: { } + void emitSignalEvent(int signalNumber); + DECLARE_EXPORT_INFO; static void destroy(JSC::JSCell* cell) diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index e8cae5e33..3262bdb5d 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -20,6 +20,12 @@ static HashMap& allSc return contexts; } +ScriptExecutionContext* ScriptExecutionContext::getScriptExecutionContext(ScriptExecutionContextIdentifier identifier) +{ + Locker locker { allScriptExecutionContextsMapLock }; + return allScriptExecutionContextsMap().get(identifier); +} + template static void registerHTTPContextForWebSocket(ScriptExecutionContext* script, us_socket_context_t* ctx, us_loop_t* loop) { diff --git a/src/bun.js/bindings/ScriptExecutionContext.h b/src/bun.js/bindings/ScriptExecutionContext.h index 5f6c56a90..aed7977a5 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.h +++ b/src/bun.js/bindings/ScriptExecutionContext.h @@ -96,7 +96,12 @@ public: } } - const WTF::URL& url() const { return m_url; } + static ScriptExecutionContext* getScriptExecutionContext(ScriptExecutionContextIdentifier identifier); + + const WTF::URL& url() const + { + return m_url; + } bool activeDOMObjectsAreSuspended() { return false; } bool activeDOMObjectsAreStopped() { return false; } bool isContextThread() { return true; } @@ -141,6 +146,7 @@ public: auto* task = new EventLoopTask(WTFMove(lambda)); postTaskOnTimeout(task, timeout); } + template void postCrossThreadTask(Arguments&&... arguments) { diff --git a/src/bun.js/bindings/webcore/EventEmitter.cpp b/src/bun.js/bindings/webcore/EventEmitter.cpp index 0650d624c..0e273042b 100644 --- a/src/bun.js/bindings/webcore/EventEmitter.cpp +++ b/src/bun.js/bindings/webcore/EventEmitter.cpp @@ -35,6 +35,8 @@ bool EventEmitter::addListener(const Identifier& eventType, Ref&& } eventListenersDidChange(); + if (this->onDidChangeListener) + this->onDidChangeListener(*this, eventType, true); return true; } @@ -62,6 +64,9 @@ bool EventEmitter::removeListener(const Identifier& eventType, EventListener& li if (data->eventListenerMap.remove(eventType, listener)) { eventListenersDidChange(); + + if (this->onDidChangeListener) + this->onDidChangeListener(*this, eventType, false); return true; } return false; @@ -93,6 +98,8 @@ bool EventEmitter::removeAllListeners(const Identifier& eventType) if (data->eventListenerMap.removeAll(eventType)) { eventListenersDidChange(); + if (this->onDidChangeListener) + this->onDidChangeListener(*this, eventType, false); return true; } return false; diff --git a/src/bun.js/bindings/webcore/EventEmitter.h b/src/bun.js/bindings/webcore/EventEmitter.h index b46bcff5d..8db59c188 100644 --- a/src/bun.js/bindings/webcore/EventEmitter.h +++ b/src/bun.js/bindings/webcore/EventEmitter.h @@ -67,6 +67,8 @@ public: bool hasActiveEventListeners(const Identifier& eventType) const; bool hasEventListeners(JSC::VM& vm, ASCIILiteral eventType) const; + WTF::Function onDidChangeListener = WTF::Function(nullptr); + unsigned getMaxListeners() const { return m_maxListeners; }; void setMaxListeners(unsigned count); @@ -101,7 +103,9 @@ private: EventEmitterData* eventTargetData() { return &m_eventTargetData; } EventEmitterData* eventTargetDataConcurrently() { return &m_eventTargetData; } EventEmitterData& ensureEventEmitterData() { return m_eventTargetData; } - void eventListenersDidChange() {} + void eventListenersDidChange() + { + } void innerInvokeEventListeners(const Identifier&, SimpleEventListenerVector, const MarkedArgumentBuffer& arguments); void invalidateEventListenerRegions(); diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp index 231ae0db4..959cbd8d7 100644 --- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp +++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp @@ -219,7 +219,7 @@ JSC_DEFINE_CUSTOM_GETTER(jsEventEmitterConstructor, (JSGlobalObject * lexicalGlo return JSValue::encode(JSEventEmitter::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); } -static inline JSC::EncodedJSValue addListener(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis, bool once, bool prepend) +inline JSC::EncodedJSValue JSEventEmitter::addListener(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, JSEventEmitter* castedThis, bool once, bool prepend) { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); @@ -251,7 +251,7 @@ static inline JSC::EncodedJSValue addListener(JSC::JSGlobalObject* lexicalGlobal static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_addListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) { - return addListener(lexicalGlobalObject, callFrame, castedThis, false, false); + return JSEventEmitter::addListener(lexicalGlobalObject, callFrame, castedThis, false, false); } static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_setMaxListenersBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) @@ -280,17 +280,17 @@ static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_getMaxListener static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_addOnceListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) { - return addListener(lexicalGlobalObject, callFrame, castedThis, true, false); + return JSEventEmitter::addListener(lexicalGlobalObject, callFrame, castedThis, true, false); } static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_prependListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) { - return addListener(lexicalGlobalObject, callFrame, castedThis, false, true); + return JSEventEmitter::addListener(lexicalGlobalObject, callFrame, castedThis, false, true); } static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_prependOnceListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) { - return addListener(lexicalGlobalObject, callFrame, castedThis, true, true); + return JSEventEmitter::addListener(lexicalGlobalObject, callFrame, castedThis, true, true); } JSC_DEFINE_HOST_FUNCTION(jsEventEmitterPrototypeFunction_addListener, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) @@ -324,6 +324,11 @@ JSC_DEFINE_HOST_FUNCTION(jsEventEmitterPrototypeFunction_prependOnceListener, (J } static inline JSC::EncodedJSValue jsEventEmitterPrototypeFunction_removeListenerBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + return JSEventEmitter::removeListener(lexicalGlobalObject, callFrame, castedThis); +} + +inline JSC::EncodedJSValue JSEventEmitter::removeListener(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, JSEventEmitter* castedThis) { auto& vm = JSC::getVM(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.h b/src/bun.js/bindings/webcore/JSEventEmitter.h index 855241011..30d62d792 100644 --- a/src/bun.js/bindings/webcore/JSEventEmitter.h +++ b/src/bun.js/bindings/webcore/JSEventEmitter.h @@ -27,6 +27,9 @@ public: static EventEmitter* toWrapped(JSC::VM&, JSC::JSValue); static void destroy(JSC::JSCell*); + static inline JSC::EncodedJSValue addListener(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, JSEventEmitter* castedThis, bool once, bool prepend); + static inline JSC::EncodedJSValue removeListener(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, JSEventEmitter* castedThis); + DECLARE_INFO; static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) diff --git a/test/js/node/process/call-raise.js b/test/js/node/process/call-raise.js new file mode 100644 index 000000000..898906759 --- /dev/null +++ b/test/js/node/process/call-raise.js @@ -0,0 +1,15 @@ +import { dlopen } from "bun:ffi"; + +var lazyRaise; +export function raise(signal) { + if (!lazyRaise) { + const suffix = process.platform === "darwin" ? "dylib" : "so.6"; + lazyRaise = dlopen(`libc.${suffix}`, { + raise: { + args: ["int"], + returns: "int", + }, + }).symbols.raise; + } + lazyRaise(signal); +} diff --git a/test/js/node/process/process-signal-handler.fixture.js b/test/js/node/process/process-signal-handler.fixture.js new file mode 100644 index 000000000..de5a78bda --- /dev/null +++ b/test/js/node/process/process-signal-handler.fixture.js @@ -0,0 +1,63 @@ +import os from "os"; +import { raise } from "./call-raise"; + +var counter = 0; +function done() { + counter++; + if (counter === 2) { + setTimeout(() => { + if (counter !== 2) { + console.log(counter); + console.log("FAIL"); + process.exit(1); + } + + console.log("PASS"); + process.exit(0); + }, 1); + } +} + +var counter2 = 0; +function done2() { + counter2++; + if (counter2 === 2) { + setTimeout(() => { + if (counter2 !== 2) { + console.log(counter2); + console.log("FAIL"); + process.exit(1); + } + + console.log("PASS"); + process.exit(0); + }, 1); + } +} + +const SIGUSR1 = os.constants.signals.SIGUSR1; +const SIGUSR2 = os.constants.signals.SIGUSR2; + +switch (process.argv.at(-1)) { + case "SIGUSR1": { + process.on("SIGUSR1", () => { + done(); + }); + process.on("SIGUSR1", () => { + done(); + }); + raise(SIGUSR1); + break; + } + case "SIGUSR2": { + process.on("SIGUSR2", () => { + done2(); + }); + process.emit("SIGUSR2"); + raise(SIGUSR2); + break; + } + default: { + throw new Error("Unknown argument: " + process.argv.at(-1)); + } +} diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index e038383de..51825b2b4 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1,6 +1,6 @@ -import { resolveSync, spawnSync, which } from "bun"; +import { spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; -import { existsSync, readFileSync, realpathSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { bunEnv, bunExe } from "harness"; import { basename, join, resolve } from "path"; @@ -381,6 +381,28 @@ it("process.getuid", () => { expect(typeof process.getuid()).toBe("number"); }); +describe("signal", () => { + const fixture = join(import.meta.dir, "./process-signal-handler.fixture.js"); + it("simple case works", async () => { + const child = Bun.spawn({ + cmd: [bunExe(), fixture, "SIGUSR1"], + env: bunEnv, + }); + + expect(await child.exited).toBe(0); + expect(await new Response(child.stdout).text()).toBe("PASS\n"); + }); + it("process.emit will call signal events", async () => { + const child = Bun.spawn({ + cmd: [bunExe(), fixture, "SIGUSR2"], + env: bunEnv, + }); + + expect(await child.exited).toBe(0); + expect(await new Response(child.stdout).text()).toBe("PASS\n"); + }); +}); + const undefinedStubs = [ "_debugEnd", "_debugProcess", -- cgit v1.2.3