diff --git a/package.json b/package.json index c9c62c9abe..6d73e6175f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "scripts": { "build": "bun run build:debug", "watch": "zig build check --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", + "watch-windows": "zig build check-windows --watch -fincremental --prominent-compile-errors --global-cache-dir build/debug/zig-check-cache --zig-lib-dir vendor/zig/lib", "bd": "(bun run --silent build:debug &> /tmp/bun.debug.build.log || (cat /tmp/bun.debug.build.log && rm -rf /tmp/bun.debug.build.log && exit 1)) && rm -f /tmp/bun.debug.build.log && ./build/debug/bun-debug", "build:debug": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -B build/debug", "build:valgrind": "bun ./scripts/build.mjs -GNinja -DCMAKE_BUILD_TYPE=Debug -DENABLE_BASELINE=ON -ENABLE_VALGRIND=ON -B build/debug-valgrind", diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index afdf569432..2d86750372 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -6613,9 +6613,10 @@ declare module "bun" { ipc?( message: any, /** - * The {@link Subprocess} that sent the message + * The {@link Subprocess} that received the message */ subprocess: Subprocess, + handle?: unknown, ): void; /** diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index 1dfd4e230c..f4d4bc31c9 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -725,6 +725,20 @@ ssize_t bsd_recv(LIBUS_SOCKET_DESCRIPTOR fd, void *buf, int length, int flags) { } } +#if !defined(_WIN32) +ssize_t bsd_recvmsg(LIBUS_SOCKET_DESCRIPTOR fd, struct msghdr *msg, int flags) { + while (1) { + ssize_t ret = recvmsg(fd, msg, flags); + + if (UNLIKELY(IS_EINTR(ret))) { + continue; + } + + return ret; + } +} +#endif + #if !defined(_WIN32) #include @@ -783,6 +797,20 @@ ssize_t bsd_send(LIBUS_SOCKET_DESCRIPTOR fd, const char *buf, int length, int ms } } +#if !defined(_WIN32) +ssize_t bsd_sendmsg(LIBUS_SOCKET_DESCRIPTOR fd, const struct msghdr *msg, int flags) { + while (1) { + ssize_t rc = sendmsg(fd, msg, flags); + + if (UNLIKELY(IS_EINTR(rc))) { + continue; + } + + return rc; + } +} +#endif + int bsd_would_block() { #ifdef _WIN32 return WSAGetLastError() == WSAEWOULDBLOCK; diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index 652398b2c2..6b734b2100 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -279,14 +279,15 @@ struct us_socket_context_t *us_create_socket_context(int ssl, struct us_loop_t * return context; } -struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err) { +struct us_socket_context_t *us_create_bun_ssl_socket_context(struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err) { #ifndef LIBUS_NO_SSL - if (ssl) { - /* This function will call us, again, with SSL = false and a bigger ext_size */ - return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options, err); - } + /* This function will call us, again, with SSL = false and a bigger ext_size */ + return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options, err); #endif + return us_create_bun_nossl_socket_context(loop, context_ext_size); +} +struct us_socket_context_t *us_create_bun_nossl_socket_context(struct us_loop_t *loop, int context_ext_size) { /* This path is taken once either way - always BEFORE whatever SSL may do LATER. * context_ext_size will however be modified larger in case of SSL, to hold SSL extensions */ @@ -370,8 +371,8 @@ struct us_listen_socket_t *us_socket_context_listen(int ssl, struct us_socket_co ls->s.timeout = 255; ls->s.long_timeout = 255; ls->s.flags.low_prio_state = 0; - ls->s.flags.is_paused = 0; - + ls->s.flags.is_paused = 0; + ls->s.flags.is_ipc = 0; ls->s.next = 0; ls->s.flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN); us_internal_socket_context_link_listen_socket(context, ls); @@ -406,6 +407,7 @@ struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, struct us_sock ls->s.flags.low_prio_state = 0; ls->s.flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN); ls->s.flags.is_paused = 0; + ls->s.flags.is_ipc = 0; ls->s.next = 0; us_internal_socket_context_link_listen_socket(context, ls); @@ -414,7 +416,6 @@ struct us_listen_socket_t *us_socket_context_listen_unix(int ssl, struct us_sock return ls; } - struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_context_t *context, struct sockaddr_storage* addr, int options, int socket_ext_size) { LIBUS_SOCKET_DESCRIPTOR connect_socket_fd = bsd_create_connect_socket(addr, options); if (connect_socket_fd == LIBUS_SOCKET_ERROR) { @@ -437,6 +438,7 @@ struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_cont socket->flags.low_prio_state = 0; socket->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN); socket->flags.is_paused = 0; + socket->flags.is_ipc = 0; socket->connect_state = NULL; @@ -563,6 +565,7 @@ int start_connections(struct us_connecting_socket_t *c, int count) { s->flags.low_prio_state = 0; s->flags.allow_half_open = (c->options & LIBUS_SOCKET_ALLOW_HALF_OPEN); s->flags.is_paused = 0; + s->flags.is_ipc = 0; /* Link it into context so that timeout fires properly */ us_internal_socket_context_link_socket(s->context, s); @@ -739,6 +742,7 @@ struct us_socket_t *us_socket_context_connect_unix(int ssl, struct us_socket_con connect_socket->flags.low_prio_state = 0; connect_socket->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN); connect_socket->flags.is_paused = 0; + connect_socket->flags.is_ipc = 0; connect_socket->connect_state = NULL; connect_socket->connect_next = NULL; us_internal_socket_context_link_socket(context, connect_socket); @@ -843,6 +847,14 @@ void us_socket_context_on_data(int ssl, struct us_socket_context_t *context, str context->on_data = on_data; } +void us_socket_context_on_fd(int ssl, struct us_socket_context_t *context, struct us_socket_t *(*on_fd)(struct us_socket_t *s, int fd)) { +#ifndef LIBUS_NO_SSL + if (ssl) return; +#endif + + context->on_fd = on_fd; +} + void us_socket_context_on_writable(int ssl, struct us_socket_context_t *context, struct us_socket_t *(*on_writable)(struct us_socket_t *s)) { #ifndef LIBUS_NO_SSL if (ssl) { diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 86a8ee7b27..4649f743ba 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -1535,10 +1535,9 @@ us_internal_bun_create_ssl_socket_context( /* Otherwise ee continue by creating a non-SSL context, but with larger ext to * hold our SSL stuff */ struct us_internal_ssl_socket_context_t *context = - (struct us_internal_ssl_socket_context_t *)us_create_bun_socket_context( - 0, loop, - sizeof(struct us_internal_ssl_socket_context_t) + context_ext_size, - options, err); + (struct us_internal_ssl_socket_context_t *)us_create_bun_nossl_socket_context( + loop, + sizeof(struct us_internal_ssl_socket_context_t) + context_ext_size); /* I guess this is the only optional callback */ context->on_server_name = NULL; @@ -2080,8 +2079,8 @@ struct us_internal_ssl_socket_t *us_internal_ssl_socket_wrap_with_tls( us_socket_context_ref(0,old_context); enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; - struct us_socket_context_t *context = us_create_bun_socket_context( - 1, old_context->loop, sizeof(struct us_wrapped_socket_context_t), + struct us_socket_context_t *context = us_create_bun_ssl_socket_context( + old_context->loop, sizeof(struct us_wrapped_socket_context_t), options, &err); // Handle SSL context creation failure diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index 62c6bf1929..b451d00100 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -170,6 +170,8 @@ struct us_socket_flags { bool allow_half_open: 1; /* 0 = not in low-prio queue, 1 = is in low-prio queue, 2 = was in low-prio queue in this iteration */ unsigned char low_prio_state: 2; + /* If true, the socket should be read using readmsg to support receiving file descriptors */ + bool is_ipc: 1; } __attribute__((packed)); @@ -287,6 +289,7 @@ struct us_socket_context_t { struct us_socket_t *(*on_open)(struct us_socket_t *, int is_client, char *ip, int ip_length); struct us_socket_t *(*on_data)(struct us_socket_t *, char *data, int length); + struct us_socket_t *(*on_fd)(struct us_socket_t *, int fd); struct us_socket_t *(*on_writable)(struct us_socket_t *); struct us_socket_t *(*on_close)(struct us_socket_t *, int code, void *reason); // void (*on_timeout)(struct us_socket_context *); diff --git a/packages/bun-usockets/src/internal/networking/bsd.h b/packages/bun-usockets/src/internal/networking/bsd.h index 56e958508e..c10b96785e 100644 --- a/packages/bun-usockets/src/internal/networking/bsd.h +++ b/packages/bun-usockets/src/internal/networking/bsd.h @@ -207,7 +207,13 @@ int bsd_addr_get_port(struct bsd_addr_t *addr); LIBUS_SOCKET_DESCRIPTOR bsd_accept_socket(LIBUS_SOCKET_DESCRIPTOR fd, struct bsd_addr_t *addr); ssize_t bsd_recv(LIBUS_SOCKET_DESCRIPTOR fd, void *buf, int length, int flags); +#if !defined(_WIN32) +ssize_t bsd_recvmsg(LIBUS_SOCKET_DESCRIPTOR fd, struct msghdr *msg, int flags); +#endif ssize_t bsd_send(LIBUS_SOCKET_DESCRIPTOR fd, const char *buf, int length, int msg_more); +#if !defined(_WIN32) +ssize_t bsd_sendmsg(LIBUS_SOCKET_DESCRIPTOR fd, const struct msghdr *msg, int flags); +#endif ssize_t bsd_write2(LIBUS_SOCKET_DESCRIPTOR fd, const char *header, int header_length, const char *payload, int payload_length); int bsd_would_block(); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index 3f877a1668..6128d855f1 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -264,8 +264,10 @@ enum create_bun_socket_error_t { CREATE_BUN_SOCKET_ERROR_INVALID_CA, }; -struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, +struct us_socket_context_t *us_create_bun_ssl_socket_context(struct us_loop_t *loop, int ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err); +struct us_socket_context_t *us_create_bun_nossl_socket_context(struct us_loop_t *loop, + int ext_size); /* Delete resources allocated at creation time (will call unref now and only free when ref count == 0). */ void us_socket_context_free(int ssl, us_socket_context_r context) nonnull_fn_decl; @@ -280,6 +282,8 @@ void us_socket_context_on_close(int ssl, us_socket_context_r context, struct us_socket_t *(*on_close)(us_socket_r s, int code, void *reason)); void us_socket_context_on_data(int ssl, us_socket_context_r context, struct us_socket_t *(*on_data)(us_socket_r s, char *data, int length)); +void us_socket_context_on_fd(int ssl, us_socket_context_r context, + struct us_socket_t *(*on_fd)(us_socket_r s, int fd)); void us_socket_context_on_writable(int ssl, us_socket_context_r context, struct us_socket_t *(*on_writable)(us_socket_r s)); void us_socket_context_on_timeout(int ssl, us_socket_context_r context, @@ -465,7 +469,7 @@ void us_socket_local_address(int ssl, us_socket_r s, char *nonnull_arg buf, int /* Bun extras */ struct us_socket_t *us_socket_pair(struct us_socket_context_t *ctx, int socket_ext_size, LIBUS_SOCKET_DESCRIPTOR* fds); -struct us_socket_t *us_socket_from_fd(struct us_socket_context_t *ctx, int socket_ext_size, LIBUS_SOCKET_DESCRIPTOR fd); +struct us_socket_t *us_socket_from_fd(struct us_socket_context_t *ctx, int socket_ext_size, LIBUS_SOCKET_DESCRIPTOR fd, int ipc); struct us_socket_t *us_socket_wrap_with_tls(int ssl, us_socket_r s, struct us_bun_socket_context_options_t options, struct us_socket_events_t events, int socket_ext_size); int us_socket_raw_write(int ssl, us_socket_r s, const char *data, int length, int msg_more); struct us_socket_t* us_socket_open(int ssl, struct us_socket_t * s, int is_client, char* ip, int ip_length); diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 5bae5bd38d..031c1a9e97 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -313,6 +313,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in s->flags.low_prio_state = 0; s->flags.allow_half_open = listen_socket->s.flags.allow_half_open; s->flags.is_paused = 0; + s->flags.is_ipc = 0; /* We always use nodelay */ bsd_socket_nodelay(client_fd, 1); @@ -391,7 +392,43 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in const int recv_flags = MSG_DONTWAIT | MSG_NOSIGNAL; #endif - int length = bsd_recv(us_poll_fd(&s->p), loop->data.recv_buf + LIBUS_RECV_BUFFER_PADDING, LIBUS_RECV_BUFFER_LENGTH, recv_flags); + int length; + #if !defined(_WIN32) + if(s->flags.is_ipc) { + struct msghdr msg = {0}; + struct iovec iov = {0}; + char cmsg_buf[CMSG_SPACE(sizeof(int))]; + + iov.iov_base = loop->data.recv_buf + LIBUS_RECV_BUFFER_PADDING; + iov.iov_len = LIBUS_RECV_BUFFER_LENGTH; + + msg.msg_flags = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_controllen = CMSG_LEN(sizeof(int)); + msg.msg_control = cmsg_buf; + + length = bsd_recvmsg(us_poll_fd(&s->p), &msg, recv_flags); + + // Extract file descriptor if present + if (length > 0 && msg.msg_controllen > 0) { + struct cmsghdr *cmsg_ptr = CMSG_FIRSTHDR(&msg); + if (cmsg_ptr && cmsg_ptr->cmsg_level == SOL_SOCKET && cmsg_ptr->cmsg_type == SCM_RIGHTS) { + int fd = *(int *)CMSG_DATA(cmsg_ptr); + s = s->context->on_fd(s, fd); + if(us_socket_is_closed(0, s)) { + break; + } + } + } + }else{ + #endif + length = bsd_recv(us_poll_fd(&s->p), loop->data.recv_buf + LIBUS_RECV_BUFFER_PADDING, LIBUS_RECV_BUFFER_LENGTH, recv_flags); + #if !defined(_WIN32) + } + #endif if (length > 0) { s = s->context->on_data(s, loop->data.recv_buf + LIBUS_RECV_BUFFER_PADDING, length); diff --git a/packages/bun-usockets/src/socket.c b/packages/bun-usockets/src/socket.c index 91744b7368..94b57ccfd9 100644 --- a/packages/bun-usockets/src/socket.c +++ b/packages/bun-usockets/src/socket.c @@ -284,7 +284,7 @@ struct us_socket_t *us_socket_pair(struct us_socket_context_t *ctx, int socket_e return 0; } - return us_socket_from_fd(ctx, socket_ext_size, fds[0]); + return us_socket_from_fd(ctx, socket_ext_size, fds[0], 0); #endif } @@ -302,7 +302,7 @@ int us_socket_write2(int ssl, struct us_socket_t *s, const char *header, int hea return written < 0 ? 0 : written; } -struct us_socket_t *us_socket_from_fd(struct us_socket_context_t *ctx, int socket_ext_size, LIBUS_SOCKET_DESCRIPTOR fd) { +struct us_socket_t *us_socket_from_fd(struct us_socket_context_t *ctx, int socket_ext_size, LIBUS_SOCKET_DESCRIPTOR fd, int ipc) { #if defined(LIBUS_USE_LIBUV) || defined(WIN32) return 0; #else @@ -321,6 +321,8 @@ struct us_socket_t *us_socket_from_fd(struct us_socket_context_t *ctx, int socke s->flags.low_prio_state = 0; s->flags.allow_half_open = 0; s->flags.is_paused = 0; + s->flags.is_ipc = 0; + s->flags.is_ipc = ipc; s->connect_state = NULL; /* We always use nodelay */ @@ -374,6 +376,44 @@ int us_socket_write(int ssl, struct us_socket_t *s, const char *data, int length return written < 0 ? 0 : written; } +#if !defined(_WIN32) +/* Send a message with data and an attached file descriptor, for use in IPC. Returns the number of bytes written. If that + number is less than the length, the file descriptor was not sent. */ +int us_socket_ipc_write_fd(struct us_socket_t *s, const char* data, int length, int fd) { + if (us_socket_is_closed(0, s) || us_socket_is_shut_down(0, s)) { + return 0; + } + + struct msghdr msg = {0}; + struct iovec iov = {0}; + char cmsgbuf[CMSG_SPACE(sizeof(int))]; + + iov.iov_base = (void*)data; + iov.iov_len = length; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsgbuf; + msg.msg_controllen = CMSG_SPACE(sizeof(int)); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + + *(int *)CMSG_DATA(cmsg) = fd; + + int sent = bsd_sendmsg(us_poll_fd(&s->p), &msg, 0); + + if (sent != length) { + s->context->loop->data.last_write_failed = 1; + us_poll_change(&s->p, s->context->loop, LIBUS_SOCKET_READABLE | LIBUS_SOCKET_WRITABLE); + } + + return sent < 0 ? 0 : sent; +} +#endif + void *us_socket_ext(int ssl, struct us_socket_t *s) { #ifndef LIBUS_NO_SSL if (ssl) { diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 0c549e2f1b..8960a71230 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -468,7 +468,11 @@ public: HttpContext *httpContext; enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; - httpContext = (HttpContext *) us_create_bun_socket_context(SSL, (us_loop_t *) loop, sizeof(HttpContextData), options, &err); + if constexpr (SSL) { + httpContext = (HttpContext *) us_create_bun_ssl_socket_context((us_loop_t *) loop, sizeof(HttpContextData), options, &err); + } else { + httpContext = (HttpContext *) us_create_bun_nossl_socket_context((us_loop_t *) loop, sizeof(HttpContextData)); + } if (!httpContext) { return nullptr; diff --git a/src/Global.zig b/src/Global.zig index 210b02ee02..084ab3ac5a 100644 --- a/src/Global.zig +++ b/src/Global.zig @@ -117,6 +117,11 @@ pub fn exit(code: u32) noreturn { // If we are crashing, allow the crash handler to finish it's work. bun.crash_handler.sleepForeverIfAnotherThreadIsCrashing(); + if (Environment.isDebug) { + bun.assert(bun.debug_allocator_data.backing.?.deinit() == .ok); + bun.debug_allocator_data.backing = null; + } + switch (Environment.os) { .mac => std.c.exit(@bitCast(code)), .windows => { diff --git a/src/bake/FrameworkRouter.zig b/src/bake/FrameworkRouter.zig index 8a9fada1e2..7b6b71b8ca 100644 --- a/src/bake/FrameworkRouter.zig +++ b/src/bake/FrameworkRouter.zig @@ -573,7 +573,7 @@ pub const Style = union(enum) { if (is_optional and !is_catch_all) return log.fail("Optional parameters can only be catch-all (change to \"[[...{s}]]\" or remove extra brackets)", .{param_name}, start, len); // Potential future proofing - if (std.mem.indexOfAny(u8, param_name, "?*{}()=:#,")) |bad_char_index| + if (bun.strings.indexOfAny(param_name, "?*{}()=:#,")) |bad_char_index| return log.fail("Parameter name cannot contain \"{c}\"", .{param_name[bad_char_index]}, start + bad_char_index, 1); if (has_ending_double_bracket and !is_optional) diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 8d34e27793..7b052a8c2d 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -3309,7 +3309,7 @@ pub fn resolveSourceMapping( }; } -extern fn Process__emitMessageEvent(global: *JSGlobalObject, value: JSValue) void; +extern fn Process__emitMessageEvent(global: *JSGlobalObject, value: JSValue, handle: JSValue) void; extern fn Process__emitDisconnectEvent(global: *JSGlobalObject) void; pub extern fn Process__emitErrorEvent(global: *JSGlobalObject, value: JSValue) void; @@ -3328,23 +3328,23 @@ pub const IPCInstance = struct { pub const new = bun.TrivialNew(@This()); pub const deinit = bun.TrivialDeinit(@This()); - globalThis: ?*JSGlobalObject, + globalThis: *JSGlobalObject, context: if (Environment.isPosix) *uws.SocketContext else void, - data: IPC.IPCData, + data: IPC.SendQueue, has_disconnect_called: bool = false, const node_cluster_binding = @import("./node/node_cluster_binding.zig"); - pub fn ipc(this: *IPCInstance) ?*IPC.IPCData { + pub fn ipc(this: *IPCInstance) ?*IPC.SendQueue { return &this.data; } pub fn getGlobalThis(this: *IPCInstance) ?*JSGlobalObject { return this.globalThis; } - pub fn handleIPCMessage(this: *IPCInstance, message: IPC.DecodedIPCMessage) void { + pub fn handleIPCMessage(this: *IPCInstance, message: IPC.DecodedIPCMessage, handle: JSValue) void { JSC.markBinding(@src()); - const globalThis = this.globalThis orelse return; + const globalThis = this.globalThis; const event_loop = JSC.VirtualMachine.get().eventLoop(); switch (message) { @@ -3357,7 +3357,7 @@ pub const IPCInstance = struct { IPC.log("Received IPC message from parent", .{}); event_loop.enter(); defer event_loop.exit(); - Process__emitMessageEvent(globalThis, data); + Process__emitMessageEvent(globalThis, data, handle); }, .internal => |data| { IPC.log("Received IPC internal message from parent", .{}); @@ -3371,7 +3371,6 @@ pub const IPCInstance = struct { pub fn handleIPCClose(this: *IPCInstance) void { IPC.log("IPCInstance#handleIPCClose", .{}); var vm = VirtualMachine.get(); - vm.ipc = null; const event_loop = vm.eventLoop(); node_cluster_binding.child_singleton.deinit(); event_loop.enter(); @@ -3381,12 +3380,11 @@ pub const IPCInstance = struct { uws.us_socket_context_free(0, this.context); } vm.channel_ref.disable(); - this.deinit(); } export fn Bun__closeChildIPC(global: *JSGlobalObject) void { if (global.bunVM().getIPCInstance()) |current_ipc| { - current_ipc.data.close(true); + current_ipc.data.closeSocketNextTick(true); } } @@ -3409,8 +3407,8 @@ pub fn getIPCInstance(this: *VirtualMachine) ?*IPCInstance { const instance = switch (Environment.os) { else => instance: { - const context = uws.us_create_socket_context(0, this.event_loop_handle.?, @sizeOf(usize), .{}).?; - IPC.Socket.configure(context, true, *IPCInstance, IPCInstance.Handlers); + const context = uws.us_create_bun_nossl_socket_context(this.event_loop_handle.?, @sizeOf(usize)).?; + IPC.Socket.configure(context, true, *IPC.SendQueue, IPC.IPCHandlers.PosixSocket); var instance = IPCInstance.new(.{ .globalThis = this.global, @@ -3420,7 +3418,9 @@ pub fn getIPCInstance(this: *VirtualMachine) ?*IPCInstance { this.ipc = .{ .initialized = instance }; - const socket = IPC.Socket.fromFd(context, opts.info, IPCInstance, instance, null) orelse { + instance.data = .init(opts.mode, .{ .virtual_machine = instance }, .uninitialized); + + const socket = IPC.Socket.fromFd(context, opts.info, IPC.SendQueue, &instance.data, null, true) orelse { instance.deinit(); this.ipc = null; Output.warn("Unable to start IPC socket", .{}); @@ -3428,7 +3428,7 @@ pub fn getIPCInstance(this: *VirtualMachine) ?*IPCInstance { }; socket.setTimeout(0); - instance.data = .{ .socket = socket, .mode = opts.mode }; + instance.data.socket = .{ .open = socket }; break :instance instance; }, @@ -3436,12 +3436,13 @@ pub fn getIPCInstance(this: *VirtualMachine) ?*IPCInstance { var instance = IPCInstance.new(.{ .globalThis = this.global, .context = {}, - .data = .{ .mode = opts.mode }, + .data = undefined, }); + instance.data = .init(opts.mode, .{ .virtual_machine = instance }, .uninitialized); this.ipc = .{ .initialized = instance }; - instance.data.configureClient(IPCInstance, instance, opts.info) catch { + instance.data.windowsConfigureClient(opts.info) catch { instance.deinit(); this.ipc = null; Output.warn("Unable to start IPC pipe '{}'", .{opts.info}); diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index e5a0d4e935..0640b10e9c 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -791,7 +791,7 @@ pub const H2FrameParser = struct { const stream = this.parser.streams.getEntry(this.stream_id) orelse return; const value = stream.value_ptr; if (value.state != .CLOSED) { - this.parser.abortStream(value, reason); + this.parser.abortStream(value, Bun__wrapAbortError(this.parser.globalThis, reason)); } } @@ -3905,7 +3905,7 @@ pub const H2FrameParser = struct { if (signal_arg.as(JSC.WebCore.AbortSignal)) |signal_| { if (signal_.aborted()) { stream.state = .IDLE; - this.abortStream(stream, signal_.abortReason()); + this.abortStream(stream, Bun__wrapAbortError(globalObject, signal_.abortReason())); return JSC.JSValue.jsNumber(stream_id); } stream.attachSignal(this, signal_); @@ -4270,6 +4270,8 @@ pub const H2FrameParser = struct { } }; +extern fn Bun__wrapAbortError(globalObject: *JSC.JSGlobalObject, cause: JSC.JSValue) JSC.JSValue; + pub fn createNodeHttp2Binding(global: *JSC.JSGlobalObject) JSC.JSValue { return JSC.JSArray.create(global, &.{ H2FrameParser.js.getConstructor(global), diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index c9e4ba6892..8c24b2b28a 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -311,6 +311,7 @@ const Handlers = struct { pub const SocketConfig = struct { hostname_or_unix: JSC.ZigString.Slice, port: ?u16 = null, + fd: ?bun.FileDescriptor = null, ssl: ?JSC.API.ServerConfig.SSLConfig = null, handlers: Handlers, default_data: JSC.JSValue = .zero, @@ -341,6 +342,7 @@ pub const SocketConfig = struct { var hostname_or_unix: JSC.ZigString.Slice = JSC.ZigString.Slice.empty; errdefer hostname_or_unix.deinit(); var port: ?u16 = null; + var fd: ?bun.FileDescriptor = null; var exclusive = false; var allowHalfOpen = false; var reusePort = false; @@ -370,6 +372,7 @@ pub const SocketConfig = struct { hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { if (fd_.isNumber()) { + fd = fd_.asFileDescriptor(); break :hostname_or_unix; } } @@ -468,6 +471,7 @@ pub const SocketConfig = struct { return SocketConfig{ .hostname_or_unix = hostname_or_unix, .port = port, + .fd = fd, .ssl = ssl, .handlers = handlers, .default_data = default_data, @@ -693,13 +697,10 @@ pub const Listener = struct { vm.eventLoop().ensureWaker(); var create_err: uws.create_bun_socket_error_t = .none; - const socket_context = uws.us_create_bun_socket_context( - @intFromBool(ssl_enabled), - uws.Loop.get(), - @sizeOf(usize), - ctx_opts, - &create_err, - ) orelse { + const socket_context = switch (ssl_enabled) { + true => uws.us_create_bun_ssl_socket_context(uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err), + false => uws.us_create_bun_nossl_socket_context(uws.Loop.get(), @sizeOf(usize)), + } orelse { var err = globalObject.createErrorInstance("Failed to listen on {s}:{d}", .{ hostname_or_unix.slice(), port orelse 0 }); defer { socket_config.handlers.unprotect(); @@ -759,7 +760,7 @@ pub const Listener = struct { var connection: Listener.UnixOrHost = if (port) |port_| .{ .host = .{ .host = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch bun.outOfMemory()).slice(), .port = port_ }, - } else .{ + } else if (socket_config.fd) |fd| .{ .fd = fd } else .{ .unix = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch bun.outOfMemory()).slice(), }; var errno: c_int = 0; @@ -789,7 +790,10 @@ pub const Listener = struct { defer bun.default_allocator.free(host); break :brk uws.us_socket_context_listen_unix(@intFromBool(ssl_enabled), socket_context, host, host.len, socket_flags, 8, &errno); }, - .fd => unreachable, + .fd => |fd| { + _ = fd; + return globalObject.ERR(.INVALID_ARG_VALUE, "Bun does not support listening on a file descriptor.", .{}).throw(); + }, } } orelse { defer { @@ -1202,7 +1206,10 @@ pub const Listener = struct { .{}; var create_err: uws.create_bun_socket_error_t = .none; - const socket_context = uws.us_create_bun_socket_context(@intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err) orelse { + const socket_context = switch (ssl_enabled) { + true => uws.us_create_bun_ssl_socket_context(uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err), + false => uws.us_create_bun_nossl_socket_context(uws.Loop.get(), @sizeOf(usize)), + } orelse { const err = JSC.SystemError{ .message = bun.String.static("Failed to connect"), .syscall = bun.String.static("connect"), @@ -1464,7 +1471,7 @@ fn NewSocket(comptime ssl: bool) type { ); }, .fd => |f| { - const socket = This.Socket.fromFd(this.socket_context.?, f, This, this, null) orelse return error.ConnectionFailed; + const socket = This.Socket.fromFd(this.socket_context.?, f, This, this, null, false) orelse return error.ConnectionFailed; this.onOpen(socket); }, } diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 27061bc9ad..7e3a53f6cf 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -30,7 +30,7 @@ has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true) this_jsvalue: JSC.JSValue = .zero, /// `null` indicates all of the IPC data is uninitialized. -ipc_data: ?IPC.IPCData, +ipc_data: ?IPC.SendQueue, flags: Flags = .{}, weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, @@ -751,66 +751,12 @@ pub fn onStdinDestroyed(this: *Subprocess) void { pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { IPClog("Subprocess#doSend", .{}); - var message, var handle, var options_, var callback = callFrame.argumentsAsArray(4); - if (handle.isFunction()) { - callback = handle; - handle = .undefined; - options_ = .undefined; - } else if (options_.isFunction()) { - callback = options_; - options_ = .undefined; - } else if (!options_.isUndefined()) { - try global.validateObject("options", options_, .{}); - } - - const S = struct { - fn impl(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments_ = callframe.arguments_old(1).slice(); - const ex = arguments_[0]; - JSC.VirtualMachine.Process__emitErrorEvent(globalThis, ex); - return .undefined; - } - }; - - const ipc_data = &(this.ipc_data orelse { - if (this.hasExited()) { - return global.ERR(.IPC_CHANNEL_CLOSED, "Subprocess.send() cannot be used after the process has exited.", .{}).throw(); - } else { - return global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); - } - }); - - if (message.isUndefined()) { - return global.throwMissingArgumentsValue(&.{"message"}); - } - if (!message.isString() and !message.isObject() and !message.isNumber() and !message.isBoolean() and !message.isNull()) { - return global.throwInvalidArgumentTypeValueOneOf("message", "string, object, number, or boolean", message); - } - - const good = ipc_data.serializeAndSend(global, message); - - if (good) { - if (callback.isFunction()) { - callback.callNextTick(global, .{JSValue.null}); - // we need to wait until the send is actually completed to trigger the callback - } - } else { - const ex = global.createTypeErrorInstance("process.send() failed", .{}); - ex.put(global, JSC.ZigString.static("syscall"), bun.String.static("write").toJS(global)); - if (callback.isFunction()) { - callback.callNextTick(global, .{ex}); - } else { - const fnvalue = JSC.JSFunction.create(global, "", S.impl, 1, .{}); - fnvalue.callNextTick(global, .{ex}); - } - } - - return .false; + return IPC.doSend(if (this.ipc_data) |*data| data else null, global, callFrame, if (this.hasExited()) .subprocess_exited else .subprocess); } pub fn disconnectIPC(this: *Subprocess, nextTick: bool) void { const ipc_data = this.ipc() orelse return; - ipc_data.close(nextTick); + ipc_data.closeSocketNextTick(nextTick); } pub fn disconnect(this: *Subprocess, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { _ = globalThis; @@ -822,7 +768,7 @@ pub fn disconnect(this: *Subprocess, globalThis: *JSGlobalObject, callframe: *JS pub fn getConnected(this: *Subprocess, globalThis: *JSGlobalObject) JSValue { _ = globalThis; const ipc_data = this.ipc(); - return JSValue.jsBoolean(ipc_data != null and ipc_data.?.disconnected == false); + return JSValue.jsBoolean(ipc_data != null and ipc_data.?.isConnected()); } pub fn pid(this: *const Subprocess) i32 { @@ -1771,6 +1717,10 @@ pub fn finalize(this: *Subprocess) callconv(.C) void { MaxBuf.removeFromSubprocess(&this.stdout_maxbuf); MaxBuf.removeFromSubprocess(&this.stderr_maxbuf); + if (this.ipc_data != null) { + this.disconnectIPC(false); + } + this.flags.finalized = true; this.deref(); } @@ -2324,9 +2274,21 @@ pub fn spawnMaybeSync( &spawn_options, @ptrCast(argv.items.ptr), @ptrCast(env_array.items.ptr), - ) catch |err| { - spawn_options.deinit(); - return globalThis.throwError(err, ": failed to spawn process") catch return .zero; + ) catch |err| switch (err) { + error.EMFILE, error.ENFILE => { + spawn_options.deinit(); + const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null) + std.mem.sliceTo(argv.items[0].?, 0) + else + ""; + var systemerror = bun.sys.Error.fromCode(if (err == error.EMFILE) .MFILE else .NFILE, .posix_spawn).withPath(display_path).toSystemError(); + systemerror.errno = if (err == error.EMFILE) -bun.sys.UV_E.MFILE else -bun.sys.UV_E.NFILE; + return globalThis.throwValue(systemerror.toErrorInstance(globalThis)); + }, + else => { + spawn_options.deinit(); + return globalThis.throwError(err, ": failed to spawn process") catch return .zero; + }, }) { .err => |err| { spawn_options.deinit(); @@ -2415,9 +2377,9 @@ pub fn spawnMaybeSync( .ref_count = .initExactRefs(2), .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), .ipc_data = if (!is_sync and comptime Environment.isWindows) - if (maybe_ipc_mode) |ipc_mode| .{ - .mode = ipc_mode, - } else null + if (maybe_ipc_mode) |ipc_mode| ( // + .init(ipc_mode, .{ .subprocess = subprocess }, .uninitialized) // + ) else null else null, @@ -2436,30 +2398,24 @@ pub fn spawnMaybeSync( if (maybe_ipc_mode) |mode| { if (uws.us_socket_from_fd( jsc_vm.rareData().spawnIPCContext(jsc_vm), - @sizeOf(*Subprocess), + @sizeOf(*IPC.SendQueue), posix_ipc_fd.cast(), + 1, )) |socket| { + subprocess.ipc_data = .init(mode, .{ .subprocess = subprocess }, .uninitialized); posix_ipc_info = IPC.Socket.from(socket); - subprocess.ipc_data = .{ - .socket = posix_ipc_info, - .mode = mode, - }; } } } if (subprocess.ipc_data) |*ipc_data| { if (Environment.isPosix) { - if (posix_ipc_info.ext(*Subprocess)) |ctx| { - ctx.* = subprocess; - subprocess.ref(); // + one ref for the IPC + if (posix_ipc_info.ext(*IPC.SendQueue)) |ctx| { + ctx.* = &subprocess.ipc_data.?; + subprocess.ipc_data.?.socket = .{ .open = posix_ipc_info }; } } else { - subprocess.ref(); // + one ref for the IPC - - if (ipc_data.configureServer( - Subprocess, - subprocess, + if (ipc_data.windowsConfigureServer( subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer, ).asErr()) |err| { subprocess.deref(); @@ -2663,6 +2619,7 @@ const node_cluster_binding = @import("./../../node/node_cluster_binding.zig"); pub fn handleIPCMessage( this: *Subprocess, message: IPC.DecodedIPCMessage, + handle: JSC.JSValue, ) void { IPClog("Subprocess#handleIPCMessage", .{}); switch (message) { @@ -2682,7 +2639,7 @@ pub fn handleIPCMessage( cb, globalThis, this_jsvalue, - &[_]JSValue{ data, this_jsvalue }, + &[_]JSValue{ data, this_jsvalue, handle }, ); } } @@ -2701,34 +2658,24 @@ pub fn handleIPCClose(this: *Subprocess) void { const globalThis = this.globalThis; this.updateHasPendingActivity(); - defer this.deref(); - var ok = false; - if (this.ipc()) |ipc_data| { - ok = true; - ipc_data.internal_msg_queue.deinit(); - } - this.ipc_data = null; - if (this_jsvalue != .zero) { // Avoid keeping the callback alive longer than necessary JSC.Codegen.JSSubprocess.ipcCallbackSetCached(this_jsvalue, globalThis, .zero); // Call the onDisconnectCallback if it exists and prevent it from being kept alive longer than necessary if (consumeOnDisconnectCallback(this_jsvalue, globalThis)) |callback| { - globalThis.bunVM().eventLoop().runCallback(callback, globalThis, this_jsvalue, &.{JSValue.jsBoolean(ok)}); + globalThis.bunVM().eventLoop().runCallback(callback, globalThis, this_jsvalue, &.{JSValue.jsBoolean(true)}); } } } -pub fn ipc(this: *Subprocess) ?*IPC.IPCData { +pub fn ipc(this: *Subprocess) ?*IPC.SendQueue { return &(this.ipc_data orelse return null); } pub fn getGlobalThis(this: *Subprocess) ?*JSC.JSGlobalObject { return this.globalThis; } -pub const IPCHandler = IPC.NewIPCHandler(Subprocess); - const default_allocator = bun.default_allocator; const bun = @import("bun"); const Environment = bun.Environment; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index c9afb6f6f1..0925964702 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -141,6 +141,7 @@ extern "C" uint8_t Bun__getExitCode(void*); extern "C" uint8_t Bun__setExitCode(void*, uint8_t); extern "C" bool Bun__closeChildIPC(JSGlobalObject*); +extern "C" bool Bun__GlobalObject__connectedIPC(JSGlobalObject*); extern "C" bool Bun__GlobalObject__hasIPC(JSGlobalObject*); extern "C" bool Bun__ensureProcessIPCInitialized(JSGlobalObject*); extern "C" const char* Bun__githubURL; @@ -1460,7 +1461,7 @@ JSC_DEFINE_CUSTOM_GETTER(processConnected, (JSC::JSGlobalObject * lexicalGlobalO return JSValue::encode(jsUndefined()); } - return JSValue::encode(jsBoolean(Bun__GlobalObject__hasIPC(process->globalObject()))); + return JSValue::encode(jsBoolean(Bun__GlobalObject__connectedIPC(process->globalObject()))); } JSC_DEFINE_CUSTOM_SETTER(setProcessConnected, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) { @@ -2274,26 +2275,16 @@ static JSValue constructProcessSend(VM& vm, JSObject* processObject) } } -JSC_DEFINE_HOST_FUNCTION(processDisonnectFinish, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - Bun__closeChildIPC(globalObject); - return JSC::JSValue::encode(jsUndefined()); -} - JSC_DEFINE_HOST_FUNCTION(Bun__Process__disconnect, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - auto& vm = JSC::getVM(globalObject); auto global = jsCast(globalObject); - if (!Bun__GlobalObject__hasIPC(globalObject)) { + if (!Bun__GlobalObject__connectedIPC(globalObject)) { Process__emitErrorEvent(global, JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s))); return JSC::JSValue::encode(jsUndefined()); } - auto finishFn = JSC::JSFunction::create(vm, globalObject, 0, String("finish"_s), processDisonnectFinish, ImplementationVisibility::Public); - auto process = jsCast(global->processObject()); - - process->queueNextTick(globalObject, finishFn); + Bun__closeChildIPC(globalObject); return JSC::JSValue::encode(jsUndefined()); } @@ -3618,7 +3609,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionLoadBuiltinModule, (JSGlobalObject * gl RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); } -extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSValue value) +extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSValue value, EncodedJSValue handle) { auto* process = static_cast(global->processObject()); auto& vm = JSC::getVM(global); @@ -3627,6 +3618,7 @@ extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSVa if (process->wrapped().hasEventListeners(ident)) { JSC::MarkedArgumentBuffer args; args.append(JSValue::decode(value)); + args.append(JSValue::decode(handle)); process->wrapped().emit(ident, args); } } diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 981a97b8e0..1a8c0cb80e 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -25,6 +25,7 @@ #include "JavaScriptCore/ErrorInstanceInlines.h" #include "JavaScriptCore/JSInternalFieldObjectImplInlines.h" #include "JSDOMException.h" +#include "JSDOMExceptionHandling.h" #include #include "ErrorCode.h" #include "ErrorStackTrace.h" @@ -100,37 +101,30 @@ namespace Bun { using namespace JSC; using namespace WTF; -static JSC::JSObject* createErrorPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::ErrorType type, WTF::ASCIILiteral name, WTF::ASCIILiteral code, bool isDOMExceptionPrototype) +static JSC::JSObject* createErrorPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::ErrorType type, WTF::ASCIILiteral name, WTF::ASCIILiteral code) { JSC::JSObject* prototype; - // Inherit from DOMException - // But preserve the error.stack property. - if (isDOMExceptionPrototype) { - auto* domGlobalObject = defaultGlobalObject(globalObject); - prototype = JSC::constructEmptyObject(globalObject, WebCore::JSDOMException::prototype(vm, *domGlobalObject)); - } else { - switch (type) { - case JSC::ErrorType::TypeError: - prototype = JSC::constructEmptyObject(globalObject, globalObject->m_typeErrorStructure.prototype(globalObject)); - break; - case JSC::ErrorType::RangeError: - prototype = JSC::constructEmptyObject(globalObject, globalObject->m_rangeErrorStructure.prototype(globalObject)); - break; - case JSC::ErrorType::Error: - prototype = JSC::constructEmptyObject(globalObject, globalObject->errorPrototype()); - break; - case JSC::ErrorType::URIError: - prototype = JSC::constructEmptyObject(globalObject, globalObject->m_URIErrorStructure.prototype(globalObject)); - break; - case JSC::ErrorType::SyntaxError: - prototype = JSC::constructEmptyObject(globalObject, globalObject->m_syntaxErrorStructure.prototype(globalObject)); - break; - default: { - RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO: Add support for more error types"); - break; - } - } + switch (type) { + case JSC::ErrorType::TypeError: + prototype = JSC::constructEmptyObject(globalObject, globalObject->m_typeErrorStructure.prototype(globalObject)); + break; + case JSC::ErrorType::RangeError: + prototype = JSC::constructEmptyObject(globalObject, globalObject->m_rangeErrorStructure.prototype(globalObject)); + break; + case JSC::ErrorType::Error: + prototype = JSC::constructEmptyObject(globalObject, globalObject->errorPrototype()); + break; + case JSC::ErrorType::URIError: + prototype = JSC::constructEmptyObject(globalObject, globalObject->m_URIErrorStructure.prototype(globalObject)); + break; + case JSC::ErrorType::SyntaxError: + prototype = JSC::constructEmptyObject(globalObject, globalObject->m_syntaxErrorStructure.prototype(globalObject)); + break; + default: { + RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO: Add support for more error types"); + break; + } } prototype->putDirect(vm, vm.propertyNames->name, jsString(vm, String(name)), 0); @@ -187,18 +181,18 @@ static ErrorCodeCache* errorCache(Zig::GlobalObject* globalObject) } // clang-format on -static Structure* createErrorStructure(JSC::VM& vm, JSGlobalObject* globalObject, JSC::ErrorType type, WTF::ASCIILiteral name, WTF::ASCIILiteral code, bool isDOMExceptionPrototype) +static Structure* createErrorStructure(JSC::VM& vm, JSGlobalObject* globalObject, JSC::ErrorType type, WTF::ASCIILiteral name, WTF::ASCIILiteral code) { - auto* prototype = createErrorPrototype(vm, globalObject, type, name, code, isDOMExceptionPrototype); + auto* prototype = createErrorPrototype(vm, globalObject, type, name, code); return ErrorInstance::createStructure(vm, globalObject, prototype); } -JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options, bool isDOMExceptionPrototype) +JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options) { auto* cache = errorCache(globalObject); const auto& data = errors[static_cast(code)]; if (!cache->internalField(static_cast(code))) { - auto* structure = createErrorStructure(vm, globalObject, data.type, data.name, data.code, isDOMExceptionPrototype); + auto* structure = createErrorStructure(vm, globalObject, data.type, data.name, data.code); cache->internalField(static_cast(code)).set(vm, cache, structure); } @@ -206,44 +200,44 @@ JSObject* ErrorCodeCache::createError(VM& vm, Zig::GlobalObject* globalObject, E return JSC::ErrorInstance::create(globalObject, structure, message, options, nullptr, JSC::RuntimeType::TypeNothing, data.type, true); } -JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, const String& message, bool isDOMExceptionPrototype) +JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, const String& message) { - return errorCache(globalObject)->createError(vm, globalObject, code, jsString(vm, message), jsUndefined(), isDOMExceptionPrototype); + return errorCache(globalObject)->createError(vm, globalObject, code, jsString(vm, message), jsUndefined()); } -JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, const String& message, bool isDOMExceptionPrototype) +JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, const String& message) { - return createError(globalObject->vm(), globalObject, code, message, isDOMExceptionPrototype); + return createError(globalObject->vm(), globalObject, code, message); } -JSObject* createError(VM& vm, JSC::JSGlobalObject* globalObject, ErrorCode code, const String& message, bool isDOMExceptionPrototype) +JSObject* createError(VM& vm, JSC::JSGlobalObject* globalObject, ErrorCode code, const String& message) { - return createError(vm, defaultGlobalObject(globalObject), code, message, isDOMExceptionPrototype); + return createError(vm, defaultGlobalObject(globalObject), code, message); } -JSObject* createError(VM& vm, JSC::JSGlobalObject* globalObject, ErrorCode code, JSValue message, bool isDOMExceptionPrototype) +JSObject* createError(VM& vm, JSC::JSGlobalObject* globalObject, ErrorCode code, JSValue message) { if (auto* zigGlobalObject = jsDynamicCast(globalObject)) - return createError(vm, zigGlobalObject, code, message, jsUndefined(), isDOMExceptionPrototype); + return createError(vm, zigGlobalObject, code, message, jsUndefined()); - auto* structure = createErrorStructure(vm, globalObject, errors[static_cast(code)].type, errors[static_cast(code)].name, errors[static_cast(code)].code, isDOMExceptionPrototype); + auto* structure = createErrorStructure(vm, globalObject, errors[static_cast(code)].type, errors[static_cast(code)].name, errors[static_cast(code)].code); return JSC::ErrorInstance::create(globalObject, structure, message, jsUndefined(), nullptr, JSC::RuntimeType::TypeNothing, errors[static_cast(code)].type, true); } -JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options, bool isDOMExceptionPrototype) +JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options) { - return errorCache(globalObject)->createError(vm, globalObject, code, message, options, isDOMExceptionPrototype); + return errorCache(globalObject)->createError(vm, globalObject, code, message, options); } -JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, const String& message, bool isDOMExceptionPrototype) +JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, const String& message) { - return createError(globalObject->vm(), globalObject, code, message, isDOMExceptionPrototype); + return createError(globalObject->vm(), globalObject, code, message); } -JSObject* createError(Zig::JSGlobalObject* globalObject, ErrorCode code, JSC::JSValue message, bool isDOMExceptionPrototype) +JSObject* createError(Zig::JSGlobalObject* globalObject, ErrorCode code, JSC::JSValue message) { auto& vm = JSC::getVM(globalObject); - return createError(vm, globalObject, code, message, isDOMExceptionPrototype); + return createError(vm, globalObject, code, message); } extern "C" BunString Bun__inspect(JSC::JSGlobalObject* globalObject, JSValue value); @@ -843,7 +837,7 @@ JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JS JSValueToStringSafe(globalObject, builder, value, true); RETURN_IF_EXCEPTION(throwScope, {}); - auto* structure = createErrorStructure(vm, globalObject, ErrorType::RangeError, "RangeError"_s, "ERR_INVALID_ARG_VALUE"_s, false); + auto* structure = createErrorStructure(vm, globalObject, ErrorType::RangeError, "RangeError"_s, "ERR_INVALID_ARG_VALUE"_s); auto error = JSC::ErrorInstance::create(vm, structure, builder.toString(), jsUndefined(), nullptr, JSC::RuntimeType::TypeNothing, ErrorType::RangeError, true); throwScope.throwException(globalObject, error); return {}; @@ -1502,6 +1496,25 @@ void throwCryptoOperationFailed(JSGlobalObject* globalObject, JSC::ThrowScope& s } // namespace Bun +extern "C" JSC::EncodedJSValue Bun__wrapAbortError(JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue causeParam) +{ + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto cause = JSC::JSValue::decode(causeParam); + + if (cause.isUndefined()) { + return JSC::JSValue::encode(Bun::createError(vm, globalObject, Bun::ErrorCode::ABORT_ERR, JSC::JSValue(globalObject->commonStrings().OperationWasAbortedString(globalObject)))); + } + + auto message = globalObject->commonStrings().OperationWasAbortedString(globalObject); + JSC::JSObject* options = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 24); + options->putDirect(vm, JSC::Identifier::fromString(vm, "cause"_s), cause); + + auto error = Bun::createError(vm, globalObject, Bun::ErrorCode::ABORT_ERR, message, options); + return JSC::JSValue::encode(error); +} + JSC_DEFINE_HOST_FUNCTION(jsFunctionMakeAbortError, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { auto* globalObject = defaultGlobalObject(lexicalGlobalObject); @@ -1512,28 +1525,25 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionMakeAbortError, (JSC::JSGlobalObject * lexica if (!options.isUndefined() && options.isCell() && !options.asCell()->isObject()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, options); if (message.isUndefined() && options.isUndefined()) { - return JSValue::encode(Bun::createError(vm, lexicalGlobalObject, Bun::ErrorCode::ABORT_ERR, JSValue(globalObject->commonStrings().OperationWasAbortedString(globalObject)), false)); + return JSValue::encode(Bun::createError(vm, lexicalGlobalObject, Bun::ErrorCode::ABORT_ERR, JSValue(globalObject->commonStrings().OperationWasAbortedString(globalObject)))); } if (message.isUndefined()) message = globalObject->commonStrings().OperationWasAbortedString(globalObject); - auto error = Bun::createError(vm, globalObject, Bun::ErrorCode::ABORT_ERR, message, options, false); + auto error = Bun::createError(vm, globalObject, Bun::ErrorCode::ABORT_ERR, message, options); return JSC::JSValue::encode(error); } JSC::JSValue WebCore::toJS(JSC::JSGlobalObject* globalObject, CommonAbortReason abortReason) { - auto* zigGlobalObject = defaultGlobalObject(globalObject); switch (abortReason) { case CommonAbortReason::Timeout: { - return createError(globalObject, Bun::ErrorCode::ABORT_ERR, zigGlobalObject->commonStrings().OperationWasAbortedString(globalObject), true); + return createDOMException(globalObject, ExceptionCode::TimeoutError, "The operation timed out."_s); } case CommonAbortReason::UserAbort: { - // This message is a standardized error message. We cannot change it. - // https://webidl.spec.whatwg.org/#idl-DOMException:~:text=The%20operation%20was%20aborted. - return createError(globalObject, Bun::ErrorCode::ABORT_ERR, zigGlobalObject->commonStrings().OperationWasAbortedString(globalObject), true); + return createDOMException(globalObject, ExceptionCode::AbortError, "The operation was aborted."_s); } case CommonAbortReason::ConnectionClosed: { - return createError(globalObject, Bun::ErrorCode::ABORT_ERR, zigGlobalObject->commonStrings().ConnectionWasClosedString(globalObject), true); + return createDOMException(globalObject, ExceptionCode::AbortError, "The connection was closed."_s); } default: { break; @@ -2232,6 +2242,8 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_DGRAM_NOT_RUNNING, "Socket is not running"_s)); case ErrorCode::ERR_INVALID_CURSOR_POS: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_CURSOR_POS, "Cannot set cursor row without setting its column"_s)); + case ErrorCode::ERR_INVALID_HANDLE_TYPE: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_HANDLE_TYPE, "This handle type cannot be sent"_s)); case ErrorCode::ERR_MULTIPLE_CALLBACK: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_MULTIPLE_CALLBACK, "Callback called multiple times"_s)); case ErrorCode::ERR_STREAM_PREMATURE_CLOSE: diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 7eb7588d3a..b5877701d2 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -40,7 +40,7 @@ public: static ErrorCodeCache* create(VM& vm, Structure* structure); static Structure* createStructure(VM& vm, JSGlobalObject* globalObject); - JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options, bool isDOMExceptionPrototype); + JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options); private: JS_EXPORT_PRIVATE ErrorCodeCache(VM&, Structure*); @@ -49,10 +49,10 @@ private: }; JSC::EncodedJSValue throwError(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, ErrorCode code, const WTF::String& message); -JSC::JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, const WTF::String& message, bool isDOMExceptionPrototype = false); -JSC::JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, const WTF::String& message, bool isDOMExceptionPrototype = false); -JSC::JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, JSC::JSValue message, bool isDOMExceptionPrototype = false); -JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options, bool isDOMExceptionPrototype = false); +JSC::JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, const WTF::String& message); +JSC::JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, const WTF::String& message); +JSC::JSObject* createError(Zig::GlobalObject* globalObject, ErrorCode code, JSC::JSValue message); +JSC::JSObject* createError(VM& vm, Zig::GlobalObject* globalObject, ErrorCode code, JSValue message, JSValue options); JSC::JSValue toJS(JSC::JSGlobalObject*, ErrorCode); JSObject* createInvalidThisError(JSGlobalObject* globalObject, JSValue thisValue, const ASCIILiteral typeName); JSObject* createInvalidThisError(JSGlobalObject* globalObject, const String& message); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index bf39cc34ff..1c77a48cf9 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -127,6 +127,7 @@ const errors: ErrorCodeMapping = [ ["ERR_INVALID_CURSOR_POS", TypeError], ["ERR_INVALID_FILE_URL_HOST", TypeError], ["ERR_INVALID_FILE_URL_PATH", TypeError], + ["ERR_INVALID_HANDLE_TYPE", TypeError], ["ERR_INVALID_HTTP_TOKEN", TypeError], ["ERR_INVALID_IP_ADDRESS", TypeError], ["ERR_INVALID_MIME_SYNTAX", TypeError], diff --git a/src/bun.js/bindings/IPC.cpp b/src/bun.js/bindings/IPC.cpp new file mode 100644 index 0000000000..24ef361934 --- /dev/null +++ b/src/bun.js/bindings/IPC.cpp @@ -0,0 +1,37 @@ +#include "root.h" +#include "headers-handwritten.h" +#include "BunBuiltinNames.h" +#include "WebCoreJSBuiltins.h" + +extern "C" JSC::EncodedJSValue IPCSerialize(JSC::JSGlobalObject* global, JSC::JSValue message, JSC::JSValue handle) +{ + auto& vm = JSC::getVM(global); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSFunction* serializeFunction = JSC::JSFunction::create(vm, global, WebCore::ipcSerializeCodeGenerator(vm), global); + JSC::CallData callData = JSC::getCallData(serializeFunction); + + JSC::MarkedArgumentBuffer args; + args.append(message); + args.append(handle); + + auto result = JSC::call(global, serializeFunction, callData, JSC::jsUndefined(), args); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(result); +} + +extern "C" JSC::EncodedJSValue IPCParse(JSC::JSGlobalObject* global, JSC::JSValue target, JSC::JSValue serialized, JSC::JSValue fd) +{ + auto& vm = JSC::getVM(global); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSFunction* parseFunction = JSC::JSFunction::create(vm, global, WebCore::ipcParseHandleCodeGenerator(vm), global); + JSC::CallData callData = JSC::getCallData(parseFunction); + + JSC::MarkedArgumentBuffer args; + args.append(target); + args.append(serialized); + args.append(fd); + + auto result = JSC::call(global, parseFunction, callData, JSC::jsUndefined(), args); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(result); +} diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index bf35de3cdf..ea24587481 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1668,6 +1668,7 @@ pub const JSValue = enum(i64) { return JSC__JSValue__eqlCell(this, other); } + /// This must match the enum in C++ in src/bun.js/bindings/bindings.cpp BuiltinNamesMap pub const BuiltinName = enum(u8) { method, headers, @@ -1692,6 +1693,7 @@ pub const JSValue = enum(i64) { ignoreBOM, type, signal, + cmd, pub fn has(property: []const u8) bool { return bun.ComptimeEnumMap(BuiltinName).has(property); diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index 4b9e5ba42a..cae14b6f7b 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -103,7 +103,7 @@ us_socket_context_t* ScriptExecutionContext::webSocketContextSSL() // but do not reject unauthorized opts.reject_unauthorized = false; enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; - this->m_ssl_client_websockets_ctx = us_create_bun_socket_context(1, loop, sizeof(size_t), opts, &err); + this->m_ssl_client_websockets_ctx = us_create_bun_ssl_socket_context(loop, sizeof(size_t), opts, &err); void** ptr = reinterpret_cast(us_socket_context_ext(1, m_ssl_client_websockets_ctx)); *ptr = this; registerHTTPContextForWebSocket(this, m_ssl_client_websockets_ctx, loop); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index fbce5027e1..266490368d 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5341,6 +5341,7 @@ JSC::EncodedJSValue JSC__JSValue__createUninitializedUint8Array(JSC::JSGlobalObj return JSC::JSValue::encode(value); } +// This enum must match the zig enum in src/bun.js/bindings/JSValue.zig JSValue.BuiltinName enum class BuiltinNamesMap : uint8_t { method, headers, @@ -5365,6 +5366,7 @@ enum class BuiltinNamesMap : uint8_t { ignoreBOM, type, signal, + cmd, }; static inline const JSC::Identifier& builtinNameMap(JSC::VM& vm, unsigned char name) @@ -5441,6 +5443,9 @@ static inline const JSC::Identifier& builtinNameMap(JSC::VM& vm, unsigned char n case BuiltinNamesMap::signal: { return clientData->builtinNames().signalPublicName(); } + case BuiltinNamesMap::cmd: { + return clientData->builtinNames().cmdPublicName(); + } default: { ASSERT_NOT_REACHED(); __builtin_unreachable(); diff --git a/src/bun.js/bindings/helpers.cpp b/src/bun.js/bindings/helpers.cpp index 834bd0a36b..c1179335d6 100644 --- a/src/bun.js/bindings/helpers.cpp +++ b/src/bun.js/bindings/helpers.cpp @@ -2,6 +2,9 @@ #include "helpers.h" #include "BunClientData.h" #include +#ifdef _WIN32 +#include +#endif JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral message, ASCIILiteral syscall, int err) { @@ -17,7 +20,12 @@ JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral message JSC::JSValue createSystemError(JSC::JSGlobalObject* global, ASCIILiteral syscall, int err) { auto errstr = String::fromLatin1(Bun__errnoName(err)); - auto* instance = JSC::createError(global, makeString(syscall, "() failed: "_s, errstr, ": "_s, String::fromLatin1(strerror(err)))); +#ifdef _WIN32 + auto strerr = uv_strerror(err); +#else + auto strerr = strerror(err); +#endif + auto* instance = JSC::createError(global, makeString(syscall, "() failed: "_s, errstr, ": "_s, String::fromLatin1(strerr))); auto& vm = global->vm(); auto& builtinNames = WebCore::builtinNames(vm); instance->putDirect(vm, builtinNames.syscallPublicName(), jsString(vm, String(syscall)), 0); diff --git a/src/bun.js/bindings/node/crypto/JSDiffieHellmanConstructor.cpp b/src/bun.js/bindings/node/crypto/JSDiffieHellmanConstructor.cpp index 90219e6b69..f388977278 100644 --- a/src/bun.js/bindings/node/crypto/JSDiffieHellmanConstructor.cpp +++ b/src/bun.js/bindings/node/crypto/JSDiffieHellmanConstructor.cpp @@ -92,7 +92,7 @@ JSC_DEFINE_HOST_FUNCTION(constructDiffieHellman, (JSC::JSGlobalObject * globalOb } if (!generatorValue.isNumber()) { - return JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "Second argument must be an int32"_s, false)); + return JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_ARG_TYPE, "Second argument must be an int32"_s)); } int32_t generator = 0; diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 1c263afa22..b8d395466f 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -11,11 +11,19 @@ const Allocator = std.mem.Allocator; const JSC = bun.JSC; const JSValue = JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; +const uv = bun.windows.libuv; const node_cluster_binding = @import("./node/node_cluster_binding.zig"); pub const log = Output.scoped(.IPC, false); +const IsInternal = enum { internal, external }; +const SerializeAndSendResult = enum { + success, + failure, + backoff, +}; + /// Mode of Inter-Process Communication. pub const Mode = enum { /// Uses SerializedScriptValue to send data. Only valid for bun <--> bun communication. @@ -82,7 +90,7 @@ const advanced = struct { } const message_type: IPCMessageType = @enumFromInt(data[0]); - const message_len: u32 = @as(*align(1) const u32, @ptrCast(data[1 .. @sizeOf(u32) + 1])).*; + const message_len = std.mem.readInt(u32, data[1 .. @sizeOf(u32) + 1], .little); log("Received IPC message type {d} ({s}) len {d}", .{ @intFromEnum(message_type), @@ -97,7 +105,7 @@ const advanced = struct { .message = .{ .version = message_len }, }; }, - .SerializedMessage => { + .SerializedMessage, .SerializedInternalMessage => |tag| { if (data.len < (header_length + message_len)) { log("Not enough bytes to decode IPC message body of len {d}, have {d} bytes", .{ message_len, data.len }); return IPCDecodeError.NotEnoughBytes; @@ -112,25 +120,7 @@ const advanced = struct { return .{ .bytes_consumed = header_length + message_len, - .message = .{ .data = deserialized }, - }; - }, - .SerializedInternalMessage => { - if (data.len < (header_length + message_len)) { - log("Not enough bytes to decode IPC message body of len {d}, have {d} bytes", .{ message_len, data.len }); - return IPCDecodeError.NotEnoughBytes; - } - - const message = data[header_length .. header_length + message_len]; - const deserialized = JSValue.deserialize(message, global); - - if (deserialized == .zero) { - return IPCDecodeError.InvalidFormat; - } - - return .{ - .bytes_consumed = header_length + message_len, - .message = .{ .internal = deserialized }, + .message = if (tag == .SerializedInternalMessage) .{ .internal = deserialized } else .{ .data = deserialized }, }; }, _ => { @@ -142,26 +132,16 @@ const advanced = struct { pub inline fn getVersionPacket() []const u8 { return comptime std.mem.asBytes(&VersionPacket{}); } - - pub fn serialize(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - const serialized = value.serialize(global, true) orelse return IPCSerializationError.SerializationFailed; - defer serialized.deinit(); - - const size: u32 = @intCast(serialized.data.len); - - const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; - - try writer.ensureUnusedCapacity(payload_length); - - writer.writeTypeAsBytesAssumeCapacity(IPCMessageType, .SerializedMessage); - writer.writeTypeAsBytesAssumeCapacity(u32, size); - writer.writeAssumeCapacity(serialized.data); - - return payload_length; + pub fn getAckPacket() []const u8 { + return "\x02\x24\x00\x00\x00\r\x00\x00\x00\x02\x03\x00\x00\x80cmd\x10\x0f\x00\x00\x80NODE_HANDLE_ACK\xff\xff\xff\xff"; + } + pub fn getNackPacket() []const u8 { + return "\x02\x25\x00\x00\x00\r\x00\x00\x00\x02\x03\x00\x00\x80cmd\x10\x10\x00\x00\x80NODE_HANDLE_NACK\xff\xff\xff\xff"; } - pub fn serializeInternal(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - const serialized = value.serialize(global, true) orelse return IPCSerializationError.SerializationFailed; + pub fn serialize(writer: *bun.io.StreamBuffer, global: *JSC.JSGlobalObject, value: JSValue, is_internal: IsInternal) !usize { + const serialized = value.serialize(global, true) orelse + return IPCSerializationError.SerializationFailed; defer serialized.deinit(); const size: u32 = @intCast(serialized.data.len); @@ -170,7 +150,10 @@ const advanced = struct { try writer.ensureUnusedCapacity(payload_length); - writer.writeTypeAsBytesAssumeCapacity(IPCMessageType, .SerializedInternalMessage); + writer.writeTypeAsBytesAssumeCapacity(IPCMessageType, switch (is_internal) { + .internal => .SerializedInternalMessage, + .external => .SerializedMessage, + }); writer.writeTypeAsBytesAssumeCapacity(u32, size); writer.writeAssumeCapacity(serialized.data); @@ -186,33 +169,33 @@ const json = struct { pub fn getVersionPacket() []const u8 { return &.{}; } + pub fn getAckPacket() []const u8 { + return "{\"cmd\":\"NODE_HANDLE_ACK\"}\n"; + } + pub fn getNackPacket() []const u8 { + return "{\"cmd\":\"NODE_HANDLE_NACK\"}\n"; + } - // In order to not have to do a property lookup json messages sent from Bun will have a single u8 prepended to them + // In order to not have to do a property lookup internal messages sent from Bun will have a single u8 prepended to them // to be able to distinguish whether it is a regular json message or an internal one for cluster ipc communication. - // 1 is regular // 2 is internal + // ["[{\d\.] is regular pub fn decodeIPCMessage(data: []const u8, globalThis: *JSC.JSGlobalObject) IPCDecodeError!DecodeIPCMessageResult { // { "foo": "bar"} // tag is 1 or 2 if (bun.strings.indexOfChar(data, '\n')) |idx| { - var kind = data[0]; var json_data = data[0..idx]; - - switch (kind) { - 2 => { - json_data = data[1..idx]; - }, - else => { - // assume it's valid json with no header - // any error will be thrown by toJSByParseJSON below - kind = 1; - }, - } - - // no point in attempting to JSON.parse an empty string + // bounds-check for the following json_data[0] // TODO: should we return NotEnoughBytes? if (json_data.len == 0) return error.InvalidFormat; + var kind: enum { regular, internal } = .regular; + if (json_data[0] == 2) { + // internal message + json_data = json_data[1..]; + kind = .internal; + } + const is_ascii = bun.strings.isAllASCII(json_data); var was_ascii_string_freed = false; @@ -246,21 +229,20 @@ const json = struct { }; return switch (kind) { - 1 => .{ + .regular => .{ .bytes_consumed = idx + 1, .message = .{ .data = deserialized }, }, - 2 => .{ + .internal => .{ .bytes_consumed = idx + 1, .message = .{ .internal = deserialized }, }, - else => @panic("invalid ipc json message kind this is a bug in Bun."), }; } return IPCDecodeError.NotEnoughBytes; } - pub fn serialize(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { + pub fn serialize(writer: *bun.io.StreamBuffer, global: *JSC.JSGlobalObject, value: JSValue, is_internal: IsInternal) !usize { var out: bun.String = undefined; value.jsonStringify(global, 0, &out); defer out.deref(); @@ -273,34 +255,18 @@ const json = struct { const slice = str.slice(); - try writer.ensureUnusedCapacity(slice.len + 1); + var result_len: usize = slice.len + 1; + if (is_internal == .internal) result_len += 1; + try writer.ensureUnusedCapacity(result_len); + + if (is_internal == .internal) { + writer.writeAssumeCapacity(&.{2}); + } writer.writeAssumeCapacity(slice); writer.writeAssumeCapacity("\n"); - return slice.len + 1; - } - - pub fn serializeInternal(_: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - var out: bun.String = undefined; - value.jsonStringify(global, 0, &out); - defer out.deref(); - - if (out.tag == .Dead) return IPCSerializationError.SerializationFailed; - - // TODO: it would be cool to have a 'toUTF8Into' which can write directly into 'ipc_data.outgoing.list' - const str = out.toUTF8(bun.default_allocator); - defer str.deinit(); - - const slice = str.slice(); - - try writer.ensureUnusedCapacity(1 + slice.len + 1); - - writer.writeAssumeCapacity(&.{2}); - writer.writeAssumeCapacity(slice); - writer.writeAssumeCapacity("\n"); - - return 1 + slice.len + 1; + return result_len; } }; @@ -320,330 +286,623 @@ pub fn getVersionPacket(mode: Mode) []const u8 { /// Given a writer interface, serialize and write a value. /// Returns true if the value was written, false if it was not. -pub fn serialize(data: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - return switch (data.mode) { - inline else => |t| @field(@This(), @tagName(t)).serialize(data, writer, global, value), +pub fn serialize(mode: Mode, writer: *bun.io.StreamBuffer, global: *JSC.JSGlobalObject, value: JSValue, is_internal: IsInternal) !usize { + return switch (mode) { + .advanced => advanced.serialize(writer, global, value, is_internal), + .json => json.serialize(writer, global, value, is_internal), }; } -/// Given a writer interface, serialize and write a value. -/// Returns true if the value was written, false if it was not. -pub fn serializeInternal(data: *IPCData, writer: anytype, global: *JSC.JSGlobalObject, value: JSValue) !usize { - return switch (data.mode) { - inline else => |t| @field(@This(), @tagName(t)).serializeInternal(data, writer, global, value), +pub fn getAckPacket(mode: Mode) []const u8 { + return switch (mode) { + .advanced => advanced.getAckPacket(), + .json => json.getAckPacket(), + }; +} + +pub fn getNackPacket(mode: Mode) []const u8 { + return switch (mode) { + .advanced => advanced.getNackPacket(), + .json => json.getNackPacket(), }; } pub const Socket = uws.NewSocketHandler(false); -/// Used on POSIX -const SocketIPCData = struct { - socket: Socket, - mode: Mode, +pub const Handle = struct { + fd: bun.FileDescriptor, + js: JSC.JSValue, + pub fn init(fd: bun.FileDescriptor, js: JSC.JSValue) @This() { + js.protect(); + return .{ .fd = fd, .js = js }; + } + fn deinit(self: *Handle) void { + self.js.unprotect(); + } +}; +pub const CallbackList = union(enum) { + ack_nack, + none, + /// js callable + callback: JSC.JSValue, + /// js array + callback_array: JSC.JSValue, - incoming: bun.ByteList = .{}, // Maybe we should use StreamBuffer here as well - outgoing: bun.io.StreamBuffer = .{}, - has_written_version: if (Environment.allow_assert) u1 else u0 = 0, - internal_msg_queue: node_cluster_binding.InternalMsgHolder = .{}, - disconnected: bool = false, - is_server: bool = false, - keep_alive: bun.Async.KeepAlive = .{}, - close_next_tick: ?JSC.Task = null, - - pub fn writeVersionPacket(this: *SocketIPCData, global: *JSC.JSGlobalObject) void { - if (Environment.allow_assert) { - bun.assert(this.has_written_version == 0); - } - const bytes = getVersionPacket(this.mode); - if (bytes.len > 0) { - const n = this.socket.write(bytes, false); - if (n >= 0 and n < @as(i32, @intCast(bytes.len))) { - this.outgoing.write(bytes[@intCast(n)..]) catch bun.outOfMemory(); - // more remaining; need to ref event loop - this.keep_alive.ref(global.bunVM()); - } - } - if (Environment.allow_assert) { - this.has_written_version = 1; + /// protects the callback + pub fn init(callback: JSC.JSValue) @This() { + if (callback.isCallable()) { + callback.protect(); + return .{ .callback = callback }; } + return .none; } - pub fn serializeAndSend(ipc_data: *SocketIPCData, global: *JSGlobalObject, value: JSValue) bool { - if (Environment.allow_assert) { - bun.assert(ipc_data.has_written_version == 1); - } - - // TODO: probably we should not direct access ipc_data.outgoing.list.items here - const start_offset = ipc_data.outgoing.list.items.len; - - const payload_length = serialize(ipc_data, &ipc_data.outgoing, global, value) catch return false; - - bun.assert(ipc_data.outgoing.list.items.len == start_offset + payload_length); - - if (start_offset == 0) { - bun.assert(ipc_data.outgoing.cursor == 0); - const n = ipc_data.socket.write(ipc_data.outgoing.list.items.ptr[start_offset..payload_length], false); - if (n == payload_length) { - ipc_data.outgoing.reset(); - } else if (n > 0) { - ipc_data.outgoing.cursor = @intCast(n); - // more remaining; need to ref event loop - ipc_data.keep_alive.ref(global.bunVM()); - } - } - - return true; - } - - pub fn serializeAndSendInternal(ipc_data: *SocketIPCData, global: *JSGlobalObject, value: JSValue) bool { - if (Environment.allow_assert) { - bun.assert(ipc_data.has_written_version == 1); - } - - // TODO: probably we should not direct access ipc_data.outgoing.list.items here - const start_offset = ipc_data.outgoing.list.items.len; - - const payload_length = serializeInternal(ipc_data, &ipc_data.outgoing, global, value) catch return false; - - bun.assert(ipc_data.outgoing.list.items.len == start_offset + payload_length); - - if (start_offset == 0) { - bun.assert(ipc_data.outgoing.cursor == 0); - const n = ipc_data.socket.write(ipc_data.outgoing.list.items.ptr[start_offset..payload_length], false); - if (n == payload_length) { - ipc_data.outgoing.reset(); - } else if (n > 0) { - ipc_data.outgoing.cursor = @intCast(n); - // more remaining; need to ref event loop - ipc_data.keep_alive.ref(global.bunVM()); - } - } - - return true; - } - - pub fn close(this: *SocketIPCData, nextTick: bool) void { - log("SocketIPCData#close", .{}); - if (this.disconnected) return; - this.disconnected = true; - if (nextTick) { - if (this.close_next_tick != null) return; - this.close_next_tick = JSC.ManagedTask.New(SocketIPCData, closeTask).init(this); - JSC.VirtualMachine.get().enqueueTask(this.close_next_tick.?); - } else { - this.closeTask(); + /// protects the callback + pub fn push(self: *@This(), callback: JSC.JSValue, global: *JSC.JSGlobalObject) void { + switch (self.*) { + .ack_nack => unreachable, + .none => { + callback.protect(); + self.* = .{ .callback = callback }; + }, + .callback => { + const prev = self.callback; + const arr = JSC.JSValue.createEmptyArray(global, 2); + arr.protect(); + arr.putIndex(global, 0, prev); // add the old callback to the array + arr.putIndex(global, 1, callback); // add the new callback to the array + prev.unprotect(); // owned by the array now + self.* = .{ .callback_array = arr }; + }, + .callback_array => |arr| { + arr.push(global, callback); + }, } } + fn callNextTick(self: *@This(), global: *JSC.JSGlobalObject) void { + switch (self.*) { + .ack_nack => {}, + .none => {}, + .callback => { + self.callback.callNextTick(global, .{.null}); + self.callback.unprotect(); + self.* = .none; + }, + .callback_array => { + var iter = self.callback_array.arrayIterator(global); + while (iter.next()) |item| { + item.callNextTick(global, .{.null}); + } + self.callback_array.unprotect(); + self.* = .none; + }, + } + } + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ack_nack => {}, + .none => {}, + .callback => self.callback.unprotect(), + .callback_array => self.callback_array.unprotect(), + } + self.* = .none; + } +}; +pub const SendHandle = struct { + // when a message has a handle, make sure it has a new SendHandle - so that if we retry sending it, + // we only retry sending the message with the handle, not the original message. + data: bun.io.StreamBuffer = .{}, + /// keep sending the handle until data is drained (assume it hasn't sent until data is fully drained) + handle: ?Handle, + callbacks: CallbackList, - pub fn closeTask(this: *SocketIPCData) void { - log("SocketIPCData#closeTask", .{}); - this.close_next_tick = null; - bun.assert(this.disconnected); - this.socket.close(.normal); + pub fn isAckNack(self: *SendHandle) bool { + return self.callbacks == .ack_nack; + } + + /// Call the callback and deinit + pub fn complete(self: *SendHandle, global: *JSC.JSGlobalObject) void { + self.callbacks.callNextTick(global); + self.deinit(); + } + pub fn deinit(self: *SendHandle) void { + self.data.deinit(); + self.callbacks.deinit(); + if (self.handle) |*handle| { + handle.deinit(); + } } }; -/// Used on Windows -const NamedPipeIPCData = struct { - const uv = bun.windows.libuv; +pub const WindowsWrite = struct { + write_req: uv.uv_write_t = std.mem.zeroes(uv.uv_write_t), + write_buffer: uv.uv_buf_t = uv.uv_buf_t.init(""), + write_slice: []const u8, + owner: ?*SendQueue, + pub fn destroy(self: *WindowsWrite) void { + bun.default_allocator.free(self.write_slice); + bun.destroy(self); + } +}; +pub const SendQueue = struct { + queue: std.ArrayList(SendHandle), + waiting_for_ack: ?SendHandle = null, - mode: Mode, - - // we will use writer pipe as Duplex - writer: bun.io.StreamingWriter(@This(), .{ - .onWrite = onWrite, - .onError = onError, - .onWritable = null, - .onClose = onPipeClose, - }) = .{}, - - incoming: bun.ByteList = .{}, // Maybe we should use IPCBuffer here as well - disconnected: bool = false, - is_server: bool = false, - connect_req: uv.uv_connect_t = std.mem.zeroes(uv.uv_connect_t), - onClose: ?CloseHandler = null, + retry_count: u32 = 0, + keep_alive: bun.Async.KeepAlive = .{}, has_written_version: if (Environment.allow_assert) u1 else u0 = 0, + mode: Mode, internal_msg_queue: node_cluster_binding.InternalMsgHolder = .{}, + incoming: bun.ByteList = .{}, // Maybe we should use StreamBuffer here as well + incoming_fd: ?bun.FileDescriptor = null, - const CloseHandler = struct { - callback: *const fn (*anyopaque) void, - context: *anyopaque, + socket: SocketUnion, + owner: SendQueueOwner, + + close_next_tick: ?JSC.Task = null, + write_in_progress: bool = false, + close_event_sent: bool = false, + + windows: switch (Environment.isWindows) { + true => struct { + is_server: bool = false, + windows_write: ?*WindowsWrite = null, + try_close_after_write: bool = false, + }, + false => struct {}, + } = .{}, + + pub const SendQueueOwner = union(enum) { + subprocess: *bun.api.Subprocess, + virtual_machine: *bun.JSC.VirtualMachine.IPCInstance, }; + pub const SocketType = switch (Environment.isWindows) { + true => *uv.Pipe, + false => Socket, + }; + pub const SocketUnion = union(enum) { + uninitialized, + open: SocketType, + closed, + }; + + pub fn init(mode: Mode, owner: SendQueueOwner, socket: SocketUnion) @This() { + log("SendQueue#init", .{}); + return .{ .queue = .init(bun.default_allocator), .mode = mode, .owner = owner, .socket = socket }; + } + pub fn deinit(self: *@This()) void { + log("SendQueue#deinit", .{}); + // must go first + self.closeSocket(.failure, .deinit); + + for (self.queue.items) |*item| item.deinit(); + self.queue.deinit(); + self.internal_msg_queue.deinit(); + self.incoming.deinitWithAllocator(bun.default_allocator); + if (self.waiting_for_ack) |*waiting| waiting.deinit(); + + // if there is a close next tick task, cancel it so it doesn't get called and then UAF + if (self.close_next_tick) |close_next_tick_task| { + const managed: *bun.JSC.ManagedTask = close_next_tick_task.as(bun.JSC.ManagedTask); + managed.cancel(); + } + } + + pub fn isConnected(this: *SendQueue) bool { + if (Environment.isWindows and this.windows.try_close_after_write) return false; + return this.socket == .open and this.close_next_tick == null; + } + + fn closeSocket(this: *SendQueue, reason: enum { normal, failure }, from: enum { user, deinit }) void { + log("SendQueue#closeSocket {s}", .{@tagName(from)}); + switch (this.socket) { + .open => |s| switch (Environment.isWindows) { + true => { + const pipe: *uv.Pipe = s; + const stream: *uv.uv_stream_t = pipe.asStream(); + stream.readStop(); + + if (this.windows.windows_write != null and from != .deinit) { + log("SendQueue#closeSocket -> mark ready for close", .{}); + // currently writing; wait for the write to complete + this.windows.try_close_after_write = true; + } else { + log("SendQueue#closeSocket -> close now", .{}); + this._windowsClose(); + } + }, + false => { + s.close(switch (reason) { + .normal => .normal, + .failure => .failure, + }); + this._socketClosed(); + }, + }, + else => { + this._socketClosed(); + }, + } + } + fn _socketClosed(this: *SendQueue) void { + log("SendQueue#_socketClosed", .{}); + if (Environment.isWindows) { + if (this.windows.windows_write) |windows_write| { + windows_write.owner = null; // so _windowsOnWriteComplete doesn't try to continue writing + } + this.windows.windows_write = null; // will be freed by _windowsOnWriteComplete + } + this.keep_alive.disable(); + this.socket = .closed; + this._onAfterIPCClosed(); + } + fn _windowsClose(this: *SendQueue) void { + log("SendQueue#_windowsClose", .{}); + if (this.socket != .open) return; + const pipe = this.socket.open; + pipe.data = pipe; + pipe.close(&_windowsOnClosed); + this._socketClosed(); + this._onAfterIPCClosed(); + } + fn _windowsOnClosed(windows: *uv.Pipe) callconv(.C) void { + log("SendQueue#_windowsOnClosed", .{}); + bun.default_allocator.destroy(windows); + } + + pub fn closeSocketNextTick(this: *SendQueue, nextTick: bool) void { + log("SendQueue#closeSocketNextTick", .{}); + if (this.socket != .open) { + this.socket = .closed; + return; + } + if (this.close_next_tick != null) return; // close already requested + if (!nextTick) { + this.closeSocket(.normal, .user); + return; + } + this.close_next_tick = JSC.ManagedTask.New(SendQueue, _closeSocketTask).init(this); + JSC.VirtualMachine.get().enqueueTask(this.close_next_tick.?); + } + + fn _closeSocketTask(this: *SendQueue) void { + log("SendQueue#closeSocketTask", .{}); + bun.assert(this.close_next_tick != null); + this.close_next_tick = null; + this.closeSocket(.normal, .user); + } + + fn _onAfterIPCClosed(this: *SendQueue) void { + log("SendQueue#_onAfterIPCClosed", .{}); + if (this.close_event_sent) return; + this.close_event_sent = true; + switch (this.owner) { + inline else => |owner| { + owner.handleIPCClose(); + }, + } + } + + /// returned pointer is invalidated if the queue is modified + pub fn startMessage(self: *SendQueue, global: *JSC.JSGlobalObject, callback: JSC.JSValue, handle: ?Handle) *SendHandle { + log("SendQueue#startMessage", .{}); + if (Environment.allow_assert) bun.debugAssert(self.has_written_version == 1); + + // optimal case: appending a message without a handle to the end of the queue when the last message also doesn't have a handle and isn't ack/nack + // this is rare. it will only happen if messages stack up after sending a handle, or if a long message is sent that is waiting for writable + if (handle == null and self.queue.items.len > 0) { + const last = &self.queue.items[self.queue.items.len - 1]; + if (last.handle == null and !last.isAckNack() and !(self.queue.items.len == 1 and self.write_in_progress)) { + if (callback.isCallable()) { + last.callbacks.push(callback, global); + } + // caller can append now + return last; + } + } + + // fallback case: append a new message to the queue + self.queue.append(.{ .handle = handle, .callbacks = .init(callback) }) catch bun.outOfMemory(); + return &self.queue.items[self.queue.items.len - 1]; + } + /// returned pointer is invalidated if the queue is modified + pub fn insertMessage(this: *SendQueue, message: SendHandle) void { + log("SendQueue#insertMessage", .{}); + if (Environment.allow_assert) bun.debugAssert(this.has_written_version == 1); + if ((this.queue.items.len == 0 or this.queue.items[0].data.cursor == 0) and !this.write_in_progress) { + // prepend (we have not started sending the next message yet because we are waiting for the ack/nack) + this.queue.insert(0, message) catch bun.outOfMemory(); + } else { + // insert at index 1 (we are in the middle of sending a message to the other process) + bun.debugAssert(this.queue.items[0].isAckNack()); + this.queue.insert(1, message) catch bun.outOfMemory(); + } + } + + pub fn onAckNack(this: *SendQueue, global: *JSGlobalObject, ack_nack: enum { ack, nack }) void { + log("SendQueue#onAckNack", .{}); + if (this.waiting_for_ack == null) { + log("onAckNack: ack received but not waiting for ack", .{}); + return; + } + const item = &this.waiting_for_ack.?; + if (item.handle == null) { + log("onAckNack: ack received but waiting_for_ack is not a handle message?", .{}); + return; + } + if (ack_nack == .nack) { + // retry up to three times + this.retry_count += 1; + if (this.retry_count < MAX_HANDLE_RETRANSMISSIONS) { + // retry sending the message + item.data.cursor = 0; + this.insertMessage(item.*); + this.waiting_for_ack = null; + log("IPC call continueSend() from onAckNack retry", .{}); + return this.continueSend(global, .new_message_appended); + } + // too many retries; give up + var warning = bun.String.static("Handle did not reach the receiving process correctly"); + var warning_name = bun.String.static("SentHandleNotReceivedWarning"); + global.emitWarning( + warning.transferToJS(global), + warning_name.transferToJS(global), + .undefined, + .undefined, + ) catch |e| { + _ = global.takeException(e); + }; + // (fall through to success code in order to consume the message and continue sending) + } + // consume the message and continue sending + item.complete(global); // call the callback & deinit + this.waiting_for_ack = null; + log("IPC call continueSend() from onAckNack success", .{}); + this.continueSend(global, .new_message_appended); + } + fn shouldRef(this: *SendQueue) bool { + if (this.waiting_for_ack != null) return true; // waiting to receive an ack/nack from the other side + if (this.queue.items.len == 0) return false; // nothing to send + const first = &this.queue.items[0]; + if (first.data.cursor > 0) return true; // send in progress, waiting on writable + if (this.write_in_progress) return true; // send in progress (windows), waiting on writable + return false; // error state. + } + pub fn updateRef(this: *SendQueue, global: *JSGlobalObject) void { + switch (this.shouldRef()) { + true => this.keep_alive.ref(global.bunVM()), + false => this.keep_alive.unref(global.bunVM()), + } + } + const ContinueSendReason = enum { + new_message_appended, + on_writable, + }; + fn continueSend(this: *SendQueue, global: *JSC.JSGlobalObject, reason: ContinueSendReason) void { + log("IPC continueSend: {s}", .{@tagName(reason)}); + this.debugLogMessageQueue(); + defer this.updateRef(global); + + if (this.queue.items.len == 0) { + return; // nothing to send + } + if (this.write_in_progress) { + return; // write in progress + } + + const first = &this.queue.items[0]; + if (this.waiting_for_ack != null and !first.isAckNack()) { + // waiting for ack/nack. may not send any items until it is received. + // only allowed to send the message if it is an ack/nack itself. + return; + } + if (reason != .on_writable and first.data.cursor != 0) { + // the last message isn't fully sent yet, we're waiting for a writable event + return; + } + const to_send = first.data.list.items[first.data.cursor..]; + if (to_send.len == 0) { + // item's length is 0, remove it and continue sending. this should rarely (never?) happen. + var itm = this.queue.orderedRemove(0); + itm.complete(global); // call the callback & deinit + log("IPC call continueSend() from empty item", .{}); + return continueSend(this, global, reason); + } + // log("sending ipc message: '{'}' (has_handle={})", .{ std.zig.fmtEscapes(to_send), first.handle != null }); + bun.assert(!this.write_in_progress); + this.write_in_progress = true; + this._write(to_send, if (first.handle) |handle| handle.fd else null); + // the write is queued. this._onWriteComplete() will be called when the write completes. + } + fn _onWriteComplete(this: *SendQueue, n: i32) void { + log("SendQueue#_onWriteComplete {d}", .{n}); + this.debugLogMessageQueue(); + if (!this.write_in_progress or this.queue.items.len < 1) { + bun.debugAssert(false); + return; + } + this.write_in_progress = false; + const globalThis = this.getGlobalThis(); + defer this.updateRef(globalThis); + const first = &this.queue.items[0]; + const to_send = first.data.list.items[first.data.cursor..]; + if (n == to_send.len) { + if (first.handle) |_| { + // the message was fully written, but it had a handle. + // we must wait for ACK or NACK before sending any more messages. + if (this.waiting_for_ack != null) { + log("[error] already waiting for ack. this should never happen.", .{}); + } + // shift the item off the queue and move it to waiting_for_ack + const item = this.queue.orderedRemove(0); + this.waiting_for_ack = item; + } else { + // the message was fully sent, but there may be more items in the queue. + // shift the queue and try to send the next item immediately. + var item = this.queue.orderedRemove(0); + item.complete(globalThis); // call the callback & deinit + } + return continueSend(this, globalThis, .on_writable); + } else if (n > 0 and n < @as(i32, @intCast(first.data.list.items.len))) { + // the item was partially sent; update the cursor and wait for writable to send the rest + // (if we tried to send a handle, a partial write means the handle wasn't sent yet.) + first.data.cursor += @intCast(n); + return; + } else if (n == 0) { + // no bytes written; wait for writable + return; + } else { + // error. close socket. + this.closeSocket(.failure, .deinit); + return; + } + } + pub fn writeVersionPacket(this: *SendQueue, global: *JSGlobalObject) void { + log("SendQueue#writeVersionPacket", .{}); + bun.debugAssert(this.has_written_version == 0); + bun.debugAssert(this.queue.items.len == 0); + bun.debugAssert(this.waiting_for_ack == null); + const bytes = getVersionPacket(this.mode); + if (bytes.len > 0) { + this.queue.append(.{ .handle = null, .callbacks = .none }) catch bun.outOfMemory(); + this.queue.items[this.queue.items.len - 1].data.write(bytes) catch bun.outOfMemory(); + log("IPC call continueSend() from version packet", .{}); + this.continueSend(global, .new_message_appended); + } + if (Environment.allow_assert) this.has_written_version = 1; + } + pub fn serializeAndSend(self: *SendQueue, global: *JSGlobalObject, value: JSValue, is_internal: IsInternal, callback: JSC.JSValue, handle: ?Handle) SerializeAndSendResult { + log("SendQueue#serializeAndSend", .{}); + const indicate_backoff = self.waiting_for_ack != null and self.queue.items.len > 0; + const msg = self.startMessage(global, callback, handle); + const start_offset = msg.data.list.items.len; + + const payload_length = serialize(self.mode, &msg.data, global, value, is_internal) catch return .failure; + bun.assert(msg.data.list.items.len == start_offset + payload_length); + // log("enqueueing ipc message: '{'}'", .{std.zig.fmtEscapes(msg.data.list.items[start_offset..])}); + + log("IPC call continueSend() from serializeAndSend", .{}); + self.continueSend(global, .new_message_appended); + + if (indicate_backoff) return .backoff; + return .success; + } + fn debugLogMessageQueue(this: *SendQueue) void { + if (!Environment.isDebug) return; + log("IPC message queue ({d} items)", .{this.queue.items.len}); + for (this.queue.items) |item| { + if (item.data.list.items.len > 100) { + log(" {d}|{d}", .{ item.data.cursor, item.data.list.items.len - item.data.cursor }); + } else { + log(" '{'}'|'{'}'", .{ std.zig.fmtEscapes(item.data.list.items[0..item.data.cursor]), std.zig.fmtEscapes(item.data.list.items[item.data.cursor..]) }); + } + } + } + + fn getSocket(this: *SendQueue) ?SocketType { + return switch (this.socket) { + .open => |s| s, + else => return null, + }; + } + + /// starts a write request. on posix, this always calls _onWriteComplete immediately. on windows, it may + /// call _onWriteComplete later. + fn _write(this: *SendQueue, data: []const u8, fd: ?bun.FileDescriptor) void { + log("SendQueue#_write len {d}", .{data.len}); + const socket = this.getSocket() orelse { + this._onWriteComplete(-1); + return; + }; + return switch (Environment.isWindows) { + true => { + if (fd) |_| { + // TODO: send fd on windows + } + const pipe: *uv.Pipe = socket; + const write_len = @min(data.len, std.math.maxInt(i32)); + + // create write request + const write_req_slice = bun.default_allocator.dupe(u8, data[0..write_len]) catch bun.outOfMemory(); + const write_req = bun.new(WindowsWrite, .{ + .owner = this, + .write_slice = write_req_slice, + .write_req = std.mem.zeroes(uv.uv_write_t), + .write_buffer = uv.uv_buf_t.init(write_req_slice), + }); + bun.assert(this.windows.windows_write == null); + this.windows.windows_write = write_req; + + pipe.ref(); // ref on write + if (this.windows.windows_write.?.write_req.write(pipe.asStream(), &this.windows.windows_write.?.write_buffer, write_req, &_windowsOnWriteComplete).asErr()) |err| { + _windowsOnWriteComplete(write_req, @enumFromInt(-@as(c_int, err.errno))); + } + // write request is queued. it will call _onWriteComplete when it completes. + }, + false => { + if (fd) |fd_unwrapped| { + this._onWriteComplete(socket.writeFd(data, fd_unwrapped)); + } else { + this._onWriteComplete(socket.write(data, false)); + } + }, + }; + } + fn _windowsOnWriteComplete(write_req: *WindowsWrite, status: uv.ReturnCode) void { + log("SendQueue#_windowsOnWriteComplete", .{}); + const write_len = write_req.write_slice.len; + const this = blk: { + defer write_req.destroy(); + break :blk write_req.owner orelse return; // orelse case if disconnected before the write completes + }; + + const vm = JSC.VirtualMachine.get(); + vm.eventLoop().enter(); + defer vm.eventLoop().exit(); + + this.windows.windows_write = null; + if (this.getSocket()) |socket| socket.unref(); // write complete; unref + if (status.toError(.write)) |_| { + this._onWriteComplete(-1); + } else { + this._onWriteComplete(@intCast(write_len)); + } + + if (this.windows.try_close_after_write) { + this.closeSocket(.normal, .user); + } + } + fn getGlobalThis(this: *SendQueue) *JSC.JSGlobalObject { + return switch (this.owner) { + inline else => |owner| owner.globalThis, + }; + } fn onServerPipeClose(this: *uv.Pipe) callconv(.C) void { // safely free the pipes bun.default_allocator.destroy(this); } - fn detach(this: *NamedPipeIPCData) void { - log("NamedPipeIPCData#detach: is_server {}", .{this.is_server}); - const source = this.writer.source.?; - // unref because we are closing the pipe - source.pipe.unref(); - this.writer.source = null; - - if (this.is_server) { - source.pipe.data = source.pipe; - source.pipe.close(onServerPipeClose); - this.onPipeClose(); - return; - } - // server will be destroyed by the process that created it - defer bun.default_allocator.destroy(source.pipe); - this.writer.source = null; - this.onPipeClose(); - } - - fn onWrite(this: *NamedPipeIPCData, amount: usize, status: bun.io.WriteStatus) void { - log("onWrite {d} {}", .{ amount, status }); - - switch (status) { - .pending => {}, - .drained => { - // unref after sending all data - this.writer.source.?.pipe.unref(); - }, - .end_of_file => { - this.detach(); - }, - } - } - - fn onError(this: *NamedPipeIPCData, err: bun.sys.Error) void { - log("Failed to write outgoing data {}", .{err}); - this.detach(); - } - - fn onPipeClose(this: *NamedPipeIPCData) void { - log("onPipeClose", .{}); - if (this.onClose) |handler| { - this.onClose = null; - handler.callback(handler.context); - // deinit dont free the instance of IPCData we should call it before the onClose callback actually frees it - this.deinit(); - } - } - - pub fn writeVersionPacket(this: *NamedPipeIPCData, _: *JSC.JSGlobalObject) void { - if (Environment.allow_assert) { - bun.assert(this.has_written_version == 0); - } - const bytes = getVersionPacket(this.mode); - if (bytes.len > 0) { - if (this.disconnected) { - // enqueue to be sent after connecting - this.writer.outgoing.write(bytes) catch bun.outOfMemory(); - } else { - _ = this.writer.write(bytes); - } - } - if (Environment.allow_assert) { - this.has_written_version = 1; - } - } - - pub fn serializeAndSend(this: *NamedPipeIPCData, global: *JSGlobalObject, value: JSValue) bool { - if (Environment.allow_assert) { - bun.assert(this.has_written_version == 1); - } - if (this.disconnected) { - return false; - } - // ref because we have pending data - this.writer.source.?.pipe.ref(); - const start_offset = this.writer.outgoing.list.items.len; - - const payload_length: usize = serialize(this, &this.writer.outgoing, global, value) catch return false; - - bun.assert(this.writer.outgoing.list.items.len == start_offset + payload_length); - - if (start_offset == 0) { - bun.assert(this.writer.outgoing.cursor == 0); - _ = this.writer.flush(); - } - - return true; - } - - pub fn serializeAndSendInternal(this: *NamedPipeIPCData, global: *JSGlobalObject, value: JSValue) bool { - if (Environment.allow_assert) { - bun.assert(this.has_written_version == 1); - } - if (this.disconnected) { - return false; - } - // ref because we have pending data - this.writer.source.?.pipe.ref(); - const start_offset = this.writer.outgoing.list.items.len; - - const payload_length: usize = serializeInternal(this, &this.writer.outgoing, global, value) catch return false; - - bun.assert(this.writer.outgoing.list.items.len == start_offset + payload_length); - - if (start_offset == 0) { - bun.assert(this.writer.outgoing.cursor == 0); - _ = this.writer.flush(); - } - - return true; - } - - pub fn close(this: *NamedPipeIPCData, nextTick: bool) void { - log("NamedPipeIPCData#close", .{}); - if (this.disconnected) return; - this.disconnected = true; - if (nextTick) { - JSC.VirtualMachine.get().enqueueTask(JSC.ManagedTask.New(NamedPipeIPCData, closeTask).init(this)); - } else { - this.closeTask(); - } - } - - pub fn closeTask(this: *NamedPipeIPCData) void { - log("NamedPipeIPCData#closeTask is_server {}", .{this.is_server}); - if (this.disconnected) { - _ = this.writer.flush(); - this.writer.end(); - if (this.writer.getStream()) |stream| { - stream.readStop(); - } - if (!this.writer.hasPendingData()) { - this.detach(); - } - } - } - - pub fn configureServer(this: *NamedPipeIPCData, comptime Context: type, instance: *Context, ipc_pipe: *uv.Pipe) JSC.Maybe(void) { + pub fn windowsConfigureServer(this: *SendQueue, ipc_pipe: *uv.Pipe) JSC.Maybe(void) { log("configureServer", .{}); - ipc_pipe.data = @ptrCast(instance); - this.onClose = .{ - .callback = @ptrCast(&NewNamedPipeIPCHandler(Context).onClose), - .context = @ptrCast(instance), - }; + ipc_pipe.data = this; ipc_pipe.unref(); - this.is_server = true; - this.writer.setParent(this); - this.writer.owns_fd = false; - const startPipeResult = this.writer.startWithPipe(ipc_pipe); - if (startPipeResult == .err) { - this.close(false); - return startPipeResult; - } + this.socket = .{ .open = ipc_pipe }; + this.windows.is_server = true; + const pipe: *uv.Pipe = this.socket.open; + pipe.data = this; - const stream = this.writer.getStream() orelse { - this.close(false); - return JSC.Maybe(void).errno(bun.sys.E.PIPE, .pipe); - }; + const stream: *uv.uv_stream_t = pipe.asStream(); - const readStartResult = stream.readStart(instance, NewNamedPipeIPCHandler(Context).onReadAlloc, NewNamedPipeIPCHandler(Context).onReadError, NewNamedPipeIPCHandler(Context).onRead); + const readStartResult = stream.readStart(this, IPCHandlers.WindowsNamedPipe.onReadAlloc, IPCHandlers.WindowsNamedPipe.onReadError, IPCHandlers.WindowsNamedPipe.onRead); if (readStartResult == .err) { - this.close(false); + this.closeSocket(.failure, .user); return readStartResult; } return .{ .result = {} }; } - pub fn configureClient(this: *NamedPipeIPCData, comptime Context: type, instance: *Context, pipe_fd: bun.FileDescriptor) !void { + pub fn windowsConfigureClient(this: *SendQueue, pipe_fd: bun.FileDescriptor) !void { log("configureClient", .{}); const ipc_pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory(); ipc_pipe.init(uv.Loop.get(), true).unwrap() catch |err| { @@ -655,41 +914,307 @@ const NamedPipeIPCData = struct { return err; }; ipc_pipe.unref(); - this.writer.owns_fd = false; - this.writer.setParent(this); - this.writer.startWithPipe(ipc_pipe).unwrap() catch |err| { - this.close(false); + this.socket = .{ .open = ipc_pipe }; + this.windows.is_server = false; + + const stream = ipc_pipe.asStream(); + + stream.readStart(this, IPCHandlers.WindowsNamedPipe.onReadAlloc, IPCHandlers.WindowsNamedPipe.onReadError, IPCHandlers.WindowsNamedPipe.onRead).unwrap() catch |err| { + this.closeSocket(.failure, .user); return err; }; - this.connect_req.data = @ptrCast(instance); - this.onClose = .{ - .callback = @ptrCast(&NewNamedPipeIPCHandler(Context).onClose), - .context = @ptrCast(instance), - }; - - const stream = this.writer.getStream() orelse { - this.close(false); - return error.FailedToConnectIPC; - }; - - stream.readStart(instance, NewNamedPipeIPCHandler(Context).onReadAlloc, NewNamedPipeIPCHandler(Context).onReadError, NewNamedPipeIPCHandler(Context).onRead).unwrap() catch |err| { - this.close(false); - return err; - }; - } - - fn deinit(this: *NamedPipeIPCData) void { - log("deinit", .{}); - this.writer.deinit(); - this.incoming.deinitWithAllocator(bun.default_allocator); } }; +const MAX_HANDLE_RETRANSMISSIONS = 3; -pub const IPCData = if (Environment.isWindows) NamedPipeIPCData else SocketIPCData; +fn emitProcessErrorEvent(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const ex = callframe.argumentsAsArray(1)[0]; + JSC.VirtualMachine.Process__emitErrorEvent(globalThis, ex); + return .undefined; +} +const FromEnum = enum { subprocess_exited, subprocess, process }; +fn doSendErr(globalObject: *JSC.JSGlobalObject, callback: JSC.JSValue, ex: JSC.JSValue, from: FromEnum) bun.JSError!JSC.JSValue { + if (callback.isCallable()) { + callback.callNextTick(globalObject, .{ex}); + return .false; + } + if (from == .process) { + const target = JSC.JSFunction.create(globalObject, bun.String.empty, emitProcessErrorEvent, 1, .{}); + target.callNextTick(globalObject, .{ex}); + return .false; + } + // Bun.spawn().send() should throw an error (unless callback is passed) + return globalObject.throwValue(ex); +} +pub fn doSend(ipc: ?*SendQueue, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame, from: FromEnum) bun.JSError!JSValue { + var message, var handle, var options_, var callback = callFrame.argumentsAsArray(4); + + if (handle.isCallable()) { + callback = handle; + handle = .undefined; + options_ = .undefined; + } else if (options_.isCallable()) { + callback = options_; + options_ = .undefined; + } else if (!options_.isUndefined()) { + try globalObject.validateObject("options", options_, .{}); + } + + const connected = ipc != null and ipc.?.isConnected(); + if (!connected) { + const ex = globalObject.ERR(.IPC_CHANNEL_CLOSED, "{s}", .{@as([]const u8, switch (from) { + .process => "process.send() can only be used if the IPC channel is open.", + .subprocess => "Subprocess.send() can only be used if an IPC channel is open.", + .subprocess_exited => "Subprocess.send() cannot be used after the process has exited.", + })}).toJS(); + return doSendErr(globalObject, callback, ex, from); + } + + const ipc_data = ipc.?; + + if (message.isUndefined()) { + return globalObject.throwMissingArgumentsValue(&.{"message"}); + } + if (!message.isString() and !message.isObject() and !message.isNumber() and !message.isBoolean() and !message.isNull()) { + return globalObject.throwInvalidArgumentTypeValueOneOf("message", "string, object, number, or boolean", message); + } + + if (!handle.isUndefinedOrNull()) { + const serialized_array: JSC.JSValue = try ipcSerialize(globalObject, message, handle); + if (serialized_array.isUndefinedOrNull()) { + handle = .undefined; + } else { + const serialized_handle = serialized_array.getIndex(globalObject, 0); + const serialized_message = serialized_array.getIndex(globalObject, 1); + handle = serialized_handle; + message = serialized_message; + } + } + + var zig_handle: ?Handle = null; + if (!handle.isUndefinedOrNull()) { + if (bun.JSC.API.Listener.fromJS(handle)) |listener| { + log("got listener", .{}); + switch (listener.listener) { + .uws => |socket_uws| { + // may need to handle ssl case + const fd = socket_uws.getSocket().getFd(); + zig_handle = .init(fd, handle); + }, + .namedPipe => |namedPipe| { + _ = namedPipe; + }, + .none => {}, + } + } else { + // + } + } + + const status = ipc_data.serializeAndSend(globalObject, message, .external, callback, zig_handle); + + if (status == .failure) { + const ex = globalObject.createTypeErrorInstance("process.send() failed", .{}); + ex.put(globalObject, JSC.ZigString.static("syscall"), bun.String.static("write").toJS(globalObject)); + return doSendErr(globalObject, callback, ex, from); + } + + // in the success or backoff case, serializeAndSend will handle calling the callback + return if (status == .success) .true else .false; +} + +pub fn emitHandleIPCMessage(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const target, const message, const handle = callframe.argumentsAsArray(3); + if (target.isNull()) { + const ipc = globalThis.bunVM().getIPCInstance() orelse return .undefined; + ipc.handleIPCMessage(.{ .data = message }, handle); + } else { + if (!target.isCell()) return .undefined; + const subprocess = bun.JSC.Subprocess.fromJSDirect(target) orelse return .undefined; + subprocess.handleIPCMessage(.{ .data = message }, handle); + } + return .undefined; +} + +const IPCCommand = union(enum) { + handle: JSC.JSValue, + ack, + nack, +}; + +fn handleIPCMessage(send_queue: *SendQueue, message: DecodedIPCMessage, globalThis: *JSC.JSGlobalObject) void { + if (Environment.isDebug) { + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + defer formatter.deinit(); + switch (message) { + .version => |version| log("received ipc message: version: {}", .{version}), + .data => |jsvalue| log("received ipc message: {}", .{jsvalue.toFmt(&formatter)}), + .internal => |jsvalue| log("received ipc message: internal: {}", .{jsvalue.toFmt(&formatter)}), + } + } + var internal_command: ?IPCCommand = null; + if (message == .data) handle_message: { + const msg_data = message.data; + if (msg_data.isObject()) { + const cmd = msg_data.fastGet(globalThis, .cmd) orelse { + if (globalThis.hasException()) _ = globalThis.takeException(bun.JSError.JSError); + break :handle_message; + }; + if (cmd.isString()) { + if (!cmd.isCell()) break :handle_message; + const cmd_str = bun.String.fromJS(cmd, globalThis) catch |e| { + _ = globalThis.takeException(e); + break :handle_message; + }; + if (cmd_str.eqlComptime("NODE_HANDLE")) { + internal_command = .{ .handle = msg_data }; + } else if (cmd_str.eqlComptime("NODE_HANDLE_ACK")) { + internal_command = .ack; + } else if (cmd_str.eqlComptime("NODE_HANDLE_NACK")) { + internal_command = .nack; + } + } + } + } + + if (internal_command) |icmd| { + switch (icmd) { + .handle => |msg_data| { + // Handle NODE_HANDLE message + const ack = send_queue.incoming_fd != null; + + const packet = if (ack) getAckPacket(send_queue.mode) else getNackPacket(send_queue.mode); + var handle = SendHandle{ .data = .{}, .handle = null, .callbacks = .ack_nack }; + handle.data.write(packet) catch bun.outOfMemory(); + + // Insert at appropriate position in send queue + send_queue.insertMessage(handle); + + // Send if needed + log("IPC call continueSend() from handleIPCMessage", .{}); + send_queue.continueSend(globalThis, .new_message_appended); + + if (!ack) return; + + // Get file descriptor and clear it + const fd = send_queue.incoming_fd.?; + send_queue.incoming_fd = null; + + const target: bun.JSC.JSValue = switch (send_queue.owner) { + .subprocess => |subprocess| subprocess.this_jsvalue, + .virtual_machine => bun.JSC.JSValue.null, + }; + + const vm = globalThis.bunVM(); + vm.eventLoop().enter(); + defer vm.eventLoop().exit(); + _ = ipcParse(globalThis, target, msg_data, fd.toJS(globalThis)) catch |e| { + // ack written already, that's okay. + globalThis.reportActiveExceptionAsUnhandled(e); + return; + }; + + // ipc_parse will call the callback which calls handleIPCMessage() + // we have sent the ack already so the next message could arrive at any time. maybe even before + // parseHandle calls emit(). however, node does this too and its messages don't end up out of order. + // so hopefully ours won't either. + return; + }, + .ack => { + send_queue.onAckNack(globalThis, .ack); + return; + }, + .nack => { + send_queue.onAckNack(globalThis, .nack); + return; + }, + } + } else { + switch (send_queue.owner) { + inline else => |owner| { + owner.handleIPCMessage(message, .undefined); + }, + } + } +} + +fn onData2(send_queue: *SendQueue, all_data: []const u8) void { + var data = all_data; + // log("onData '{'}'", .{std.zig.fmtEscapes(data)}); + + // In the VirtualMachine case, `globalThis` is an optional, in case + // the vm is freed before the socket closes. + const globalThis = send_queue.getGlobalThis(); + + // Decode the message with just the temporary buffer, and if that + // fails (not enough bytes) then we allocate to .ipc_buffer + if (send_queue.incoming.len == 0) { + while (true) { + const result = decodeIPCMessage(send_queue.mode, data, globalThis) catch |e| switch (e) { + error.NotEnoughBytes => { + _ = send_queue.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); + log("hit NotEnoughBytes", .{}); + return; + }, + error.InvalidFormat => { + send_queue.closeSocket(.failure, .user); + return; + }, + error.OutOfMemory => { + Output.printErrorln("IPC message is too long.", .{}); + send_queue.closeSocket(.failure, .user); + return; + }, + }; + + handleIPCMessage(send_queue, result.message, globalThis); + + if (result.bytes_consumed < data.len) { + data = data[result.bytes_consumed..]; + } else { + return; + } + } + } + + _ = send_queue.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); + + var slice = send_queue.incoming.slice(); + while (true) { + const result = decodeIPCMessage(send_queue.mode, slice, globalThis) catch |e| switch (e) { + error.NotEnoughBytes => { + // copy the remaining bytes to the start of the buffer + bun.copy(u8, send_queue.incoming.ptr[0..slice.len], slice); + send_queue.incoming.len = @truncate(slice.len); + log("hit NotEnoughBytes2", .{}); + return; + }, + error.InvalidFormat => { + send_queue.closeSocket(.failure, .user); + return; + }, + error.OutOfMemory => { + Output.printErrorln("IPC message is too long.", .{}); + send_queue.closeSocket(.failure, .user); + return; + }, + }; + + handleIPCMessage(send_queue, result.message, globalThis); + + if (result.bytes_consumed < slice.len) { + slice = slice[result.bytes_consumed..]; + } else { + // clear the buffer + send_queue.incoming.len = 0; + return; + } + } +} /// Used on POSIX -fn NewSocketIPCHandler(comptime Context: type) type { - return struct { +pub const IPCHandlers = struct { + pub const PosixSocket = struct { pub fn onOpen( _: *anyopaque, _: Socket, @@ -705,272 +1230,168 @@ fn NewSocketIPCHandler(comptime Context: type) type { } pub fn onClose( - this: *Context, + send_queue: *SendQueue, _: Socket, _: c_int, _: ?*anyopaque, ) void { - log("onClose", .{}); - const ipc = this.ipc() orelse return; - // unref if needed - ipc.keep_alive.unref((this.getGlobalThis() orelse return).bunVM()); - // Note: uSockets has already freed the underlying socket, so calling Socket.close() can segfault + // uSockets has already freed the underlying socket log("NewSocketIPCHandler#onClose\n", .{}); - - if (ipc.close_next_tick) |close_next_tick_task| { - const managed: *bun.JSC.ManagedTask = close_next_tick_task.as(bun.JSC.ManagedTask); - managed.cancel(); - ipc.close_next_tick = null; - } - // after onClose(), socketIPCData.close should never be called again because socketIPCData may be freed. just in case, set disconnected to true. - ipc.disconnected = true; - - this.handleIPCClose(); + send_queue._socketClosed(); } pub fn onData( - this: *Context, - socket: Socket, + send_queue: *SendQueue, + _: Socket, all_data: []const u8, ) void { - var data = all_data; - const ipc = this.ipc() orelse return; - log("onData {}", .{std.fmt.fmtSliceHexLower(data)}); + const globalThis = send_queue.getGlobalThis(); + const loop = globalThis.bunVM().eventLoop(); + loop.enter(); + defer loop.exit(); + onData2(send_queue, all_data); + } - // In the VirtualMachine case, `globalThis` is an optional, in case - // the vm is freed before the socket closes. - const globalThis = switch (@typeInfo(@TypeOf(this.globalThis))) { - .pointer => this.globalThis, - .optional => brk: { - if (this.globalThis) |global| { - break :brk global; - } - this.handleIPCClose(); - socket.close(.failure); - return; - }, - else => @panic("Unexpected globalThis type: " ++ @typeName(@TypeOf(this.globalThis))), - }; - - // Decode the message with just the temporary buffer, and if that - // fails (not enough bytes) then we allocate to .ipc_buffer - if (ipc.incoming.len == 0) { - while (true) { - const result = decodeIPCMessage(ipc.mode, data, globalThis) catch |e| switch (e) { - error.NotEnoughBytes => { - _ = ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); - log("hit NotEnoughBytes", .{}); - return; - }, - error.InvalidFormat => { - socket.close(.failure); - return; - }, - error.OutOfMemory => { - Output.printErrorln("IPC message is too long.", .{}); - this.handleIPCClose(); - socket.close(.failure); - return; - }, - }; - - this.handleIPCMessage(result.message); - - if (result.bytes_consumed < data.len) { - data = data[result.bytes_consumed..]; - } else { - return; - } - } - } - - _ = ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); - - var slice = ipc.incoming.slice(); - while (true) { - const result = decodeIPCMessage(ipc.mode, slice, globalThis) catch |e| switch (e) { - error.NotEnoughBytes => { - // copy the remaining bytes to the start of the buffer - bun.copy(u8, ipc.incoming.ptr[0..slice.len], slice); - ipc.incoming.len = @truncate(slice.len); - log("hit NotEnoughBytes2", .{}); - return; - }, - error.InvalidFormat => { - socket.close(.failure); - return; - }, - error.OutOfMemory => { - Output.printErrorln("IPC message is too long.", .{}); - this.handleIPCClose(); - socket.close(.failure); - return; - }, - }; - - this.handleIPCMessage(result.message); - - if (result.bytes_consumed < slice.len) { - slice = slice[result.bytes_consumed..]; - } else { - // clear the buffer - ipc.incoming.len = 0; - return; - } + pub fn onFd( + send_queue: *SendQueue, + _: Socket, + fd: c_int, + ) void { + log("onFd: {d}", .{fd}); + if (send_queue.incoming_fd != null) { + log("onFd: incoming_fd already set; overwriting", .{}); } + send_queue.incoming_fd = bun.FD.fromNative(fd); } pub fn onWritable( - context: *Context, - socket: Socket, + send_queue: *SendQueue, + _: Socket, ) void { log("onWritable", .{}); - const ipc = context.ipc() orelse return; - const to_write = ipc.outgoing.slice(); - if (to_write.len == 0) { - ipc.outgoing.reset(); - // done sending message; unref event loop - ipc.keep_alive.unref((context.getGlobalThis() orelse return).bunVM()); - return; - } - const n = socket.write(to_write, false); - if (n == to_write.len) { - ipc.outgoing.reset(); - // almost done sending message; unref event loop - ipc.keep_alive.unref((context.getGlobalThis() orelse return).bunVM()); - } else if (n > 0) { - ipc.outgoing.cursor += @intCast(n); - } + + const globalThis = send_queue.getGlobalThis(); + const loop = globalThis.bunVM().eventLoop(); + loop.enter(); + defer loop.exit(); + log("IPC call continueSend() from onWritable", .{}); + send_queue.continueSend(globalThis, .on_writable); } pub fn onTimeout( - context: *Context, + _: *SendQueue, _: Socket, ) void { log("onTimeout", .{}); - const ipc = context.ipc() orelse return; // unref if needed - ipc.keep_alive.unref((context.getGlobalThis() orelse return).bunVM()); } pub fn onLongTimeout( - context: *Context, + _: *SendQueue, _: Socket, ) void { log("onLongTimeout", .{}); - const ipc = context.ipc() orelse return; - // unref if needed - ipc.keep_alive.unref((context.getGlobalThis() orelse return).bunVM()); + // onLongTimeout } pub fn onConnectError( - _: *anyopaque, + send_queue: *SendQueue, _: Socket, _: c_int, ) void { log("onConnectError", .{}); // context has not been initialized + send_queue.closeSocket(.failure, .user); } pub fn onEnd( - _: *Context, - s: Socket, + send_queue: *SendQueue, + _: Socket, ) void { log("onEnd", .{}); - s.close(.failure); + send_queue.closeSocket(.failure, .user); } }; -} -/// Used on Windows -fn NewNamedPipeIPCHandler(comptime Context: type) type { - return struct { - fn onReadAlloc(this: *Context, suggested_size: usize) []u8 { - const ipc = this.ipc() orelse return ""; - var available = ipc.incoming.available(); + pub const WindowsNamedPipe = struct { + fn onReadAlloc(send_queue: *SendQueue, suggested_size: usize) []u8 { + var available = send_queue.incoming.available(); if (available.len < suggested_size) { - ipc.incoming.ensureUnusedCapacity(bun.default_allocator, suggested_size) catch bun.outOfMemory(); - available = ipc.incoming.available(); + send_queue.incoming.ensureUnusedCapacity(bun.default_allocator, suggested_size) catch bun.outOfMemory(); + available = send_queue.incoming.available(); } log("NewNamedPipeIPCHandler#onReadAlloc {d}", .{suggested_size}); return available.ptr[0..suggested_size]; } - fn onReadError(this: *Context, err: bun.sys.E) void { + fn onReadError(send_queue: *SendQueue, err: bun.sys.E) void { log("NewNamedPipeIPCHandler#onReadError {}", .{err}); - if (this.ipc()) |ipc_data| { - ipc_data.close(true); - } + send_queue.closeSocketNextTick(true); } - fn onRead(this: *Context, buffer: []const u8) void { - const ipc = this.ipc() orelse return; - + fn onRead(send_queue: *SendQueue, buffer: []const u8) void { log("NewNamedPipeIPCHandler#onRead {d}", .{buffer.len}); - ipc.incoming.len += @as(u32, @truncate(buffer.len)); - var slice = ipc.incoming.slice(); + const globalThis = send_queue.getGlobalThis(); + const loop = globalThis.bunVM().eventLoop(); + loop.enter(); + defer loop.exit(); + send_queue.incoming.len += @as(u32, @truncate(buffer.len)); + var slice = send_queue.incoming.slice(); - bun.assert(ipc.incoming.len <= ipc.incoming.cap); - bun.assert(bun.isSliceInBuffer(buffer, ipc.incoming.allocatedSlice())); + bun.assert(send_queue.incoming.len <= send_queue.incoming.cap); + bun.assert(bun.isSliceInBuffer(buffer, send_queue.incoming.allocatedSlice())); - const globalThis = switch (@typeInfo(@TypeOf(this.globalThis))) { - .pointer => this.globalThis, - .optional => brk: { - if (this.globalThis) |global| { - break :brk global; - } - ipc.close(true); - return; - }, - else => @panic("Unexpected globalThis type: " ++ @typeName(@TypeOf(this.globalThis))), - }; while (true) { - const result = decodeIPCMessage(ipc.mode, slice, globalThis) catch |e| switch (e) { + const result = decodeIPCMessage(send_queue.mode, slice, globalThis) catch |e| switch (e) { error.NotEnoughBytes => { // copy the remaining bytes to the start of the buffer - bun.copy(u8, ipc.incoming.ptr[0..slice.len], slice); - ipc.incoming.len = @truncate(slice.len); + bun.copy(u8, send_queue.incoming.ptr[0..slice.len], slice); + send_queue.incoming.len = @truncate(slice.len); log("hit NotEnoughBytes3", .{}); return; }, error.InvalidFormat => { - ipc.close(false); + send_queue.closeSocket(.failure, .user); return; }, error.OutOfMemory => { Output.printErrorln("IPC message is too long.", .{}); - ipc.close(false); + send_queue.closeSocket(.failure, .user); return; }, }; - this.handleIPCMessage(result.message); + handleIPCMessage(send_queue, result.message, globalThis); if (result.bytes_consumed < slice.len) { slice = slice[result.bytes_consumed..]; } else { // clear the buffer - ipc.incoming.len = 0; + send_queue.incoming.len = 0; return; } } } - pub fn onClose(this: *Context) void { + pub fn onClose(send_queue: *SendQueue) void { log("NewNamedPipeIPCHandler#onClose\n", .{}); - this.handleIPCClose(); + send_queue._onAfterIPCClosed(); } }; +}; + +extern "C" fn IPCSerialize(globalObject: *JSC.JSGlobalObject, message: JSC.JSValue, handle: JSC.JSValue) JSC.JSValue; + +pub fn ipcSerialize(globalObject: *JSC.JSGlobalObject, message: JSC.JSValue, handle: JSC.JSValue) bun.JSError!JSC.JSValue { + const result = IPCSerialize(globalObject, message, handle); + if (result == .zero) return error.JSError; + return result; } -/// This type is shared between VirtualMachine and Subprocess for their respective IPC handlers -/// -/// `Context` must be a struct that implements this interface: -/// struct { -/// globalThis: ?*JSGlobalObject, -/// -/// fn ipc(*Context) ?*IPCData, -/// fn handleIPCMessage(*Context, DecodedIPCMessage) void -/// fn handleIPCClose(*Context) void -/// } -pub const NewIPCHandler = if (Environment.isWindows) NewNamedPipeIPCHandler else NewSocketIPCHandler; +extern "C" fn IPCParse(globalObject: *JSC.JSGlobalObject, target: JSC.JSValue, serialized: JSC.JSValue, fd: JSC.JSValue) JSC.JSValue; + +pub fn ipcParse(globalObject: *JSC.JSGlobalObject, target: JSC.JSValue, serialized: JSC.JSValue, fd: JSC.JSValue) bun.JSError!JSC.JSValue { + const result = IPCParse(globalObject, target, serialized, fd); + if (result == .zero) return error.JSError; + return result; +} diff --git a/src/bun.js/node/node_cluster_binding.zig b/src/bun.js/node/node_cluster_binding.zig index 27f259661f..4efea80c45 100644 --- a/src/bun.js/node/node_cluster_binding.zig +++ b/src/bun.js/node/node_cluster_binding.zig @@ -65,9 +65,9 @@ pub fn sendHelperChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFram } }; - const good = ipc_instance.data.serializeAndSendInternal(globalThis, message); + const good = ipc_instance.data.serializeAndSend(globalThis, message, .internal, .null, null); - if (!good) { + if (good == .failure) { const ex = globalThis.createTypeErrorInstance("sendInternal() failed", .{}); ex.put(globalThis, ZigString.static("syscall"), bun.String.static("write").toJS(globalThis)); const fnvalue = JSC.JSFunction.create(globalThis, "", S.impl, 1, .{}); @@ -75,7 +75,7 @@ pub fn sendHelperChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFram return .false; } - return .true; + return if (good == .success) .true else .false; } pub fn onInternalMessageChild(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { @@ -210,10 +210,8 @@ pub fn sendHelperPrimary(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFr if (Environment.isDebug) log("primary: {}", .{message.toFmt(&formatter)}); _ = handle; - const success = ipc_data.serializeAndSendInternal(globalThis, message); - if (!success) return .false; - - return .true; + const success = ipc_data.serializeAndSend(globalThis, message, .internal, .null, null); + return if (success == .success) .true else .false; } pub fn onInternalMessagePrimary(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 1b5e959974..580fbf21ba 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -438,9 +438,8 @@ pub fn spawnIPCContext(rare: *RareData, vm: *JSC.VirtualMachine) *uws.SocketCont return ctx; } - const opts: uws.us_socket_context_options_t = .{}; - const ctx = uws.us_create_socket_context(0, vm.event_loop_handle.?, @sizeOf(usize), opts).?; - IPC.Socket.configure(ctx, true, *JSC.Subprocess, JSC.Subprocess.IPCHandler); + const ctx = uws.us_create_bun_nossl_socket_context(vm.event_loop_handle.?, @sizeOf(usize)).?; + IPC.Socket.configure(ctx, true, *IPC.SendQueue, IPC.IPCHandlers.PosixSocket); rare.spawn_ipc_usockets_context = ctx; return ctx; } diff --git a/src/bun.js/virtual_machine_exports.zig b/src/bun.js/virtual_machine_exports.zig index 14c0da9e4f..8ec6ea59e7 100644 --- a/src/bun.js/virtual_machine_exports.zig +++ b/src/bun.js/virtual_machine_exports.zig @@ -25,8 +25,20 @@ export fn Bun__readOriginTimerStart(vm: *JSC.VirtualMachine) f64 { return @as(f64, @floatCast((@as(f64, @floatFromInt(vm.origin_timestamp)) + JSC.VirtualMachine.origin_relative_epoch) / 1_000_000.0)); } +pub export fn Bun__GlobalObject__connectedIPC(global: *JSGlobalObject) bool { + if (global.bunVM().ipc) |ipc| { + if (ipc == .initialized) { + return ipc.initialized.data.isConnected(); + } + return true; + } + return false; +} pub export fn Bun__GlobalObject__hasIPC(global: *JSGlobalObject) bool { - return global.bunVM().ipc != null; + if (global.bunVM().ipc != null) { + return true; + } + return false; } export fn Bun__VirtualMachine__exitDuringUncaughtException(this: *JSC.VirtualMachine) void { @@ -39,65 +51,9 @@ comptime { } pub fn Bun__Process__send_(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { JSC.markBinding(@src()); - var message, var handle, var options_, var callback = callFrame.argumentsAsArray(4); - - if (handle.isFunction()) { - callback = handle; - handle = .undefined; - options_ = .undefined; - } else if (options_.isFunction()) { - callback = options_; - options_ = .undefined; - } else if (!options_.isUndefined()) { - try globalObject.validateObject("options", options_, .{}); - } - - const S = struct { - fn impl(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - const arguments_ = callframe.arguments_old(1).slice(); - const ex = arguments_[0]; - VirtualMachine.Process__emitErrorEvent(globalThis, ex); - return .undefined; - } - }; const vm = globalObject.bunVM(); - const ipc_instance = vm.getIPCInstance() orelse { - const ex = globalObject.ERR(.IPC_CHANNEL_CLOSED, "Channel closed.", .{}).toJS(); - if (callback.isFunction()) { - callback.callNextTick(globalObject, .{ex}); - } else { - const fnvalue = JSC.JSFunction.create(globalObject, "", S.impl, 1, .{}); - fnvalue.callNextTick(globalObject, .{ex}); - } - return .false; - }; - - if (message.isUndefined()) { - return globalObject.throwMissingArgumentsValue(&.{"message"}); - } - if (!message.isString() and !message.isObject() and !message.isNumber() and !message.isBoolean() and !message.isNull()) { - return globalObject.throwInvalidArgumentTypeValue("message", "string, object, number, or boolean", message); - } - - const good = ipc_instance.data.serializeAndSend(globalObject, message); - - if (good) { - if (callback.isFunction()) { - callback.callNextTick(globalObject, .{.null}); - } - } else { - const ex = globalObject.createTypeErrorInstance("process.send() failed", .{}); - ex.put(globalObject, JSC.ZigString.static("syscall"), bun.String.static("write").toJS(globalObject)); - if (callback.isFunction()) { - callback.callNextTick(globalObject, .{ex}); - } else { - const fnvalue = JSC.JSFunction.create(globalObject, "", S.impl, 1, .{}); - fnvalue.callNextTick(globalObject, .{ex}); - } - } - - return .true; + return IPC.doSend(if (vm.getIPCInstance()) |i| &i.data else null, globalObject, callFrame, .process); } pub export fn Bun__isBunMain(globalObject: *JSGlobalObject, str: *const bun.String) bool { @@ -235,3 +191,4 @@ const VirtualMachine = JSC.VirtualMachine; const JSGlobalObject = JSC.JSGlobalObject; const JSValue = JSC.JSValue; const PluginRunner = bun.transpiler.PluginRunner; +const IPC = @import("ipc.zig"); diff --git a/src/bun.zig b/src/bun.zig index 61c400387b..761ee0bba4 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -25,6 +25,43 @@ else pub const callmod_inline: std.builtin.CallModifier = if (builtin.mode == .Debug) .auto else .always_inline; pub const callconv_inline: std.builtin.CallingConvention = if (builtin.mode == .Debug) .Unspecified else .Inline; +/// In debug builds, this will catch memory leaks. In release builds, it is mimalloc. +pub const debug_allocator: std.mem.Allocator = if (Environment.isDebug) + debug_allocator_data.allocator +else + default_allocator; +pub const debug_allocator_data = struct { + comptime { + if (!Environment.isDebug) @compileError("only available in debug"); + } + pub var backing: ?std.heap.DebugAllocator(.{}) = null; + pub const allocator: std.mem.Allocator = .{ + .ptr = undefined, + .vtable = &.{ + .alloc = &alloc, + .resize = &resize, + .remap = &remap, + .free = &free, + }, + }; + + fn alloc(_: *anyopaque, new_len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 { + return backing.?.allocator().rawAlloc(new_len, alignment, ret_addr); + } + + fn resize(_: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { + return backing.?.allocator().rawResize(memory, alignment, new_len, ret_addr); + } + + fn remap(_: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { + return backing.?.allocator().rawRemap(memory, alignment, new_len, ret_addr); + } + + fn free(_: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void { + return backing.?.allocator().rawFree(memory, alignment, ret_addr); + } +}; + pub extern "c" fn powf(x: f32, y: f32) f32; pub extern "c" fn pow(x: f64, y: f64) f64; diff --git a/src/codegen/bundle-functions.ts b/src/codegen/bundle-functions.ts index 9e55ebc53b..0ebc48a3a9 100644 --- a/src/codegen/bundle-functions.ts +++ b/src/codegen/bundle-functions.ts @@ -73,6 +73,7 @@ async function processFileSplit(filename: string): Promise<{ functions: BundledB let contents = await Bun.file(filename).text(); contents = applyGlobalReplacements(contents); + const originalContents = contents; // first approach doesnt work perfectly because we actually need to split each function declaration // and then compile those separately @@ -93,7 +94,36 @@ async function processFileSplit(filename: string): Promise<{ functions: BundledB if (!contents.length) break; const match = contents.match(consumeTopLevelContent); if (!match) { - throw new SyntaxError("Could not process input:\n" + contents.slice(0, contents.indexOf("\n"))); + const pos = originalContents.length - contents.length; + let lineNumber = 1; + let columnNumber = 1; + let lineStartPos = 0; + for (let i = 0; i < pos; i++) { + if (originalContents[i] === "\n") { + lineNumber++; + columnNumber = 1; + lineStartPos = i + 1; + } else { + columnNumber++; + } + if (i === pos) { + break; + } + } + const lineEndPos = lineStartPos + originalContents.slice(lineStartPos).indexOf("\n"); + throw new SyntaxError( + "Could not process input:\n" + + originalContents.slice(lineStartPos, lineEndPos) + + "\n" + + " ".repeat(pos - lineStartPos) + + "^" + + "\n at " + + filename + + ":" + + lineNumber + + ":" + + columnNumber, + ); } contents = contents.slice(match.index!); if (match[1] === "import") { diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 3d4cf4330a..3ab0540dbc 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -1426,6 +1426,10 @@ pub const Pipe = extern struct { pub fn setPendingInstancesCount(this: *@This(), count: i32) void { uv_pipe_pending_instances(this, count); } + + pub fn asStream(this: *@This()) *uv_stream_t { + return @ptrCast(this); + } }; const union_unnamed_416 = extern union { fd: c_int, diff --git a/src/deps/libuwsockets.cpp b/src/deps/libuwsockets.cpp index a27fe46db5..6e25017317 100644 --- a/src/deps/libuwsockets.cpp +++ b/src/deps/libuwsockets.cpp @@ -1784,6 +1784,10 @@ __attribute__((callback (corker, ctx))) us_poll_change(&s->p, s->context->loop, LIBUS_SOCKET_READABLE | LIBUS_SOCKET_WRITABLE); } + LIBUS_SOCKET_DESCRIPTOR us_socket_get_fd(us_socket_r s) { + return us_poll_fd(&s->p); + } + // Gets the remote address and port // Returns 0 if failure / unix socket uint64_t uws_res_get_remote_address_info(uws_res_r res, const char **dest, int *port, bool *is_ipv6) diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 80ba2882b2..16a13868e8 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -1555,6 +1555,13 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { }; } + pub fn writeFd(this: ThisSocket, data: []const u8, file_descriptor: bun.FileDescriptor) i32 { + return switch (this.socket) { + .upgradedDuplex, .pipe => this.write(data, false), + .connected => |socket| socket.writeFd(data, file_descriptor), + .connecting, .detached => 0, + }; + } pub fn rawWrite(this: ThisSocket, data: []const u8, msg_more: bool) i32 { return switch (this.socket) { .connected => |socket| socket.rawWrite(is_ssl, data, msg_more), @@ -1767,8 +1774,9 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { comptime This: type, this: *This, comptime socket_field_name: ?[]const u8, + is_ipc: bool, ) ?ThisSocket { - const socket_ = ThisSocket{ .socket = .{ .connected = us_socket_from_fd(ctx, @sizeOf(*anyopaque), handle.asSocketFd()) orelse return null } }; + const socket_ = ThisSocket{ .socket = .{ .connected = us_socket_from_fd(ctx, @sizeOf(*anyopaque), handle.asSocketFd(), @intFromBool(is_ipc)) orelse return null } }; if (socket_.ext(*anyopaque)) |holder| { holder.* = this; @@ -1979,6 +1987,8 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { 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, "onFd") and @typeInfo(@TypeOf(Type.onFd)) != .null) + us_socket_context_on_fd(ssl_int, ctx, SocketHandler.on_fd); 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) @@ -2051,6 +2061,14 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { ); return socket; } + pub fn on_fd(socket: *Socket, file_descriptor: c_int) callconv(.C) ?*Socket { + Fields.onFd( + getValue(socket), + ThisSocket.from(socket), + file_descriptor, + ); + return socket; + } pub fn on_writable(socket: *Socket) callconv(.C) ?*Socket { Fields.onWritable( getValue(socket), @@ -2124,6 +2142,8 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { 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, "onFd") and @typeInfo(@TypeOf(Type.onFd)) != .null) + us_socket_context_on_fd(ssl_int, ctx, SocketHandler.on_fd); 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) @@ -2247,6 +2267,9 @@ pub const SocketContext = opaque { fn data(socket: *Socket, _: [*c]u8, _: i32) callconv(.C) ?*Socket { return socket; } + fn fd(socket: *Socket, _: c_int) callconv(.C) ?*Socket { + return socket; + } fn writable(socket: *Socket) callconv(.C) ?*Socket { return socket; } @@ -2270,6 +2293,7 @@ pub const SocketContext = opaque { us_socket_context_on_open(ssl_int, ctx, DummyCallbacks.open); us_socket_context_on_close(ssl_int, ctx, DummyCallbacks.close); us_socket_context_on_data(ssl_int, ctx, DummyCallbacks.data); + us_socket_context_on_fd(ssl_int, ctx, DummyCallbacks.fd); us_socket_context_on_writable(ssl_int, ctx, DummyCallbacks.writable); us_socket_context_on_timeout(ssl_int, ctx, DummyCallbacks.timeout); us_socket_context_on_connect_error(ssl_int, ctx, DummyCallbacks.connect_error); @@ -2578,7 +2602,8 @@ pub extern fn us_socket_context_remove_server_name(ssl: i32, context: ?*SocketCo extern fn us_socket_context_on_server_name(ssl: i32, context: ?*SocketContext, cb: ?*const fn (?*SocketContext, [*c]const u8) callconv(.C) void) void; extern fn us_socket_context_get_native_handle(ssl: i32, context: ?*SocketContext) ?*anyopaque; pub extern fn us_create_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_socket_context_options_t) ?*SocketContext; -pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t, err: *create_bun_socket_error_t) ?*SocketContext; +pub extern fn us_create_bun_ssl_socket_context(loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t, err: *create_bun_socket_error_t) ?*SocketContext; +pub extern fn us_create_bun_nossl_socket_context(loop: ?*Loop, ext_size: i32) ?*SocketContext; pub extern fn us_bun_socket_context_add_server_name(ssl: i32, context: ?*SocketContext, hostname_pattern: [*c]const u8, options: us_bun_socket_context_options_t, ?*anyopaque) void; pub extern fn us_socket_context_free(ssl: i32, context: ?*SocketContext) void; pub extern fn us_socket_context_ref(ssl: i32, context: ?*SocketContext) void; @@ -2586,6 +2611,7 @@ pub extern fn us_socket_context_unref(ssl: i32, context: ?*SocketContext) void; extern fn us_socket_context_on_open(ssl: i32, context: ?*SocketContext, on_open: *const fn (*Socket, i32, [*c]u8, i32) callconv(.C) ?*Socket) void; extern fn us_socket_context_on_close(ssl: i32, context: ?*SocketContext, on_close: *const fn (*Socket, i32, ?*anyopaque) callconv(.C) ?*Socket) void; extern fn us_socket_context_on_data(ssl: i32, context: ?*SocketContext, on_data: *const fn (*Socket, [*c]u8, i32) callconv(.C) ?*Socket) void; +extern fn us_socket_context_on_fd(ssl: i32, context: ?*SocketContext, on_fd: *const fn (*Socket, c_int) callconv(.C) ?*Socket) void; extern fn us_socket_context_on_writable(ssl: i32, context: ?*SocketContext, on_writable: *const fn (*Socket) callconv(.C) ?*Socket) void; extern fn us_socket_context_on_handshake(ssl: i32, context: ?*SocketContext, on_handshake: *const fn (*Socket, i32, us_bun_verify_error_t, ?*anyopaque) callconv(.C) void, ?*anyopaque) void; @@ -3021,12 +3047,13 @@ pub const ListenSocket = opaque { us_listen_socket_close(@intFromBool(ssl), this); } pub fn getLocalAddress(this: *ListenSocket, ssl: bool, buf: []u8) ![]const u8 { - const self: *uws.Socket = @ptrCast(this); - return self.localAddress(ssl, buf); + return this.getSocket().localAddress(ssl, buf); } pub fn getLocalPort(this: *ListenSocket, ssl: bool) i32 { - const self: *uws.Socket = @ptrCast(this); - return self.localPort(ssl); + return this.getSocket().localPort(ssl); + } + pub fn getSocket(this: *ListenSocket) *uws.Socket { + return @ptrCast(this); } }; extern fn us_listen_socket_close(ssl: i32, ls: *ListenSocket) void; @@ -4298,6 +4325,7 @@ pub extern fn us_socket_from_fd( ctx: *SocketContext, ext_size: c_int, fd: LIBUS_SOCKET_DESCRIPTOR, + is_ipc: c_int, ) ?*Socket; pub fn newSocketFromPair(ctx: *SocketContext, ext_size: c_int, fds: *[2]LIBUS_SOCKET_DESCRIPTOR) ?SocketTCP { diff --git a/src/deps/uws/socket.zig b/src/deps/uws/socket.zig index 49410e1558..d2cc4e4f14 100644 --- a/src/deps/uws/socket.zig +++ b/src/deps/uws/socket.zig @@ -134,6 +134,13 @@ pub const Socket = opaque { return rc; } + pub fn writeFd(this: *Socket, data: []const u8, file_descriptor: bun.FD) i32 { + if (bun.Environment.isWindows) @compileError("TODO: implement writeFd on Windows"); + const rc = us_socket_ipc_write_fd(this, data.ptr, @intCast(data.len), file_descriptor.native()); + debug("us_socket_ipc_write_fd({d}, {d}, {d}) = {d}", .{ @intFromPtr(this), data.len, file_descriptor.native(), rc }); + return rc; + } + pub fn write2(this: *Socket, ssl: bool, first: []const u8, second: []const u8) i32 { const rc = us_socket_write2(@intFromBool(ssl), this, first.ptr, first.len, second.ptr, second.len); debug("us_socket_write2({d}, {d}, {d}) = {d}", .{ @intFromPtr(this), first.len, second.len, rc }); @@ -153,6 +160,10 @@ pub const Socket = opaque { us_socket_sendfile_needs_more(this); } + pub fn getFd(this: *Socket) bun.FD { + return .fromNative(us_socket_get_fd(this)); + } + extern fn us_socket_get_native_handle(ssl: i32, s: ?*Socket) ?*anyopaque; extern fn us_socket_local_port(ssl: i32, s: ?*Socket) i32; @@ -168,6 +179,7 @@ pub const Socket = opaque { extern fn us_socket_context(ssl: i32, s: ?*Socket) ?*SocketContext; extern fn us_socket_write(ssl: i32, s: ?*Socket, data: [*c]const u8, length: i32, msg_more: i32) i32; + extern fn us_socket_ipc_write_fd(s: ?*Socket, data: [*c]const u8, length: i32, fd: i32) i32; extern "c" fn us_socket_write2(ssl: i32, *Socket, header: ?[*]const u8, len: usize, payload: ?[*]const u8, usize) 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_flush(ssl: i32, s: ?*Socket) void; @@ -184,4 +196,9 @@ pub const Socket = opaque { extern fn us_socket_is_shut_down(ssl: i32, s: ?*Socket) i32; extern fn us_socket_sendfile_needs_more(socket: *Socket) void; + extern fn us_socket_get_fd(s: ?*Socket) LIBUS_SOCKET_DESCRIPTOR; + const LIBUS_SOCKET_DESCRIPTOR = switch (bun.Environment.isWindows) { + true => *anyopaque, + false => i32, + }; }; diff --git a/src/env.zig b/src/env.zig index 645cb4fff0..41364f1da4 100644 --- a/src/env.zig +++ b/src/env.zig @@ -18,7 +18,7 @@ pub const isMac = build_target == .native and @import("builtin").target.os.tag = pub const isBrowser = !isWasi and isWasm; pub const isWindows = @import("builtin").target.os.tag == .windows; pub const isPosix = !isWindows and !isWasm; -pub const isDebug = std.builtin.Mode.Debug == @import("builtin").mode; +pub const isDebug = @import("builtin").mode == .Debug; pub const isTest = @import("builtin").is_test; pub const isLinux = @import("builtin").target.os.tag == .linux; pub const isAarch64 = @import("builtin").target.cpu.arch.isAARCH64(); diff --git a/src/errno/windows_errno.zig b/src/errno/windows_errno.zig index be989ac9e5..106785702b 100644 --- a/src/errno/windows_errno.zig +++ b/src/errno/windows_errno.zig @@ -963,6 +963,16 @@ pub const SystemErrno = enum(u16) { if (code <= @intFromEnum(Win32Error.IO_REISSUE_AS_CACHED) or (code >= @intFromEnum(Win32Error.WSAEINTR) and code <= @intFromEnum(Win32Error.WSA_QOS_RESERVED_PETYPE))) { return init(@as(Win32Error, @enumFromInt(code))); } else { + // uv error codes + inline for (@typeInfo(SystemErrno).@"enum".fields) |field| { + if (comptime std.mem.startsWith(u8, field.name, "UV_")) { + if (comptime @hasField(SystemErrno, field.name["UV_".len..])) { + if (code == field.value) { + return @field(SystemErrno, field.name["UV_".len..]); + } + } + } + } if (comptime bun.Environment.allow_assert) bun.Output.debugWarn("Unknown error code: {any}\n", .{code}); @@ -1079,7 +1089,6 @@ pub const SystemErrno = enum(u16) { if (code < 0) return init(-code); - if (code >= max) return null; return @as(SystemErrno, @enumFromInt(code)); } }; diff --git a/src/http.zig b/src/http.zig index 3d376d4ae8..8401a55cdc 100644 --- a/src/http.zig +++ b/src/http.zig @@ -643,7 +643,7 @@ fn NewHTTPContext(comptime ssl: bool) type { } var err: uws.create_bun_socket_error_t = .none; - const socket = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts.*, &err); + const socket = uws.us_create_bun_ssl_socket_context(http_thread.loop.loop, @sizeOf(usize), opts.*, &err); if (socket == null) { return switch (err) { .load_ca_file => error.LoadCAFile, @@ -686,7 +686,7 @@ fn NewHTTPContext(comptime ssl: bool) type { .reject_unauthorized = 0, }; var err: uws.create_bun_socket_error_t = .none; - this.us_socket_context = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts, &err).?; + this.us_socket_context = uws.us_create_bun_ssl_socket_context(http_thread.loop.loop, @sizeOf(usize), opts, &err).?; this.sslCtx().setup(); } else { diff --git a/src/io/source.zig b/src/io/source.zig index ed5d3d5ba9..eddafbba0c 100644 --- a/src/io/source.zig +++ b/src/io/source.zig @@ -48,7 +48,7 @@ pub const Source = union(enum) { } pub fn toStream(this: Source) *uv.uv_stream_t { return switch (this) { - .pipe => @ptrCast(this.pipe), + .pipe => this.pipe.asStream(), .tty => @ptrCast(this.tty), .sync_file, .file => unreachable, }; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 89a2dc9191..beeb288684 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -690,6 +690,7 @@ declare function $ERR_BROTLI_INVALID_PARAM(p: number): RangeError; declare function $ERR_TLS_CERT_ALTNAME_INVALID(reason: string, host: string, cert): Error; declare function $ERR_USE_AFTER_CLOSE(name: string): Error; declare function $ERR_HTTP2_INVALID_HEADER_VALUE(value: string, name: string): TypeError; +declare function $ERR_INVALID_HANDLE_TYPE(): TypeError; declare function $ERR_INVALID_HTTP_TOKEN(name: string, value: string): TypeError; declare function $ERR_HTTP2_STATUS_INVALID(code: number): RangeError; declare function $ERR_HTTP2_INVALID_PSEUDOHEADER(name: string): TypeError; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 39f912a38b..b61eb33e02 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -71,6 +71,7 @@ using namespace JSC; macro(closed) \ macro(closedPromise) \ macro(closedPromiseCapability) \ + macro(cmd) \ macro(code) \ macro(connect) \ macro(controlledReadableStream) \ diff --git a/src/js/builtins/Ipc.ts b/src/js/builtins/Ipc.ts new file mode 100644 index 0000000000..8971d6290f --- /dev/null +++ b/src/js/builtins/Ipc.ts @@ -0,0 +1,236 @@ +// for net.Server, get ._handle + +// const handleConversion = { +// "net.Server": { +// simultaneousAccepts: true, + +// send(message, server, options) { +// return server._handle; +// }, + +// got(message, handle, emit) { +// const server = new net.Server(); +// server.listen(handle, () => { +// emit(server); +// }); +// }, +// }, + +// "net.Socket": { +// send(message, socket, options) { +// if (!socket._handle) return; + +// // If the socket was created by net.Server +// if (socket.server) { +// // The worker should keep track of the socket +// message.key = socket.server._connectionKey; + +// const firstTime = !this[kChannelHandle].sockets.send[message.key]; +// const socketList = getSocketList("send", this, message.key); + +// // The server should no longer expose a .connection property +// // and when asked to close it should query the socket status from +// // the workers +// if (firstTime) socket.server._setupWorker(socketList); + +// // Act like socket is detached +// if (!options.keepOpen) socket.server._connections--; +// } + +// const handle = socket._handle; + +// // Remove handle from socket object, it will be closed when the socket +// // will be sent +// if (!options.keepOpen) { +// handle.onread = nop; +// socket._handle = null; +// socket.setTimeout(0); + +// if (freeParser === undefined) freeParser = require("_http_common").freeParser; +// if (HTTPParser === undefined) HTTPParser = require("_http_common").HTTPParser; + +// // In case of an HTTP connection socket, release the associated +// // resources +// if (socket.parser && socket.parser instanceof HTTPParser) { +// freeParser(socket.parser, null, socket); +// if (socket._httpMessage) socket._httpMessage.detachSocket(socket); +// } +// } + +// return handle; +// }, + +// postSend(message, handle, options, callback, target) { +// // Store the handle after successfully sending it, so it can be closed +// // when the NODE_HANDLE_ACK is received. If the handle could not be sent, +// // just close it. +// if (handle && !options.keepOpen) { +// if (target) { +// // There can only be one _pendingMessage as passing handles are +// // processed one at a time: handles are stored in _handleQueue while +// // waiting for the NODE_HANDLE_ACK of the current passing handle. +// assert(!target._pendingMessage); +// target._pendingMessage = { callback, message, handle, options, retransmissions: 0 }; +// } else { +// handle.close(); +// } +// } +// // NOTE that another function will call _pendingMessage.handle.close() and set _pendingMessage to null +// }, + +// got(message, handle, emit) { +// const socket = new net.Socket({ +// handle: handle, +// readable: true, +// writable: true, +// }); + +// // If the socket was created by net.Server we will track the socket +// if (message.key) { +// // Add socket to connections list +// const socketList = getSocketList("got", this, message.key); +// socketList.add({ +// socket: socket, +// }); +// } + +// emit(socket); +// }, +// }, + +// "dgram.Native": { +// simultaneousAccepts: false, + +// send(message, handle, options) { +// return handle; +// }, + +// got(message, handle, emit) { +// emit(handle); +// }, +// }, + +// "dgram.Socket": { +// simultaneousAccepts: false, + +// send(message, socket, options) { +// message.dgramType = socket.type; + +// return socket[kStateSymbol].handle; +// }, + +// got(message, handle, emit) { +// const socket = new dgram.Socket(message.dgramType); + +// socket.bind(handle, () => { +// emit(socket); +// }); +// }, +// }, +// }; + +// have to use jsdoc type definitions because bundle-functions is based on regex +/** + * @typedef {Object} Serialized + * @property {"NODE_HANDLE"} cmd + * @property {unknown} message + * @property {"net.Socket" | "net.Server" | "dgram.Socket"} type + */ +/** + * @typedef {import("node:net").Server | import("node:net").Socket | import("node:dgram").Socket} Handle + */ +/** + * @param {unknown} message + * @param {Handle} handle + * @param {{ keepOpen?: boolean } | undefined} options + * @returns {[unknown, Serialized] | null} + */ +export function serialize(_message, _handle, _options) { + // sending file descriptors is not supported yet + return null; // send the message without the file descriptor + + /* + const net = require("node:net"); + const dgram = require("node:dgram"); + if (handle instanceof net.Server) { + // this one doesn't need a close function, but the fd needs to be kept alive until it is sent + const server = handle as unknown as (typeof net)["Server"] & { _handle: Bun.TCPSocketListener }; + return [server._handle, { cmd: "NODE_HANDLE", message, type: "net.Server" }]; + } else if (handle instanceof net.Socket) { + const new_message: { cmd: "NODE_HANDLE"; message: unknown; type: "net.Socket"; key?: string } = { + cmd: "NODE_HANDLE", + message, + type: "net.Socket", + }; + const socket = handle as unknown as (typeof net)["Socket"] & { + _handle: Bun.Socket; + server: (typeof net)["Server"] | null; + setTimeout(timeout: number): void; + }; + if (!socket._handle) return null; // failed + + // If the socket was created by net.Server + if (socket.server) { + // The worker should keep track of the socket + new_message.key = socket.server._connectionKey; + + const firstTime = !this[kChannelHandle].sockets.send[message.key]; + const socketList = getSocketList("send", this, message.key); + + // The server should no longer expose a .connection property + // and when asked to close it should query the socket status from + // the workers + if (firstTime) socket.server._setupWorker(socketList); + + // Act like socket is detached + if (!options?.keepOpen) socket.server._connections--; + } + + const internal_handle = socket._handle; + + // Remove handle from socket object, it will be closed when the socket + // will be sent + if (!options?.keepOpen) { + // we can use a $newZigFunction to have it unset the callback + internal_handle.onread = nop; + socket._handle = null; + socket.setTimeout(0); + } + return [internal_handle, new_message]; + } else if (handle instanceof dgram.Socket) { + // this one doesn't need a close function, but the fd needs to be kept alive until it is sent + throw new Error("todo serialize dgram.Socket"); + } else { + throw $ERR_INVALID_HANDLE_TYPE(); + } + */ +} +/** + * @param {Serialized} serialized + * @param {unknown} handle + * @param {(handle: Handle) => void} emit + * @returns {void} + */ +export function parseHandle(target, serialized, fd) { + const emit = $newZigFunction("ipc.zig", "emitHandleIPCMessage", 3); + const net = require("node:net"); + // const dgram = require("node:dgram"); + switch (serialized.type) { + case "net.Server": { + const server = new net.Server(); + server.listen({ fd }, () => { + emit(target, serialized.message, server); + }); + return; + } + case "net.Socket": { + throw new Error("TODO case net.Socket"); + } + case "dgram.Socket": { + throw new Error("TODO case dgram.Socket"); + } + default: { + throw new Error("failed to parse handle"); + } + } +} diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index eb80156ca7..a66a7f0fee 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -206,6 +206,7 @@ function execFile(file, args, options, callback) { ({ file, args, options, callback } = normalizeExecFileArgs(file, args, options, callback)); options = { + __proto__: null, encoding: "utf8", timeout: 0, maxBuffer: MAX_BUFFER, @@ -845,7 +846,7 @@ function normalizeExecArgs(command, options, callback) { } // Make a shallow copy so we don't clobber the user's options object. - options = { ...options }; + options = { __proto__: null, ...options }; options.shell = typeof options.shell === "string" ? options.shell : true; return { @@ -878,6 +879,7 @@ function normalizeSpawnArguments(file, args, options) { if (options === undefined) options = {}; else validateObject(options, "options"); + options = { __proto__: null, ...options }; let cwd = options.cwd; // Validate the cwd, if present. @@ -1128,12 +1130,16 @@ class ChildProcess extends EventEmitter { if (!stdin) // This can happen if the process was already killed. return new ShimmedStdin(); - return require("internal/fs/streams").writableFromFileSink(stdin); + const result = require("internal/fs/streams").writableFromFileSink(stdin); + result.readable = false; + return result; } case "inherit": return null; case "destroyed": return new ShimmedStdin(); + case "undefined": + return undefined; default: return null; } @@ -1154,6 +1160,8 @@ class ChildProcess extends EventEmitter { } case "destroyed": return new ShimmedStdioOutStream(); + case "undefined": + return undefined; default: return null; } @@ -1184,6 +1192,9 @@ class ChildProcess extends EventEmitter { for (let i = 0; i < length; i++) { const element = opts[i]; + if (element === "undefined") { + return undefined; + } if (element !== "pipe") { result[i] = null; continue; @@ -1337,19 +1348,38 @@ class ChildProcess extends EventEmitter { } } } catch (ex) { - if (ex == null || typeof ex !== "object" || !Object.hasOwn(ex, "errno")) throw ex; - this.#handle = null; - ex.syscall = "spawn " + this.spawnfile; - ex.spawnargs = Array.prototype.slice.$call(this.spawnargs, 1); - process.nextTick(() => { - this.emit("error", ex); - this.emit("close", (ex as SystemError).errno ?? -1); - }); + if ( + ex != null && + typeof ex === "object" && + Object.hasOwn(ex, "code") && + // node sends these errors on the next tick rather than throwing + (ex.code === "EACCES" || + ex.code === "EAGAIN" || + ex.code === "EMFILE" || + ex.code === "ENFILE" || + ex.code === "ENOENT") + ) { + this.#handle = null; + ex.syscall = "spawn " + this.spawnfile; + ex.spawnargs = Array.prototype.slice.$call(this.spawnargs, 1); + process.nextTick(() => { + this.emit("error", ex); + this.emit("close", (ex as SystemError).errno ?? -1); + }); + if (ex.code === "EMFILE" || ex.code === "ENFILE") { + // emfile/enfile error; in this case node does not initialize stdio streams. + this.#stdioOptions[0] = "undefined"; + this.#stdioOptions[1] = "undefined"; + this.#stdioOptions[2] = "undefined"; + } + } else { + throw ex; + } } } - #emitIpcMessage(message) { - this.emit("message", message); + #emitIpcMessage(message, _, handle) { + this.emit("message", message, handle); } #send(message, handle, options, callback) { @@ -1375,19 +1405,16 @@ class ChildProcess extends EventEmitter { return false; } - // Bun does not handle handles yet - try { - this.#handle.send(message); - if (callback) process.nextTick(callback, null); - return true; - } catch (error) { + // We still need this send function because + return this.#handle.send(message, handle, options, err => { + // node does process.nextTick() to emit or call the callback + // we don't need to because the send callback is called on nextTick by ipc.zig if (callback) { - process.nextTick(callback, error); - } else { - this.emit("error", error); + callback(err); + } else if (err) { + this.emit("error", err); } - return false; - } + }); } #onDisconnect(firstTime: boolean) { diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 9463c41efc..6320e7ab3c 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -1342,6 +1342,7 @@ Server.prototype.listen = function listen(port, hostname, onListen) { let allowHalfOpen = false; let reusePort = false; let ipv6Only = false; + let fd; //port is actually path if (typeof port === "string") { if (Number.isSafeInteger(hostname)) { @@ -1378,6 +1379,11 @@ Server.prototype.listen = function listen(port, hostname, onListen) { allowHalfOpen = options.allowHalfOpen; reusePort = options.reusePort; + if (typeof options.fd === "number" && options.fd >= 0) { + fd = options.fd; + port = 0; + } + const isLinux = process.platform === "linux"; if (!Number.isSafeInteger(port) || port < 0) { @@ -1456,7 +1462,7 @@ Server.prototype.listen = function listen(port, hostname, onListen) { port, 4, backlog, - undefined, + fd, exclusive, ipv6Only, allowHalfOpen, @@ -1486,6 +1492,7 @@ Server.prototype[kRealListen] = function ( tls, contexts, _onListen, + fd, ) { if (path) { this._handle = Bun.listen({ @@ -1497,6 +1504,17 @@ Server.prototype[kRealListen] = function ( exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, socket: ServerHandlers, }); + } else if (fd != null) { + this._handle = Bun.listen({ + fd, + hostname, + tls, + allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false, + reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false, + ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false, + exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false, + socket: ServerHandlers, + }); } else { this._handle = Bun.listen({ port, @@ -1601,7 +1619,19 @@ function listenInCluster( if (cluster === undefined) cluster = require("node:cluster"); if (cluster.isPrimary || exclusive) { - server[kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen); + server[kRealListen]( + path, + port, + hostname, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + tls, + contexts, + onListen, + fd, + ); return; } @@ -1619,7 +1649,19 @@ function listenInCluster( if (err) { throw new ExceptionWithHostPort(err, "bind", address, port); } - server[kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen); + server[kRealListen]( + path, + port, + hostname, + exclusive, + ipv6Only, + allowHalfOpen, + reusePort, + tls, + contexts, + onListen, + fd, + ); }); } diff --git a/src/main.zig b/src/main.zig index 2988946c71..c97fe2e143 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,6 +33,10 @@ pub fn main() void { std.posix.sigaction(std.posix.SIG.XFSZ, &act, null); } + if (Environment.isDebug) { + bun.debug_allocator_data.backing = .init; + } + // This should appear before we make any calls at all to libuv. // So it's safest to put it very early in the main function. if (Environment.isWindows) { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 523d109a9e..dbc49a3ac7 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -896,7 +896,7 @@ pub const Resolver = struct { // Fragments in URLs in CSS imports are technically expected to work if (tmp == .not_found and kind.isFromCSS()) try_without_suffix: { // If resolution failed, try again with the URL query and/or hash removed - const maybe_suffix = std.mem.indexOfAny(u8, import_path, "?#"); + const maybe_suffix = bun.strings.indexOfAny(import_path, "?#"); if (maybe_suffix == null or maybe_suffix.? < 1) break :try_without_suffix; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 8f8427a2d4..1e6af437e1 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -1843,7 +1843,7 @@ pub const PostgresSQLConnection = struct { // We create it right here so we can throw errors early. const context_options = tls_config.asUSockets(); var err: uws.create_bun_socket_error_t = .none; - tls_ctx = uws.us_create_bun_socket_context(1, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), context_options, &err) orelse { + tls_ctx = uws.us_create_bun_ssl_socket_context(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), context_options, &err) orelse { if (err != .none) { return globalObject.throw("failed to create TLS context", .{}); } else { @@ -1951,8 +1951,7 @@ pub const PostgresSQLConnection = struct { defer hostname.deinit(); const ctx = vm.rareData().postgresql_context.tcp orelse brk: { - var err: uws.create_bun_socket_error_t = .none; - const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), uws.us_bun_socket_context_options_t{}, &err).?; + const ctx_ = uws.us_create_bun_nossl_socket_context(vm.uwsLoop(), @sizeOf(*PostgresSQLConnection)).?; uws.NewSocketHandler(false).configure(ctx_, true, *PostgresSQLConnection, SocketHandler(false)); vm.rareData().postgresql_context.tcp = ctx_; break :brk ctx_; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 6fece06564..30fe0042bc 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -3765,8 +3765,7 @@ pub fn wtf8Sequence(code_point: u32) [4]u8 { pub inline fn wtf8ByteSequenceLength(first_byte: u8) u8 { return switch (first_byte) { - 0 => 0, - 1...0x80 - 1 => 1, + 0...0x80 - 1 => 1, else => if ((first_byte & 0xE0) == 0xC0) 2 else if ((first_byte & 0xF0) == 0xE0) diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index dbb14bed70..135cc2eb97 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -532,8 +532,7 @@ pub const JSValkeyClient = struct { .none => .{ vm.rareData().valkey_context.tcp orelse brk_ctx: { // TCP socket - var err: uws.create_bun_socket_error_t = .none; - const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*JSValkeyClient), uws.us_bun_socket_context_options_t{}, &err).?; + const ctx_ = uws.us_create_bun_nossl_socket_context(vm.uwsLoop(), @sizeOf(*JSValkeyClient)).?; uws.NewSocketHandler(false).configure(ctx_, true, *JSValkeyClient, SocketHandler(false)); vm.rareData().valkey_context.tcp = ctx_; break :brk_ctx ctx_; @@ -544,7 +543,7 @@ pub const JSValkeyClient = struct { vm.rareData().valkey_context.tls orelse brk_ctx: { // TLS socket, default config var err: uws.create_bun_socket_error_t = .none; - const ctx_ = uws.us_create_bun_socket_context(1, vm.uwsLoop(), @sizeOf(*JSValkeyClient), uws.us_bun_socket_context_options_t{}, &err).?; + const ctx_ = uws.us_create_bun_ssl_socket_context(vm.uwsLoop(), @sizeOf(*JSValkeyClient), uws.us_bun_socket_context_options_t{}, &err).?; uws.NewSocketHandler(true).configure(ctx_, true, *JSValkeyClient, SocketHandler(true)); vm.rareData().valkey_context.tls = ctx_; break :brk_ctx ctx_; @@ -555,7 +554,7 @@ pub const JSValkeyClient = struct { // TLS socket, custom config var err: uws.create_bun_socket_error_t = .none; const options = custom.asUSockets(); - const ctx_ = uws.us_create_bun_socket_context(1, vm.uwsLoop(), @sizeOf(*JSValkeyClient), options, &err).?; + const ctx_ = uws.us_create_bun_ssl_socket_context(vm.uwsLoop(), @sizeOf(*JSValkeyClient), options, &err).?; uws.NewSocketHandler(true).configure(ctx_, true, *JSValkeyClient, SocketHandler(true)); break :brk_ctx .{ ctx_, true }; }, diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 8f047aeecb..5aa7cc2180 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -12,7 +12,8 @@ const words: Record "std.debug.assert": { reason: "Use bun.assert instead", limit: 26 }, "std.debug.dumpStackTrace": { reason: "Use bun.handleErrorReturnTrace or bun.crash_handler.dumpStackTrace instead" }, "std.debug.print": { reason: "Don't let this be committed", limit: 0 }, - "std.mem.indexOfAny(u8": { reason: "Use bun.strings.indexOfAny", limit: 2 }, + "std.log": { reason: "Don't let this be committed", limit: 1 }, + "std.mem.indexOfAny(u8": { reason: "Use bun.strings.indexOfAny" }, "std.StringArrayHashMapUnmanaged(": { reason: "bun.StringArrayHashMapUnmanaged has a faster `eql`", limit: 12 }, "std.StringArrayHashMap(": { reason: "bun.StringArrayHashMap has a faster `eql`", limit: 1 }, "std.StringHashMapUnmanaged(": { reason: "bun.StringHashMapUnmanaged has a faster `eql`" }, @@ -32,6 +33,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 240, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1850 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 180 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 }, @@ -39,7 +41,7 @@ const words: Record ".stdFile()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 18 }, ".stdDir()": { reason: "Prefer bun.sys + bun.FD instead of std.fs.File. Zig hides 'errno' when Bun wants to match libuv", limit: 48 }, - ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 289 }, + ".arguments_old(": { reason: "Please migrate to .argumentsAsArray() or another argument API", limit: 287 }, "// autofix": { reason: "Evaluate if this variable should be deleted entirely or explicitly discarded.", limit: 176 }, }; diff --git a/test/js/node/child_process/child_process_ipc.test.js b/test/js/node/child_process/child_process_ipc.test.js new file mode 100644 index 0000000000..2e2b3e6143 --- /dev/null +++ b/test/js/node/child_process/child_process_ipc.test.js @@ -0,0 +1,15 @@ +import { $ } from "bun"; +import { bunExe } from "harness"; + +test("child_process ipc", async () => { + const output = await $`${bunExe()} ${import.meta.dir}/fixtures/ipc_fixture.js`.text(); + // node (v23.4.0) has identical output + expect(output).toMatchInlineSnapshot(` + "Parent received: {"status":"Child process started"} + Child process exited with code 0 + send returned false + uncaughtException ERR_IPC_CHANNEL_CLOSED + cb ERR_IPC_CHANNEL_CLOSED + " + `); +}); diff --git a/test/js/node/child_process/child_process_ipc_large_disconnect.test.js b/test/js/node/child_process/child_process_ipc_large_disconnect.test.js new file mode 100644 index 0000000000..13866b8164 --- /dev/null +++ b/test/js/node/child_process/child_process_ipc_large_disconnect.test.js @@ -0,0 +1,12 @@ +import { bunExe } from "harness"; + +test("child_process_ipc_large_disconnect", () => { + const file = __dirname + "/fixtures/child-process-ipc-large-disconect.mjs"; + const actual = Bun.spawnSync([bunExe(), file]); + + expect(actual.stderr.toString()).toBe(""); + expect(actual.exitCode).toBe(0); + expect(actual.stdout.toString()).toStartWith(`2: a\n2: b\n2: c\n2: d\n`); + // large messages aren't always sent before disconnect. they are on windows but not on mac. + expect(actual.stdout.toString()).toEndWith(`disconnected\n`); +}); diff --git a/test/js/node/child_process/child_process_send_cb.test.js b/test/js/node/child_process/child_process_send_cb.test.js new file mode 100644 index 0000000000..dd85c3def2 --- /dev/null +++ b/test/js/node/child_process/child_process_send_cb.test.js @@ -0,0 +1,52 @@ +import { test, expect } from "bun:test"; +import { bunExe } from "harness"; + +const ok_repeated = "ok".repeat(16384); + +test("child_process_send_cb", () => { + const child = Bun.spawnSync({ + cmd: [bunExe(), import.meta.dirname + "/fixtures/child-process-send-cb-more.js"], + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + NO_COLOR: "1", + }, + }); + const stdout_text = child.stdout.toString(); + const stderr_text = child.stderr.toString(); + // identical output to node (v23.4.0) + expect("CHILD\n" + stdout_text + "\nPARENT\n" + stderr_text + "\nEXIT CODE: " + child.exitCode) + .toMatchInlineSnapshot(` + "CHILD + send simple + send ok.repeat(16384) + send 2 + send 3 + send 4 + send 5 + cb simple null + cb ok.repeat(16384) null + cb 2 null + cb 3 null + cb 4 null + cb 5 null + send 6 + send 7 + cb 6 null + cb 7 null + + PARENT + parent got message "simple" + parent got message "ok…ok" + parent got message "2" + parent got message "3" + parent got message "4" + parent got message "5" + parent got message "6" + parent got message "ok…ok" + parent got exit event 0 null + + EXIT CODE: 0" + `); +}); diff --git a/test/js/node/child_process/fixtures/child-process-ipc-large-disconect.mjs b/test/js/node/child_process/fixtures/child-process-ipc-large-disconect.mjs new file mode 100644 index 0000000000..38307ee423 --- /dev/null +++ b/test/js/node/child_process/fixtures/child-process-ipc-large-disconect.mjs @@ -0,0 +1,23 @@ +import { fork } from "child_process"; + +if (process.argv[2] === "child") { + process.send("a!"); + process.send("b!"); + process.send("c!"); + process.send("d!"); + process.send("hello".repeat(2 ** 15)); + process.send("goodbye".repeat(2 ** 15)); + process.send("hello".repeat(2 ** 15)); + process.send("goodbye".repeat(2 ** 15)); + process.disconnect(); +} else { + const proc = fork(process.argv[1], ["child"], {}); + + proc.on("message", message => { + console.log(message.length + ": " + message[message.length - 2]); + }); + + proc.on("disconnect", () => { + console.log("disconnected"); + }); +} diff --git a/test/js/node/child_process/fixtures/child-process-send-cb-more.js b/test/js/node/child_process/fixtures/child-process-send-cb-more.js new file mode 100644 index 0000000000..81575c81b3 --- /dev/null +++ b/test/js/node/child_process/fixtures/child-process-send-cb-more.js @@ -0,0 +1,53 @@ +// more comprehensive version of test-child-process-send-cb + +"use strict"; +const fork = require("child_process").fork; + +if (process.argv[2] === "child") { + console.log("send simple"); + process.send("simple", err => { + console.log("cb simple", err); + }); + console.log("send ok.repeat(16384)"); + process.send("ok".repeat(16384), err => { + console.log("cb ok.repeat(16384)", err); + }); + console.log("send 2"); + process.send("2", err => { + console.log("cb 2", err); + }); + console.log("send 3"); + process.send("3", err => { + console.log("cb 3", err); + }); + console.log("send 4"); + process.send("4", err => { + console.log("cb 4", err); + }); + console.log("send 5"); + process.send("5", err => { + console.log("cb 5", err); + console.log("send 6"); + process.send("6", err => { + // interestingly, node will call this callback before the outer callbacks are done being called + console.log("cb 6", err); + }); + console.log("send 7"); + process.send("ok".repeat(16384), err => { + console.log("cb 7", err); + }); + }); +} else { + const child = fork(process.argv[1], ["child"], { + // env: { + // ...process.env, + // "BUN_DEBUG": "out2", + // }, + }); + child.on("message", message => { + console.error("parent got message", JSON.stringify(message).replace("ok".repeat(16384), "ok…ok")); + }); + child.on("exit", (exitCode, signalCode) => { + console.error(`parent got exit event ${exitCode} ${signalCode}`); + }); +} diff --git a/test/js/node/child_process/fixtures/ipc_fixture.js b/test/js/node/child_process/fixtures/ipc_fixture.js new file mode 100644 index 0000000000..1a78cb5d60 --- /dev/null +++ b/test/js/node/child_process/fixtures/ipc_fixture.js @@ -0,0 +1,57 @@ +const { spawn } = require("child_process"); +const path = require("path"); +const net = require("net"); + +if (process.argv[2] === "child") { + // Send initial message to parent + process.send({ status: "Child process started" }); +} else { + // Spawn child process with IPC enabled + const child = spawn(process.execPath, [process.argv[1], "child"], { + stdio: ["inherit", "inherit", "inherit", "ipc"], + }); + + // Listen for messages from child + child.on("message", message => { + console.log("Parent received:", JSON.stringify(message)); + }); + + // Handle child process exit + child.on("exit", code => { + console.log(`Child process exited with code ${code}`); + try { + console.log("send returned", child.send({ msg: "uh oh" })); + } catch (ex) { + console.log("[1]caught", ex.code); + } + try { + child.send({ msg: "uh oh!" }, a => { + console.log("cb", a.code); + }); + } catch (ex) { + console.log("[2]caught", ex.code); + } + }); + process.on("uncaughtException", err => { + console.log("uncaughtException", err.code); + }); + + // Send initial message to child + + // support: + // net.Socket, net.Server, net.Native, dgram.Socket, dgram.Native + // sends message {cmd: NODE_HANDLE, type: } + + const server = net.createServer(); + + child.send({ greeting: "Hello child process!" }, server); + + // Listen for messages from parent + process.on("message", message => { + console.log("Child received:", JSON.stringify(message)); + + // Send a message back to parent + process.send({ message: "Hello from child!" }); + process.channel.unref(); + }); +} diff --git a/test/js/node/net/double-connect-repro.mjs b/test/js/node/net/double-connect-repro.mjs new file mode 100644 index 0000000000..f6ff59d640 --- /dev/null +++ b/test/js/node/net/double-connect-repro.mjs @@ -0,0 +1,89 @@ +import { fork } from "child_process"; +import { connect, createServer } from "net"; + +if (process.argv[2] === "child") { + // child + console.log("[child] starting"); + process.send({ what: "ready" }); + const [message, handle] = await new Promise(r => process.once("message", (message, handle) => r([message, handle]))); + console.log("[child] <-", JSON.stringify(message), handle != null); + handle.on("connection", socket => { + console.log("\x1b[95m[client] got connection\x1b[m"); + socket.destroy(); + }); + process.send({ what: "listening" }); + const message2 = await new Promise(r => process.once("message", r)); + console.log("[child] <-", JSON.stringify(message2)); + handle.close(); +} else if (process.argv[2] === "minimal") { + const server = createServer(); + server.on("connection", socket => { + console.log("\x1b[92m[parent] got connection\x1b[m"); + socket.destroy(); + }); + await new Promise(r => { + server.on("listening", r); + server.listen(0); + }); + console.log("[parent] server listening on port", server.address().port > 0); + + console.log("[connection] create"); + let socket; + await new Promise(r => (socket = connect(server.address().port, r))); + console.log("[connection] connected"); + await new Promise(r => socket.on("close", r)); + console.log("[connection] closed"); + + server.close(); +} else { + console.log("[parent] starting"); + const child = fork(process.argv[1], ["child"]); + console.log("[parent] <- ", JSON.stringify(await new Promise(r => child.once("message", r)))); + + const server = createServer(); + server.on("connection", socket => { + console.log("\x1b[92m[parent] got connection\x1b[m"); + socket.destroy(); + }); + await new Promise(r => { + server.on("listening", r); + server.listen(0); + }); + console.log("[parent] server listening on port", server.address().port > 0); + + for (let i = 0; i < 4; i++) { + console.log("[connection] create"); + let socket; + await new Promise(r => (socket = connect(server.address().port, r))); + console.log("[connection] connected"); + await new Promise(r => socket.on("close", r)); + console.log("[connection] closed"); + } + + const result = await new Promise(r => child.send({ what: "server" }, server, r)); + if (result != null) throw result; + console.log("[parent] sent server to child"); + console.log("[parent] <- ", JSON.stringify(await new Promise(r => child.once("message", r)))); + + // once sent to the child, messages can be handled by either the parent or the child + for (let i = 0; i < 128; i++) { + // console.log("[connection] create"); + let socket; + await new Promise( + r => + (socket = connect( + { + port: server.address().port, + host: "127.0.0.1", + }, + r, + )), + ); + // console.log("[connection] connected"); + await new Promise(r => socket.on("close", r)); + // console.log("[connection] closed"); + } + + server.close(); + child.send({ what: "close" }); +} diff --git a/test/js/node/net/double-connect.test.ts b/test/js/node/net/double-connect.test.ts new file mode 100644 index 0000000000..c14d410c4a --- /dev/null +++ b/test/js/node/net/double-connect.test.ts @@ -0,0 +1,26 @@ +import { bunExe } from "harness"; + +// TODO: fix double connect +test.failing("double connect", () => { + const output = Bun.spawnSync({ + cmd: [bunExe(), import.meta.dirname + "/double-connect-repro.mjs", "minimal"], + }); + expect({ + exitCode: output.exitCode, + stderr: output.stderr.toString("utf-8"), + stdout: output.stdout.toString("utf-8"), + }).toMatchInlineSnapshot(` + { + "exitCode": 0, + "stderr": "", + "stdout": + "[parent] server listening on port true + [connection] create + [connection] connected + \x1B[92m[parent] got connection\x1B[m + [connection] closed + " + , + } + `); +}); diff --git a/test/js/node/test/parallel/test-child-process-detached.js b/test/js/node/test/parallel/test-child-process-detached.js new file mode 100644 index 0000000000..b9d636959e --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-detached.js @@ -0,0 +1,43 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +const spawn = require('child_process').spawn; +const childPath = fixtures.path('parent-process-nonpersistent.js'); +let persistentPid = -1; + +const child = spawn(process.execPath, [ childPath ]); + +child.stdout.on('data', function(data) { + persistentPid = parseInt(data, 10); +}); + +process.on('exit', function() { + assert.notStrictEqual(persistentPid, -1); + assert.throws(function() { + process.kill(child.pid); + }, /^Error: kill ESRCH$|^SystemError: kill\(\) failed: ESRCH: [Nn]o such process$/); + process.kill(persistentPid); +}); diff --git a/test/js/node/test/parallel/test-child-process-emfile.js b/test/js/node/test/parallel/test-child-process-emfile.js new file mode 100644 index 0000000000..8ee6dd52e3 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-emfile.js @@ -0,0 +1,78 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (common.isWindows) + common.skip('no RLIMIT_NOFILE on Windows'); + +const assert = require('assert'); +const child_process = require('child_process'); +const fs = require('fs'); + +const ulimit = Number(child_process.execSync('ulimit -Hn')); +if (ulimit > 64 || Number.isNaN(ulimit)) { + const [cmd, opts] = common.escapePOSIXShell`ulimit -n 64 && "${process.execPath}" "${__filename}"`; + // Sorry about this nonsense. It can be replaced if + // https://github.com/nodejs/node-v0.x-archive/pull/2143#issuecomment-2847886 + // ever happens. + const result = child_process.spawnSync( + '/bin/sh', + ['-c', cmd], + opts, + ); + assert.strictEqual(result.stdout.toString(), ''); + assert.strictEqual(result.stderr.toString(), ''); + assert.strictEqual(result.status, 0); + assert.strictEqual(result.error, undefined); + return; +} + +const openFds = []; + +for (;;) { + try { + openFds.push(fs.openSync(__filename, 'r')); + } catch (err) { + assert.strictEqual(err.code, 'EMFILE'); + break; + } +} + +// Should emit an error, not throw. +const proc = child_process.spawn(process.execPath, ['-e', '0']); + +// Verify that stdio is not setup on EMFILE or ENFILE. +assert.strictEqual(proc.stdin, undefined); +assert.strictEqual(proc.stdout, undefined); +assert.strictEqual(proc.stderr, undefined); +assert.strictEqual(proc.stdio, undefined); + +proc.on('error', common.mustCall(function(err) { + assert.strictEqual(err.code, 'EMFILE'); +})); + +proc.on('exit', common.mustNotCall('"exit" event should not be emitted')); + +// Close one fd for LSan +if (openFds.length >= 1) { + fs.closeSync(openFds.pop()); +} diff --git a/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js b/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js index bfc9cd7a53..176126ed9d 100644 --- a/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js +++ b/test/js/node/test/parallel/test-child-process-exec-abortcontroller-promisified.js @@ -1,3 +1,5 @@ +// Modified to allow the abort error to have a 'stack' property + 'use strict'; const common = require('../common'); const assert = require('assert'); @@ -15,15 +17,27 @@ const waitCommand = common.isWindows ? `"${process.execPath}" -e "setInterval(()=>{}, 99)"` : 'sleep 2m'; -{ +if(typeof Bun !== "undefined") { const ac = new AbortController(); const signal = ac.signal; const promise = execPromisifed(waitCommand, { signal }); + promise.catch(common.mustCall(e => { + assert.equal(e.name, 'AbortError'); + assert.ok(e.cause instanceof DOMException); + assert.equal(e.cause.name, 'AbortError'); + assert.equal(e.cause.message, 'The operation was aborted.'); + assert.equal(e.cause.code, 20); + })); ac.abort(); +}else{ + const ac = new AbortController(); + const signal = ac.signal; + const promise = execPromisifed(waitCommand, { signal }); assert.rejects(promise, { name: 'AbortError', - cause: ac.signal.reason, + cause: new DOMException('This operation was aborted', 'AbortError'), }).then(common.mustCall()); + ac.abort(); } { diff --git a/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs b/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs new file mode 100644 index 0000000000..d94c4bdbc6 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-prototype-tampering.mjs @@ -0,0 +1,91 @@ +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { EOL } from 'node:os'; +import { strictEqual, notStrictEqual, throws } from 'node:assert'; +import cp from 'node:child_process'; + +// TODO(LiviaMedeiros): test on different platforms +if (!common.isLinux) + common.skip(); + +const expectedCWD = process.cwd(); +const expectedUID = process.getuid(); + +for (const tamperedCwd of ['', '/tmp', '/not/existing/malicious/path', 42n]) { + Object.prototype.cwd = tamperedCwd; + + cp.exec('pwd', common.mustSucceed((out) => { + strictEqual(`${out}`, `${expectedCWD}${EOL}`); + })); + strictEqual(`${cp.execSync('pwd')}`, `${expectedCWD}${EOL}`); + cp.execFile('pwd', common.mustSucceed((out) => { + strictEqual(`${out}`, `${expectedCWD}${EOL}`); + })); + strictEqual(`${cp.execFileSync('pwd')}`, `${expectedCWD}${EOL}`); + cp.spawn('pwd').stdout.on('data', common.mustCall((out) => { + strictEqual(`${out}`, `${expectedCWD}${EOL}`); + })); + strictEqual(`${cp.spawnSync('pwd').stdout}`, `${expectedCWD}${EOL}`); + + delete Object.prototype.cwd; +} + +for (const tamperedUID of [0, 1, 999, 1000, 0n, 'gwak']) { + Object.prototype.uid = tamperedUID; + + cp.exec('id -u', common.mustSucceed((out) => { + strictEqual(`${out}`, `${expectedUID}${EOL}`); + })); + strictEqual(`${cp.execSync('id -u')}`, `${expectedUID}${EOL}`); + cp.execFile('id', ['-u'], common.mustSucceed((out) => { + strictEqual(`${out}`, `${expectedUID}${EOL}`); + })); + strictEqual(`${cp.execFileSync('id', ['-u'])}`, `${expectedUID}${EOL}`); + cp.spawn('id', ['-u']).stdout.on('data', common.mustCall((out) => { + strictEqual(`${out}`, `${expectedUID}${EOL}`); + })); + strictEqual(`${cp.spawnSync('id', ['-u']).stdout}`, `${expectedUID}${EOL}`); + + delete Object.prototype.uid; +} + +{ + Object.prototype.execPath = '/not/existing/malicious/path'; + + // Does not throw ENOENT + cp.fork(fixtures.path('empty.js')); + + delete Object.prototype.execPath; +} + +for (const shellCommandArgument of ['-L && echo "tampered"']) { + Object.prototype.shell = true; + const cmd = 'pwd'; + let cmdExitCode = ''; + + const program = cp.spawn(cmd, [shellCommandArgument], { cwd: expectedCWD }); + program.stderr.on('data', common.mustCall()); + program.stdout.on('data', common.mustNotCall()); + + program.on('exit', common.mustCall((code) => { + notStrictEqual(code, 0); + })); + + cp.execFile(cmd, [shellCommandArgument], { cwd: expectedCWD }, + common.mustCall((err) => { + notStrictEqual(err.code, 0); + }) + ); + + throws(() => { + cp.execFileSync(cmd, [shellCommandArgument], { cwd: expectedCWD }); + }, (e) => { + notStrictEqual(e.status, 0); + return true; + }); + + cmdExitCode = cp.spawnSync(cmd, [shellCommandArgument], { cwd: expectedCWD }).status; + notStrictEqual(cmdExitCode, 0); + + delete Object.prototype.shell; +} diff --git a/test/js/node/test/parallel/test-child-process-reject-null-bytes.js b/test/js/node/test/parallel/test-child-process-reject-null-bytes.js new file mode 100644 index 0000000000..db0db64fd8 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-reject-null-bytes.js @@ -0,0 +1,296 @@ +'use strict'; +const { mustNotCall } = require('../common'); + +// Regression test for https://github.com/nodejs/node/issues/44768 + +const { throws } = require('assert'); +const { + exec, + execFile, + execFileSync, + execSync, + fork, + spawn, + spawnSync, +} = require('child_process'); + +// Tests for the 'command' argument + +throws(() => exec(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => exec('BBB\0XXX AAA CCC', mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync('BBB\0XXX AAA CCC'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'file' argument + +throws(() => spawn('BBB\0XXX'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile('BBB\0XXX', mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync('BBB\0XXX'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn('BBB\0XXX'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync('BBB\0XXX'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'modulePath' argument + +throws(() => fork('BBB\0XXX'), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'args' argument + +// Not testing exec() and execSync() because these accept 'args' as a part of +// 'command' as space-separated arguments. + +throws(() => execFile(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC'], mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => fork(__filename, ['AAA', 'BBB\0XXX', 'CCC']), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.cwd' argument + +throws(() => exec(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, { cwd: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(process.execPath, { cwd: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => fork(__filename, { cwd: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn(process.execPath, { cwd: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, { cwd: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.argv0' argument + +throws(() => exec(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, { argv0: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(process.execPath, { argv0: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => fork(__filename, { argv0: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn(process.execPath, { argv0: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, { argv0: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.shell' argument + +throws(() => exec(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, { shell: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(process.execPath, { shell: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Not testing fork() because it doesn't accept the shell option (internally it +// explicitly sets shell to false). + +throws(() => spawn(process.execPath, { shell: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, { shell: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.env' argument + +throws(() => exec(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => exec(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFile(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execFileSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => execSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => fork(__filename, { env: { 'AAA': 'BBB\0XXX' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => fork(__filename, { env: { 'BBB\0XXX': 'AAA' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawn(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +throws(() => spawnSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.execPath' argument +throws(() => fork(__filename, { execPath: 'BBB\0XXX' }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', +}); + +// Tests for the 'options.execArgv' argument +if(typeof Bun === 'undefined') { // This test is disabled in bun because bun does not support execArgv. + throws(() => fork(__filename, { execArgv: ['AAA', 'BBB\0XXX', 'CCC'] }), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); +} diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-kill-signal.js b/test/js/node/test/parallel/test-child-process-spawnsync-kill-signal.js new file mode 100644 index 0000000000..77d3c699fa --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-kill-signal.js @@ -0,0 +1,49 @@ +// This test is modified to not test node internals, only public APIs. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +if (process.argv[2] === 'child') { + setInterval(() => {}, 1000); +} else { + const { SIGKILL } = require('os').constants.signals; + + function spawn(killSignal) { + const child = cp.spawnSync(process.execPath, + [__filename, 'child'], + { killSignal, timeout: 100 }); + assert.strictEqual(child.status, null); + assert.strictEqual(child.error.code, 'ETIMEDOUT'); + return child; + } + + // Verify that an error is thrown for unknown signals. + assert.throws(() => { + spawn('SIG_NOT_A_REAL_SIGNAL'); + }, { code: 'ERR_UNKNOWN_SIGNAL', name: 'TypeError' }); + + // Verify that the default kill signal is SIGTERM. + { + const child = spawn(undefined); + + assert.strictEqual(child.signal, 'SIGTERM'); + } + + // Verify that a string signal name is handled properly. + { + const child = spawn('SIGKILL'); + + assert.strictEqual(child.signal, 'SIGKILL'); + } + + // Verify that a numeric signal is handled properly. + { + assert.strictEqual(typeof SIGKILL, 'number'); + + const child = spawn(SIGKILL); + + assert.strictEqual(child.signal, 'SIGKILL'); + } +} diff --git a/test/js/node/test/parallel/test-child-process-spawnsync-shell.js b/test/js/node/test/parallel/test-child-process-spawnsync-shell.js new file mode 100644 index 0000000000..ebbc892a88 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-spawnsync-shell.js @@ -0,0 +1,81 @@ +// This test is modified to not test node internals, only public APIs. It is also modified to use `-p` rather than `-pe` because Bun does not support `-pe`. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +// Verify that a shell is, in fact, executed +const doesNotExist = cp.spawnSync('does-not-exist', { shell: true }); + +assert.notStrictEqual(doesNotExist.file, 'does-not-exist'); +assert.strictEqual(doesNotExist.error, undefined); +assert.strictEqual(doesNotExist.signal, null); + +if (common.isWindows) + assert.strictEqual(doesNotExist.status, 1); // Exit code of cmd.exe +else + assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh + +// Verify that passing arguments works +const echo = cp.spawnSync('echo', ['foo'], { shell: true }); + +assert.strictEqual(echo.stdout.toString().trim(), 'foo'); + +// Verify that shell features can be used +const cmd = 'echo bar | cat'; +const command = cp.spawnSync(cmd, { shell: true }); + +assert.strictEqual(command.stdout.toString().trim(), 'bar'); + +// Verify that the environment is properly inherited +const env = cp.spawnSync(`"${common.isWindows ? process.execPath : '$NODE'}" -p process.env.BAZ`, { + env: { ...process.env, BAZ: 'buzz', NODE: process.execPath }, + shell: true +}); + +assert.strictEqual(env.stdout.toString().trim(), 'buzz'); + +// Verify that the shell internals work properly across platforms. +{ + const originalComspec = process.env.comspec; + + // Enable monkey patching process.platform. + const originalPlatform = process.platform; + let platform = null; + Object.defineProperty(process, 'platform', { get: () => platform }); + + function test(testPlatform, shell, shellOutput) { + platform = testPlatform; + const cmd = 'not_a_real_command'; + + cp.spawnSync(cmd, { shell }); + } + + // Test Unix platforms with the default shell. + test('darwin', true, '/bin/sh'); + + // Test Unix platforms with a user specified shell. + test('darwin', '/bin/csh', '/bin/csh'); + + // Test Android platforms. + test('android', true, '/system/bin/sh'); + + // Test Windows platforms with a user specified shell. + test('win32', 'powershell.exe', 'powershell.exe'); + + // Test Windows platforms with the default shell and no comspec. + delete process.env.comspec; + test('win32', true, 'cmd.exe'); + + // Test Windows platforms with the default shell and a comspec value. + process.env.comspec = 'powershell.exe'; + test('win32', true, process.env.comspec); + + // Restore the original value of process.platform. + platform = originalPlatform; + + // Restore the original comspec environment variable if necessary. + if (originalComspec) + process.env.comspec = originalComspec; +} diff --git a/test/js/node/test/parallel/test-child-process-stdin.js b/test/js/node/test/parallel/test-child-process-stdin.js new file mode 100644 index 0000000000..24a79d6238 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-stdin.js @@ -0,0 +1,62 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const { + mustCall, + mustCallAtLeast, + mustNotCall, +} = require('../common'); +const assert = require('assert'); +const debug = require('util').debuglog('test'); +const spawn = require('child_process').spawn; + +const cat = spawn('cat'); +cat.stdin.write('hello'); +cat.stdin.write(' '); +cat.stdin.write('world'); + +assert.strictEqual(cat.stdin.writable, true); +assert.strictEqual(cat.stdin.readable, false); + +cat.stdin.end(); + +let response = ''; + +cat.stdout.setEncoding('utf8'); +cat.stdout.on('data', mustCallAtLeast((chunk) => { + debug(`stdout: ${chunk}`); + response += chunk; +})); + +cat.stdout.on('end', mustCall()); + +cat.stderr.on('data', mustNotCall()); + +cat.stderr.on('end', mustCall()); + +cat.on('exit', mustCall((status) => { + assert.strictEqual(status, 0); +})); + +cat.on('close', mustCall(() => { + assert.strictEqual(response, 'hello world'); +})); diff --git a/test/js/node/test/parallel/test-child-process-windows-hide.js b/test/js/node/test/parallel/test-child-process-windows-hide.js new file mode 100644 index 0000000000..e71adf76f3 --- /dev/null +++ b/test/js/node/test/parallel/test-child-process-windows-hide.js @@ -0,0 +1,38 @@ +// This test is modified to not test node internals, only public APIs. windowsHide is not observable, +// so this only tests that the flag does not cause an error. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const { test } = require('node:test'); +const cmd = process.execPath; +const args = ['-p', '42']; +const options = { windowsHide: true }; + +test('spawnSync() passes windowsHide correctly', (t) => { + const child = cp.spawnSync(cmd, args, options); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.strictEqual(child.stdout.toString().trim(), '42'); + assert.strictEqual(child.stderr.toString().trim(), ''); +}); + +test('spawn() passes windowsHide correctly', (t, done) => { + const child = cp.spawn(cmd, args, options); + + child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + done(); + })); +}); + +test('execFile() passes windowsHide correctly', (t, done) => { + cp.execFile(cmd, args, options, common.mustSucceed((stdout, stderr) => { + assert.strictEqual(stdout.trim(), '42'); + assert.strictEqual(stderr.trim(), ''); + done(); + })); +}); diff --git a/test/js/web/abort/abort.test.ts b/test/js/web/abort/abort.test.ts index e337ba8559..2c0b39dac8 100644 --- a/test/js/web/abort/abort.test.ts +++ b/test/js/web/abort/abort.test.ts @@ -1,4 +1,6 @@ +import assert from "assert"; import { describe, expect, test } from "bun:test"; +import { spawn } from "child_process"; import { writeFileSync } from "fs"; import { bunEnv, bunExe, tmpdirSync } from "harness"; import { tmpdir } from "os"; @@ -70,4 +72,28 @@ describe("AbortSignal", () => { await testAny(0); await testAny(1); }); + + function fmt(value: any) { + const res = {}; + for (const key in value) { + if (key === "column" || key === "line" || key === "sourceURL") continue; + res[key] = value[key]; + } + return res; + } + + test(".signal.reason should be a DOMException", () => { + const ac = new AbortController(); + ac.abort(); + expect(ac.signal.reason).toBeInstanceOf(DOMException); + expect(fmt(ac.signal.reason)).toEqual(fmt(new DOMException("The operation was aborted.", "AbortError"))); + expect(ac.signal.reason.code).toBe(20); + }); + test(".signal.reason should be a DOMException for timeout", async () => { + const ac = AbortSignal.timeout(0); + await Bun.sleep(10); + expect(ac.reason).toBeInstanceOf(DOMException); + expect(fmt(ac.reason)).toEqual(fmt(new DOMException("The operation timed out.", "TimeoutError"))); + expect(ac.reason.code).toBe(23); + }); });