From 5fa13625a1ca0ea1a3a1c5bb86d0880dcfac349f Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Wed, 21 Jun 2023 23:38:18 -0700 Subject: upgrade zig to `v0.11.0-dev.3737+9eb008717` (#3374) * progress * finish `@memset/@memcpy` update * Update build.zig * change `@enumToInt` to `@intFromEnum` and friends * update zig versions * it was 1 * add link to issue * add `compileError` reminder * fix merge * format * upgrade to llvm 16 * Revert "upgrade to llvm 16" This reverts commit cc930ceb1c5b4db9614a7638596948f704544ab8. --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/node/buffer.zig | 2 +- src/bun.js/node/dir_iterator.zig | 54 +++++++++++++++---------------- src/bun.js/node/node_fs.zig | 34 ++++++++++---------- src/bun.js/node/node_fs_constant.zig | 6 ++-- src/bun.js/node/node_os.zig | 6 ++-- src/bun.js/node/os/constants.zig | 4 +-- src/bun.js/node/syscall.zig | 30 ++++++++--------- src/bun.js/node/types.zig | 62 ++++++++++++++++++------------------ 8 files changed, 99 insertions(+), 99 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/buffer.zig b/src/bun.js/node/buffer.zig index f73498069..3a0750f05 100644 --- a/src/bun.js/node/buffer.zig +++ b/src/bun.js/node/buffer.zig @@ -50,7 +50,7 @@ pub const BufferVectorized = struct { switch (written) { 0 => {}, - 1 => @memset(buf.ptr, buf[0], buf.len), + 1 => @memset(buf, buf[0]), else => { var contents = buf[0..written]; buf = buf[written..]; diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index aa939679c..dac78e5e2 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -78,15 +78,15 @@ pub const Iterator = switch (builtin.os.tag) { } const entry_kind = switch (darwin_entry.d_type) { - os.DT.BLK => Entry.Kind.BlockDevice, - os.DT.CHR => Entry.Kind.CharacterDevice, - os.DT.DIR => Entry.Kind.Directory, - os.DT.FIFO => Entry.Kind.NamedPipe, - os.DT.LNK => Entry.Kind.SymLink, - os.DT.REG => Entry.Kind.File, - os.DT.SOCK => Entry.Kind.UnixDomainSocket, - os.DT.WHT => Entry.Kind.Whiteout, - else => Entry.Kind.Unknown, + os.DT.BLK => Entry.Kind.block_device, + os.DT.CHR => Entry.Kind.character_device, + os.DT.DIR => Entry.Kind.directory, + os.DT.FIFO => Entry.Kind.named_pipe, + os.DT.LNK => Entry.Kind.sym_link, + os.DT.REG => Entry.Kind.file, + os.DT.SOCK => Entry.Kind.unix_domain_socket, + os.DT.WHT => Entry.Kind.whiteout, + else => Entry.Kind.unknown, }; return .{ .result = IteratorResult{ @@ -134,14 +134,14 @@ pub const Iterator = switch (builtin.os.tag) { } const entry_kind = switch (linux_entry.d_type) { - linux.DT.BLK => Entry.Kind.BlockDevice, - linux.DT.CHR => Entry.Kind.CharacterDevice, - linux.DT.DIR => Entry.Kind.Directory, - linux.DT.FIFO => Entry.Kind.NamedPipe, - linux.DT.LNK => Entry.Kind.SymLink, - linux.DT.REG => Entry.Kind.File, - linux.DT.SOCK => Entry.Kind.UnixDomainSocket, - else => Entry.Kind.Unknown, + linux.DT.BLK => Entry.Kind.block_device, + linux.DT.CHR => Entry.Kind.character_device, + linux.DT.DIR => Entry.Kind.directory, + linux.DT.FIFO => Entry.Kind.named_pipe, + linux.DT.LNK => Entry.Kind.sym_link, + linux.DT.REG => Entry.Kind.file, + linux.DT.SOCK => Entry.Kind.unix_domain_socket, + else => Entry.Kind.unknown, }; return .{ .result = IteratorResult{ @@ -213,9 +213,9 @@ pub const Iterator = switch (builtin.os.tag) { const name_utf8 = self.name_data[0..name_utf8_len]; const kind = blk: { const attrs = dir_info.FileAttributes; - if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.Directory; - if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.SymLink; - break :blk Entry.Kind.File; + if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.directory; + if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.sym_link; + break :blk Entry.Kind.file; }; return .{ .result = IteratorResult{ @@ -275,13 +275,13 @@ pub const Iterator = switch (builtin.os.tag) { } const entry_kind = switch (entry.d_type) { - .BLOCK_DEVICE => Entry.Kind.BlockDevice, - .CHARACTER_DEVICE => Entry.Kind.CharacterDevice, - .DIRECTORY => Entry.Kind.Directory, - .SYMBOLIC_LINK => Entry.Kind.SymLink, - .REGULAR_FILE => Entry.Kind.File, - .SOCKET_STREAM, .SOCKET_DGRAM => Entry.Kind.UnixDomainSocket, - else => Entry.Kind.Unknown, + .BLOCK_DEVICE => Entry.Kind.block_device, + .CHARACTER_DEVICE => Entry.Kind.character_device, + .DIRECTORY => Entry.Kind.directory, + .SYMBOLIC_LINK => Entry.Kind.sym_link, + .REGULAR_FILE => Entry.Kind.file, + .SOCKET_STREAM, .SOCKET_DGRAM => Entry.Kind.unix_domain_socket, + else => Entry.Kind.unknown, }; return IteratorResult{ .name = name, diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 254d58455..3ea0822e6 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -2264,7 +2264,7 @@ pub const Arguments = struct { return CopyFile{ .src = src, .dest = dest, - .mode = @intToEnum(Constants.Copyfile, mode), + .mode = @enumFromInt(Constants.Copyfile, mode), }; } }; @@ -2498,7 +2498,7 @@ pub const NodeFS = struct { pub fn access(this: *NodeFS, args: Arguments.Access, comptime _: Flavor) Maybe(Return.Access) { var path = args.path.sliceZ(&this.sync_error_buf); - const rc = Syscall.system.access(path, @enumToInt(args.mode)); + const rc = Syscall.system.access(path, @intFromEnum(args.mode)); return Maybe(Return.Access).errnoSysP(rc, .access, path) orelse Maybe(Return.Access).success; } @@ -2528,7 +2528,7 @@ pub const NodeFS = struct { const path = path_.sliceZ(&this.sync_error_buf); switch (comptime flavor) { .sync => { - const fd = switch (Syscall.open(path, @enumToInt(FileSystemFlags.a), 0o000666)) { + const fd = switch (Syscall.open(path, @intFromEnum(FileSystemFlags.a), 0o000666)) { .result => |result| result, .err => |err| return .{ .err = err }, }; @@ -2594,7 +2594,7 @@ pub const NodeFS = struct { }; if (!os.S.ISREG(stat_.mode)) { - return Maybe(Return.CopyFile){ .err = .{ .errno = @enumToInt(C.SystemErrno.ENOTSUP) } }; + return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } }; } // 64 KB is about the break-even point for clonefile() to be worth it @@ -2723,7 +2723,7 @@ pub const NodeFS = struct { }; if (!os.S.ISREG(stat_.mode)) { - return Maybe(Return.CopyFile){ .err = .{ .errno = @enumToInt(C.SystemErrno.ENOTSUP) } }; + return Maybe(Return.CopyFile){ .err = .{ .errno = @intFromEnum(C.SystemErrno.ENOTSUP) } }; } var flags: Mode = std.os.O.CREAT | std.os.O.WRONLY; @@ -3026,7 +3026,7 @@ pub const NodeFS = struct { .err => |err| { switch (err.getErrno()) { else => { - @memcpy(&this.sync_error_buf, path.ptr, len); + @memcpy(this.sync_error_buf[0..len], path[0..len]); return .{ .err = err.withPath(this.sync_error_buf[0..len]) }; }, @@ -3043,7 +3043,7 @@ pub const NodeFS = struct { } var working_mem = &this.sync_error_buf; - @memcpy(working_mem, path.ptr, len); + @memcpy(working_mem[0..len], path[0..len]); var i: u16 = len - 1; @@ -3146,7 +3146,7 @@ pub const NodeFS = struct { const prefix_slice = args.prefix.slice(); const len = @min(prefix_slice.len, prefix_buf.len -| 7); if (len > 0) { - @memcpy(prefix_buf, prefix_slice.ptr, len); + @memcpy(prefix_buf[0..len], prefix_slice[0..len]); } prefix_buf[len..][0..6].* = "XXXXXX".*; prefix_buf[len..][6] = 0; @@ -3162,15 +3162,15 @@ pub const NodeFS = struct { }; } // std.c.getErrno(rc) returns SUCCESS if rc is null so we call std.c._errno() directly - const errno = @intToEnum(std.c.E, std.c._errno().*); - return .{ .err = Syscall.Error{ .errno = @truncate(Syscall.Error.Int, @enumToInt(errno)), .syscall = .mkdtemp } }; + const errno = @enumFromInt(std.c.E, std.c._errno().*); + return .{ .err = Syscall.Error{ .errno = @truncate(Syscall.Error.Int, @intFromEnum(errno)), .syscall = .mkdtemp } }; } pub fn open(this: *NodeFS, args: Arguments.Open, comptime flavor: Flavor) Maybe(Return.Open) { switch (comptime flavor) { // The sync version does no allocation except when returning the path .sync => { const path = args.path.sliceZ(&this.sync_error_buf); - return switch (Syscall.open(path, @enumToInt(args.flags), args.mode)) { + return switch (Syscall.open(path, @intFromEnum(args.flags), args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()), }, @@ -3605,7 +3605,7 @@ pub const NodeFS = struct { break :brk switch (Syscall.openat( args.dirfd, path, - @enumToInt(args.flag) | os.O.NOCTTY, + @intFromEnum(args.flag) | os.O.NOCTTY, args.mode, )) { .err => |err| return .{ @@ -3672,7 +3672,7 @@ pub const NodeFS = struct { } // https://github.com/oven-sh/bun/issues/2931 - if ((@enumToInt(args.flag) & std.os.O.APPEND) == 0) { + if ((@intFromEnum(args.flag) & std.os.O.APPEND) == 0) { _ = ftruncateSync(.{ .fd = fd, .len = @truncate(JSC.WebCore.Blob.SizeType, written) }); } @@ -3819,12 +3819,12 @@ pub const NodeFS = struct { while (true) { if (Maybe(Return.Rmdir).errnoSys(bun.C.darwin.removefileat(std.os.AT.FDCWD, dest, null, flags), .rmdir)) |errno| { - switch (@intToEnum(os.E, errno.err.errno)) { + switch (@enumFromInt(os.E, errno.err.errno)) { .AGAIN, .INTR => continue, .NOENT => return Maybe(Return.Rmdir).success, .MLINK => { var copy: [bun.MAX_PATH_BYTES]u8 = undefined; - @memcpy(©, dest.ptr, dest.len); + @memcpy(copy[0..dest.len], dest); copy[dest.len] = 0; var dest_copy = copy[0..dest.len :0]; switch (Syscall.unlink(dest_copy).getErrno()) { @@ -3906,7 +3906,7 @@ pub const NodeFS = struct { } if (Maybe(Return.Rm).errnoSys(bun.C.darwin.removefileat(std.os.AT.FDCWD, dest, null, flags), .unlink)) |errno| { - switch (@intToEnum(os.E, errno.err.errno)) { + switch (@enumFromInt(os.E, errno.err.errno)) { .AGAIN, .INTR => continue, .NOENT => { if (args.force) { @@ -3918,7 +3918,7 @@ pub const NodeFS = struct { .MLINK => { var copy: [bun.MAX_PATH_BYTES]u8 = undefined; - @memcpy(©, dest.ptr, dest.len); + @memcpy(copy[0..dest.len], dest); copy[dest.len] = 0; var dest_copy = copy[0..dest.len :0]; switch (Syscall.unlink(dest_copy).getErrno()) { diff --git a/src/bun.js/node/node_fs_constant.zig b/src/bun.js/node/node_fs_constant.zig index 378f332c6..8e642ebad 100644 --- a/src/bun.js/node/node_fs_constant.zig +++ b/src/bun.js/node/node_fs_constant.zig @@ -26,17 +26,17 @@ pub const Constants = struct { pub const force = 4; pub inline fn isForceClone(this: Copyfile) bool { - return (@enumToInt(this) & COPYFILE_FICLONE_FORCE) != 0; + return (@intFromEnum(this) & COPYFILE_FICLONE_FORCE) != 0; } pub inline fn shouldntOverwrite(this: Copyfile) bool { - return (@enumToInt(this) & COPYFILE_EXCL) != 0; + return (@intFromEnum(this) & COPYFILE_EXCL) != 0; } pub inline fn canUseClone(this: Copyfile) bool { _ = this; return Environment.isMac; - // return (@enumToInt(this) | COPYFILE_FICLONE) != 0; + // return (@intFromEnum(this) | COPYFILE_FICLONE) != 0; } }; diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 4b37640cf..7f9ea2e28 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -211,7 +211,7 @@ pub const Os = struct { if (local_bindings.host_processor_info(std.c.mach_host_self(), local_bindings.PROCESSOR_CPU_LOAD_INFO, &num_cpus, @ptrCast(*local_bindings.processor_info_array_t, &info), &info_size) != .SUCCESS) { return error.no_processor_info; } - defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @ptrToInt(info), info_size); + defer _ = std.c.vm_deallocate(std.c.mach_task_self(), @intFromPtr(info), info_size); // Ensure we got the amount of data we expected to guard against buffer overruns if (info_size != C.PROCESSOR_CPU_LOAD_INFO_COUNT * num_cpus) { @@ -380,7 +380,7 @@ pub const Os = struct { const err = JSC.SystemError{ .message = JSC.ZigString.init("A system error occurred: getifaddrs returned an error"), .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), - .errno = @enumToInt(std.os.errno(rc)), + .errno = @intFromEnum(std.os.errno(rc)), .syscall = JSC.ZigString.init("getifaddrs"), }; @@ -461,7 +461,7 @@ pub const Os = struct { var cidr = JSC.JSValue.null; if (maybe_suffix) |suffix| { //NOTE addr_str might not start at buf[0] due to slicing in formatIp - const start = @ptrToInt(addr_str.ptr) - @ptrToInt(&buf[0]); + const start = @intFromPtr(addr_str.ptr) - @intFromPtr(&buf[0]); // Start writing the suffix immediately after the address const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len ..], "/{}", .{suffix}) catch unreachable; // The full cidr value is the address + the suffix diff --git a/src/bun.js/node/os/constants.zig b/src/bun.js/node/os/constants.zig index a3508d383..9cf754e03 100644 --- a/src/bun.js/node/os/constants.zig +++ b/src/bun.js/node/os/constants.zig @@ -8,14 +8,14 @@ const ConstantType = enum { ERRNO, ERRNO_WIN, SIG, DLOPEN, OTHER }; fn getErrnoConstant(comptime name: []const u8) ?comptime_int { return if (@hasField(std.os.E, name)) - return @enumToInt(@field(std.os.E, name)) + return @intFromEnum(@field(std.os.E, name)) else return null; } fn getWindowsErrnoConstant(comptime name: []const u8) ?comptime_int { return if (@hasField(std.os.E, name)) - return @enumToInt(@field(std.os.windows.ws2_32.WinsockError, name)) + return @intFromEnum(@field(std.os.windows.ws2_32.WinsockError, name)) else return null; } diff --git a/src/bun.js/node/syscall.zig b/src/bun.js/node/syscall.zig index 7b10a3028..77bd5b13d 100644 --- a/src/bun.js/node/syscall.zig +++ b/src/bun.js/node/syscall.zig @@ -202,7 +202,7 @@ pub fn openat(dirfd: bun.FileDescriptor, file_path: [:0]const u8, flags: JSC.Nod .SUCCESS => .{ .result = @intCast(bun.FileDescriptor, rc) }, else => |err| .{ .err = .{ - .errno = @truncate(Syscall.Error.Int, @enumToInt(err)), + .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)), .syscall = .open, }, }, @@ -218,7 +218,7 @@ pub fn openat(dirfd: bun.FileDescriptor, file_path: [:0]const u8, flags: JSC.Nod else => |err| { return Maybe(std.os.fd_t){ .err = .{ - .errno = @truncate(Syscall.Error.Int, @enumToInt(err)), + .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)), .syscall = .open, }, }; @@ -253,14 +253,14 @@ pub fn closeAllowingStdoutAndStderr(fd: std.os.fd_t) ?Syscall.Error { if (comptime Environment.isMac) { // This avoids the EINTR problem. return switch (system.getErrno(system.@"close$NOCANCEL"(fd))) { - .BADF => Syscall.Error{ .errno = @enumToInt(os.E.BADF), .syscall = .close }, + .BADF => Syscall.Error{ .errno = @intFromEnum(os.E.BADF), .syscall = .close }, else => null, }; } if (comptime Environment.isLinux) { return switch (linux.getErrno(linux.close(fd))) { - .BADF => Syscall.Error{ .errno = @enumToInt(os.E.BADF), .syscall = .close }, + .BADF => Syscall.Error{ .errno = @intFromEnum(os.E.BADF), .syscall = .close }, else => null, }; } @@ -546,7 +546,7 @@ pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) Maybe([]u8) { .macos, .ios, .watchos, .tvos => { // On macOS, we can use F.GETPATH fcntl command to query the OS for // the path to the file descriptor. - @memset(out_buffer, 0, MAX_PATH_BYTES); + @memset(out_buffer[0..MAX_PATH_BYTES], 0); if (Maybe([]u8).errnoSys(system.fcntl(fd, os.F.GETPATH, out_buffer), .fcntl)) |err| { return err; } @@ -594,7 +594,7 @@ fn mmap( const fail = std.c.MAP.FAILED; if (rc == fail) { return Maybe([]align(mem.page_size) u8){ - .err = .{ .errno = @truncate(Syscall.Error.Int, @enumToInt(std.c.getErrno(@bitCast(i64, @ptrToInt(fail))))), .syscall = .mmap }, + .err = .{ .errno = @truncate(Syscall.Error.Int, @intFromEnum(std.c.getErrno(@bitCast(i64, @intFromPtr(fail))))), .syscall = .mmap }, }; } @@ -643,16 +643,16 @@ pub fn munmap(memory: []align(mem.page_size) const u8) Maybe(void) { pub const Error = struct { const max_errno_value = brk: { const errno_values = std.enums.values(os.E); - var err = @enumToInt(os.E.SUCCESS); + var err = @intFromEnum(os.E.SUCCESS); for (errno_values) |errn| { - err = @max(err, @enumToInt(errn)); + err = @max(err, @intFromEnum(errn)); } break :brk err; }; pub const Int: type = std.math.IntFittingRange(0, max_errno_value + 5); errno: Int, - syscall: Syscall.Tag = @intToEnum(Syscall.Tag, 0), + syscall: Syscall.Tag = @enumFromInt(Syscall.Tag, 0), path: []const u8 = "", fd: i32 = -1, @@ -661,7 +661,7 @@ pub const Error = struct { } pub fn fromCode(errno: os.E, syscall: Syscall.Tag) Error { - return .{ .errno = @truncate(Int, @enumToInt(errno)), .syscall = syscall }; + return .{ .errno = @truncate(Int, @intFromEnum(errno)), .syscall = syscall }; } pub fn format(self: Error, comptime fmt: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { @@ -672,16 +672,16 @@ pub const Error = struct { pub const retry = Error{ .errno = if (Environment.isLinux) - @intCast(Int, @enumToInt(os.E.AGAIN)) + @intCast(Int, @intFromEnum(os.E.AGAIN)) else if (Environment.isMac) - @intCast(Int, @enumToInt(os.E.WOULDBLOCK)) + @intCast(Int, @intFromEnum(os.E.WOULDBLOCK)) else - @intCast(Int, @enumToInt(os.E.INTR)), + @intCast(Int, @intFromEnum(os.E.INTR)), .syscall = .retry, }; pub inline fn getErrno(this: Error) os.E { - return @intToEnum(os.E, this.errno); + return @enumFromInt(os.E, this.errno); } pub inline fn withPath(this: Error, path: anytype) Error { @@ -726,7 +726,7 @@ pub const Error = struct { // errno label if (this.errno > 0 and this.errno < C.SystemErrno.max) { - const system_errno = @intToEnum(C.SystemErrno, this.errno); + const system_errno = @enumFromInt(C.SystemErrno, this.errno); err.code = JSC.ZigString.init(@tagName(system_errno)); if (C.SystemErrno.labels.get(system_errno)) |label| { err.message = JSC.ZigString.init(label); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 1fe378a84..e2de35706 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -135,7 +135,7 @@ pub fn Maybe(comptime ResultType: type) type { pub inline fn getErrno(this: @This()) os.E { return switch (this) { .result => os.E.SUCCESS, - .err => |err| @intToEnum(os.E, err.errno), + .err => |err| @enumFromInt(os.E, err.errno), }; } @@ -144,7 +144,7 @@ pub fn Maybe(comptime ResultType: type) type { .SUCCESS => null, else => |err| @This(){ // always truncate - .err = .{ .errno = @truncate(Syscall.Error.Int, @enumToInt(err)) }, + .err = .{ .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)) }, }, }; } @@ -154,7 +154,7 @@ pub fn Maybe(comptime ResultType: type) type { .SUCCESS => null, else => |err| @This(){ // always truncate - .err = .{ .errno = @truncate(Syscall.Error.Int, @enumToInt(err)), .syscall = syscall }, + .err = .{ .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)), .syscall = syscall }, }, }; } @@ -165,7 +165,7 @@ pub fn Maybe(comptime ResultType: type) type { else => |err| @This(){ // always truncate .err = .{ - .errno = @truncate(Syscall.Error.Int, @enumToInt(err)), + .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)), .syscall = syscall, .fd = @intCast(i32, fd), }, @@ -178,7 +178,7 @@ pub fn Maybe(comptime ResultType: type) type { .SUCCESS => null, else => |err| @This(){ // always truncate - .err = .{ .errno = @truncate(Syscall.Error.Int, @enumToInt(err)), .syscall = syscall, .path = bun.asByteSlice(path) }, + .err = .{ .errno = @truncate(Syscall.Error.Int, @intFromEnum(err)), .syscall = syscall, .path = bun.asByteSlice(path) }, }, }; } @@ -632,7 +632,7 @@ pub const PathLike = union(Tag) { } } - @memcpy(buf, sliced.ptr, sliced.len); + @memcpy(buf[0..sliced.len], sliced); buf[sliced.len] = 0; return buf[0..sliced.len :0]; } @@ -904,7 +904,7 @@ pub fn timeLikeFromJS(globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, _: JS return null; } - return @truncate(TimeLike, @floatToInt(i64, milliseconds / @as(f64, std.time.ms_per_s))); + return @truncate(TimeLike, @intFromFloat(i64, milliseconds / @as(f64, std.time.ms_per_s))); } if (!value.isNumber() and !value.isString()) { @@ -916,7 +916,7 @@ pub fn timeLikeFromJS(globalThis: *JSC.JSGlobalObject, value: JSC.JSValue, _: JS return null; } - return @truncate(TimeLike, @floatToInt(i64, seconds)); + return @truncate(TimeLike, @intFromFloat(i64, seconds)); } pub fn modeFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?Mode { @@ -968,8 +968,8 @@ pub const PathOrFileDescriptor = union(Tag) { pub fn hash(this: JSC.Node.PathOrFileDescriptor) u64 { return switch (this) { - .path => std.hash.Wyhash.hash(0, this.path.slice()), - .fd => std.hash.Wyhash.hash(0, std.mem.asBytes(&this.fd)), + .path => bun.hash(this.path.slice()), + .fd => bun.hash(std.mem.asBytes(&this.fd)), }; } @@ -1104,7 +1104,7 @@ pub const FileSystemFlags = enum(Mode) { pub fn fromJS(ctx: JSC.C.JSContextRef, val: JSC.JSValue, exception: JSC.C.ExceptionRef) ?FileSystemFlags { if (val.isNumber()) { const number = val.coerce(i32, ctx); - return @intToEnum(FileSystemFlags, @intCast(Mode, @max(number, 0))); + return @enumFromInt(FileSystemFlags, @intCast(Mode, @max(number, 0))); } const jsType = val.jsType(); @@ -1160,7 +1160,7 @@ pub const FileSystemFlags = enum(Mode) { return null; }; - return @intToEnum(FileSystemFlags, @intCast(Mode, flags)); + return @enumFromInt(FileSystemFlags, @intCast(Mode, flags)); } return null; @@ -1172,7 +1172,7 @@ pub const Date = enum(u64) { _, pub fn toJS(this: Date, ctx: JSC.C.JSContextRef, exception: JSC.C.ExceptionRef) JSC.C.JSValueRef { - const seconds = @floatCast(f64, @intToFloat(f64, @enumToInt(this)) * 1000.0); + const seconds = @floatCast(f64, @floatFromInt(f64, @intFromEnum(this)) * 1000.0); const unix_timestamp = JSC.JSValue.jsNumber(seconds); const array: [1]JSC.C.JSValueRef = .{unix_timestamp.asObjectRef()}; const obj = JSC.C.JSObjectMakeDate(ctx, 1, &array, exception); @@ -1219,12 +1219,12 @@ fn StatsDataType(comptime T: type) type { .size = @truncate(T, @intCast(i64, stat_.size)), .blksize = @truncate(T, @intCast(i64, stat_.blksize)), .blocks = @truncate(T, @intCast(i64, stat_.blocks)), - .atime_ms = (@intToFloat(f64, @max(atime.tv_sec, 0)) * std.time.ms_per_s) + (@intToFloat(f64, @intCast(usize, @max(atime.tv_nsec, 0))) / std.time.ns_per_ms), - .mtime_ms = (@intToFloat(f64, @max(mtime.tv_sec, 0)) * std.time.ms_per_s) + (@intToFloat(f64, @intCast(usize, @max(mtime.tv_nsec, 0))) / std.time.ns_per_ms), - .ctime_ms = (@intToFloat(f64, @max(ctime.tv_sec, 0)) * std.time.ms_per_s) + (@intToFloat(f64, @intCast(usize, @max(ctime.tv_nsec, 0))) / std.time.ns_per_ms), - .atime = @intToEnum(Date, @intCast(u64, @max(atime.tv_sec, 0))), - .mtime = @intToEnum(Date, @intCast(u64, @max(mtime.tv_sec, 0))), - .ctime = @intToEnum(Date, @intCast(u64, @max(ctime.tv_sec, 0))), + .atime_ms = (@floatFromInt(f64, @max(atime.tv_sec, 0)) * std.time.ms_per_s) + (@floatFromInt(f64, @intCast(usize, @max(atime.tv_nsec, 0))) / std.time.ns_per_ms), + .mtime_ms = (@floatFromInt(f64, @max(mtime.tv_sec, 0)) * std.time.ms_per_s) + (@floatFromInt(f64, @intCast(usize, @max(mtime.tv_nsec, 0))) / std.time.ns_per_ms), + .ctime_ms = (@floatFromInt(f64, @max(ctime.tv_sec, 0)) * std.time.ms_per_s) + (@floatFromInt(f64, @intCast(usize, @max(ctime.tv_nsec, 0))) / std.time.ns_per_ms), + .atime = @enumFromInt(Date, @intCast(u64, @max(atime.tv_sec, 0))), + .mtime = @enumFromInt(Date, @intCast(u64, @max(mtime.tv_sec, 0))), + .ctime = @enumFromInt(Date, @intCast(u64, @max(ctime.tv_sec, 0))), // Linux doesn't include this info in stat // maybe it does in statx, but do you really need birthtime? If you do please file an issue. @@ -1234,9 +1234,9 @@ fn StatsDataType(comptime T: type) type { @truncate(T, @intCast(i64, if (stat_.birthtime().tv_nsec > 0) (@intCast(usize, stat_.birthtime().tv_nsec) / std.time.ns_per_ms) else 0)), .birthtime = if (Environment.isLinux) - @intToEnum(Date, 0) + @enumFromInt(Date, 0) else - @intToEnum(Date, @intCast(u64, @max(stat_.birthtime().tv_sec, 0))), + @enumFromInt(Date, @intCast(u64, @max(stat_.birthtime().tv_sec, 0))), }; } }; @@ -1426,49 +1426,49 @@ pub const Dirent = struct { _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.BlockDevice); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.block_device); } pub fn isCharacterDevice( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.CharacterDevice); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.character_device); } pub fn isDirectory( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.Directory); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.directory); } pub fn isFIFO( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.NamedPipe or this.kind == std.fs.File.Kind.EventPort); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.named_pipe or this.kind == std.fs.File.Kind.event_port); } pub fn isFile( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.File); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.file); } pub fn isSocket( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.UnixDomainSocket); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.unix_domain_socket); } pub fn isSymbolicLink( this: *Dirent, _: *JSC.JSGlobalObject, _: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.SymLink); + return JSC.JSValue.jsBoolean(this.kind == std.fs.File.Kind.sym_link); } pub fn finalize(this: *Dirent) callconv(.C) void { @@ -1490,14 +1490,14 @@ pub const Emitter = struct { pub fn append(this: *List, allocator: std.mem.Allocator, ctx: JSC.C.JSContextRef, listener: Listener) !void { JSC.C.JSValueProtect(ctx, listener.callback.asObjectRef()); try this.list.append(allocator, listener); - this.once_count +|= @as(u32, @boolToInt(listener.once)); + this.once_count +|= @as(u32, @intFromBool(listener.once)); } pub fn prepend(this: *List, allocator: std.mem.Allocator, ctx: JSC.C.JSContextRef, listener: Listener) !void { JSC.C.JSValueProtect(ctx, listener.callback.asObjectRef()); try this.list.ensureUnusedCapacity(allocator, 1); this.list.insertAssumeCapacity(0, listener); - this.once_count +|= @as(u32, @boolToInt(listener.once)); + this.once_count +|= @as(u32, @intFromBool(listener.once)); } // removeListener() will remove, at most, one instance of a listener from the @@ -1510,7 +1510,7 @@ pub const Emitter = struct { for (callbacks, 0..) |item, i| { if (callback.eqlValue(item)) { JSC.C.JSValueUnprotect(ctx, callback.asObjectRef()); - this.once_count -|= @as(u32, @boolToInt(this.list.items(.once)[i])); + this.once_count -|= @as(u32, @intFromBool(this.list.items(.once)[i])); this.list.orderedRemove(i); return true; } -- cgit v1.2.3 From 069b42a7cc1275969859dc60e7c303528ca2dccb Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sat, 24 Jun 2023 03:24:34 -0300 Subject: [feat] fs.watch (#3249) * initial support * add types * fix comment * fix types * bigfix up * more fixes * fix some encoding support for watch * fix rename event * fixup * fix latin1 * add fs_events, still failing some tests * fixuup * remove unecesary check * readd tests ops * this is necessary? just testing CI/CD weird errors * just use dupe here * cleanup and fix deinit * fix zig upgrade --- packages/bun-types/fs.d.ts | 97 +++ packages/bun-types/fs/promises.d.ts | 58 ++ src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- src/bun.js/bindings/JSSinkLookupTable.h | 2 +- .../ZigGeneratedClasses+DOMClientIsoSubspaces.h | 1 + .../bindings/ZigGeneratedClasses+DOMIsoSubspaces.h | 1 + .../ZigGeneratedClasses+lazyStructureHeader.h | 6 + .../ZigGeneratedClasses+lazyStructureImpl.h | 7 + src/bun.js/bindings/ZigGeneratedClasses.cpp | 322 ++++++++ src/bun.js/bindings/ZigGeneratedClasses.h | 56 ++ src/bun.js/bindings/generated_classes.zig | 94 +++ src/bun.js/bindings/generated_classes_list.zig | 1 + src/bun.js/event_loop.zig | 7 + src/bun.js/javascript.zig | 7 + src/bun.js/node/fs_events.zig | 609 ++++++++++++++ src/bun.js/node/node.classes.ts | 30 +- src/bun.js/node/node_fs.zig | 13 +- src/bun.js/node/node_fs_binding.zig | 2 + src/bun.js/node/node_fs_watcher.zig | 913 +++++++++++++++++++++ src/bun.js/node/types.zig | 4 + src/bun.js/webcore/encoding.zig | 15 +- src/fs.zig | 54 ++ src/http.zig | 7 +- src/js/node/fs.js | 65 +- src/js/node/fs.promises.ts | 51 ++ src/js/out/modules/node/fs.js | 50 +- src/js/out/modules/node/fs.promises.js | 2 +- src/js/private.d.ts | 86 +- src/jsc.zig | 1 + src/watcher.zig | 68 +- test/js/node/watch/fixtures/close.js | 7 + test/js/node/watch/fixtures/persistent.js | 5 + test/js/node/watch/fixtures/relative.js | 23 + test/js/node/watch/fixtures/unref.js | 7 + test/js/node/watch/fs.watch.test.js | 424 ++++++++++ 36 files changed, 3072 insertions(+), 27 deletions(-) create mode 100644 src/bun.js/node/fs_events.zig create mode 100644 src/bun.js/node/node_fs_watcher.zig create mode 100644 test/js/node/watch/fixtures/close.js create mode 100644 test/js/node/watch/fixtures/persistent.js create mode 100644 test/js/node/watch/fixtures/relative.js create mode 100644 test/js/node/watch/fixtures/unref.js create mode 100644 test/js/node/watch/fs.watch.test.js (limited to 'src/bun.js/node') diff --git a/packages/bun-types/fs.d.ts b/packages/bun-types/fs.d.ts index 14c5c1d1d..5dfb2c7f2 100644 --- a/packages/bun-types/fs.d.ts +++ b/packages/bun-types/fs.d.ts @@ -19,6 +19,7 @@ */ declare module "fs" { import * as stream from "stream"; + import type EventEmitter from "events"; import type { SystemError, ArrayBufferView } from "bun"; interface ObjectEncodingOptions { encoding?: BufferEncoding | null | undefined; @@ -3929,6 +3930,102 @@ declare module "fs" { */ recursive?: boolean; } + + export interface FSWatcher extends EventEmitter { + /** + * Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the `fs.FSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; + + /** + * events.EventEmitter + * 1. change + * 2. error + */ + addListener(event: string, listener: (...args: any[]) => void): this; + addListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + addListener(event: 'error', listener: (error: Error) => void): this; + addListener(event: 'close', listener: () => void): this; + on(event: string, listener: (...args: any[]) => void): this; + on(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'close', listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + once(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: 'error', listener: (error: Error) => void): this; + once(event: 'close', listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependListener(event: 'error', listener: (error: Error) => void): this; + prependListener(event: 'close', listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: 'change', listener: (eventType: string, filename: string | Buffer) => void): this; + prependOnceListener(event: 'error', listener: (error: Error) => void): this; + prependOnceListener(event: 'close', listener: () => void): this; + } + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'` or `'change'`, and `filename` is the name of the file + * which triggered the event. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * The listener callback is attached to the `'change'` event fired by `fs.FSWatcher`, but it is not the same thing as the `'change'` value of`eventType`. + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `fs.FSWatcher`. + * @since v0.6.8 + * @param listener + */ + export function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer', + listener?: WatchListener + ): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options?: WatchOptions | BufferEncoding | null, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + export function watch(filename: PathLike, options: WatchOptions | string, listener?: WatchListener): FSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + export function watch(filename: PathLike, listener?: WatchListener): FSWatcher; } declare module "node:fs" { diff --git a/packages/bun-types/fs/promises.d.ts b/packages/bun-types/fs/promises.d.ts index 0d71464b9..2b908fceb 100644 --- a/packages/bun-types/fs/promises.d.ts +++ b/packages/bun-types/fs/promises.d.ts @@ -26,6 +26,7 @@ declare module "fs/promises" { Abortable, RmOptions, RmDirOptions, + WatchOptions, } from "node:fs"; const constants: typeof import("node:fs")["constants"]; @@ -709,6 +710,63 @@ declare module "fs/promises" { * To remove a directory recursively, use `fs.promises.rm()` instead, with the `recursive` option set to `true`. */ function rmdir(path: PathLike, options?: RmDirOptions): Promise; + + /** + * Returns an async iterator that watches for changes on `filename`, where `filename`is either a file or a directory. + * + * ```js + * const { watch } = require('node:fs/promises'); + * + * const ac = new AbortController(); + * const { signal } = ac; + * setTimeout(() => ac.abort(), 10000); + * + * (async () => { + * try { + * const watcher = watch(__filename, { signal }); + * for await (const event of watcher) + * console.log(event); + * } catch (err) { + * if (err.name === 'AbortError') + * return; + * throw err; + * } + * })(); + * ``` + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * All the `caveats` for `fs.watch()` also apply to `fsPromises.watch()`. + * @since v0.6.8 + * @return of objects with the properties: + */ + function watch( + filename: PathLike, + options: + | (WatchOptions & { + encoding: 'buffer'; + }) + | 'buffer' + ): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options?: WatchOptions | BufferEncoding): AsyncIterable>; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `FSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + function watch(filename: PathLike, options: WatchOptions | string): AsyncIterable> | AsyncIterable>; } declare module "node:fs/promises" { diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index 36be334dd..4acf01ff7 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.447Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 5bbfab777..37c458e9b 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-05-18T01:04:00.446Z +// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z // #pragma once diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h index a4ace6dc3..e4ed81629 100644 --- a/src/bun.js/bindings/JSSinkLookupTable.h +++ b/src/bun.js/bindings/JSSinkLookupTable.h @@ -1,4 +1,4 @@ -// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/jarred/Code/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! +// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h index b16febcdb..f0d491c0b 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_clientSubspaceForExpectConstructor;std: std::unique_ptr m_clientSubspaceForExpectAnything; std::unique_ptr m_clientSubspaceForExpectStringContaining; std::unique_ptr m_clientSubspaceForExpectStringMatching; +std::unique_ptr m_clientSubspaceForFSWatcher; std::unique_ptr m_clientSubspaceForFileSystemRouter; std::unique_ptr m_clientSubspaceForFileSystemRouterConstructor;std::unique_ptr m_clientSubspaceForListener; std::unique_ptr m_clientSubspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h index 59263e62c..02a9adbca 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h @@ -8,6 +8,7 @@ std::unique_ptr m_subspaceForExpectConstructor;std::unique_ptr m_subspaceForExpectAnything; std::unique_ptr m_subspaceForExpectStringContaining; std::unique_ptr m_subspaceForExpectStringMatching; +std::unique_ptr m_subspaceForFSWatcher; std::unique_ptr m_subspaceForFileSystemRouter; std::unique_ptr m_subspaceForFileSystemRouterConstructor;std::unique_ptr m_subspaceForListener; std::unique_ptr m_subspaceForMD4; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h index 4471fbab3..ac03032e6 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h @@ -58,6 +58,12 @@ JSC::Structure* JSExpectStringMatchingStructure() { return m_JSExpectStringMatch JSC::LazyClassStructure m_JSExpectStringMatching; bool hasJSExpectStringMatchingSetterValue { false }; mutable JSC::WriteBarrier m_JSExpectStringMatchingSetterValue; +JSC::Structure* JSFSWatcherStructure() { return m_JSFSWatcher.getInitializedOnMainThread(this); } + JSC::JSObject* JSFSWatcherConstructor() { return m_JSFSWatcher.constructorInitializedOnMainThread(this); } + JSC::JSValue JSFSWatcherPrototype() { return m_JSFSWatcher.prototypeInitializedOnMainThread(this); } + JSC::LazyClassStructure m_JSFSWatcher; + bool hasJSFSWatcherSetterValue { false }; + mutable JSC::WriteBarrier m_JSFSWatcherSetterValue; JSC::Structure* JSFileSystemRouterStructure() { return m_JSFileSystemRouter.getInitializedOnMainThread(this); } JSC::JSObject* JSFileSystemRouterConstructor() { return m_JSFileSystemRouter.constructorInitializedOnMainThread(this); } JSC::JSValue JSFileSystemRouterPrototype() { return m_JSFileSystemRouter.prototypeInitializedOnMainThread(this); } diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h index 4e5a2c1fa..b3b5327a4 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h @@ -58,6 +58,12 @@ void GlobalObject::initGeneratedLazyClasses() { init.setPrototype(WebCore::JSExpectStringMatching::createPrototype(init.vm, reinterpret_cast(init.global))); init.setStructure(WebCore::JSExpectStringMatching::createStructure(init.vm, init.global, init.prototype)); + }); + m_JSFSWatcher.initLater( + [](LazyClassStructure::Initializer& init) { + init.setPrototype(WebCore::JSFSWatcher::createPrototype(init.vm, reinterpret_cast(init.global))); + init.setStructure(WebCore::JSFSWatcher::createStructure(init.vm, init.global, init.prototype)); + }); m_JSFileSystemRouter.initLater( [](LazyClassStructure::Initializer& init) { @@ -211,6 +217,7 @@ void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& thisObject->m_JSExpectAnything.visit(visitor); visitor.append(thisObject->m_JSExpectAnythingSetterValue); thisObject->m_JSExpectStringContaining.visit(visitor); visitor.append(thisObject->m_JSExpectStringContainingSetterValue); thisObject->m_JSExpectStringMatching.visit(visitor); visitor.append(thisObject->m_JSExpectStringMatchingSetterValue); + thisObject->m_JSFSWatcher.visit(visitor); visitor.append(thisObject->m_JSFSWatcherSetterValue); thisObject->m_JSFileSystemRouter.visit(visitor); visitor.append(thisObject->m_JSFileSystemRouterSetterValue); thisObject->m_JSListener.visit(visitor); visitor.append(thisObject->m_JSListenerSetterValue); thisObject->m_JSMD4.visit(visitor); visitor.append(thisObject->m_JSMD4SetterValue); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index d51a1959a..e0a3f33d6 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -5381,6 +5381,297 @@ void JSExpectStringMatching::visitOutputConstraintsImpl(JSCell* cell, Visitor& v } DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSExpectStringMatching); +class JSFSWatcherPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + + static JSFSWatcherPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSFSWatcherPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcherPrototype(vm, globalObject, structure); + ptr->finishCreation(vm, globalObject); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSFSWatcherPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); +}; + +extern "C" void FSWatcherClass__finalize(void*); + +extern "C" EncodedJSValue FSWatcherPrototype__doClose(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__closeCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__hasRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__refCallback); + +extern "C" EncodedJSValue FSWatcherPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback); + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSFSWatcherPrototype, JSFSWatcherPrototype::Base); + +static const HashTableValue JSFSWatcherPrototypeTableValues[] = { + { "close"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__closeCallback, 0 } }, + { "hasRef"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__hasRefCallback, 0 } }, + { "ref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__refCallback, 0 } }, + { "unref"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, FSWatcherPrototype__unrefCallback, 0 } } +}; + +const ClassInfo JSFSWatcherPrototype::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcherPrototype) }; + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__closeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doClose(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__hasRefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__hasRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__refCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(FSWatcherPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSFSWatcher* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return FSWatcherPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +extern "C" void FSWatcherPrototype__listenerSetCachedValue(JSC::EncodedJSValue thisValue, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value) +{ + auto& vm = globalObject->vm(); + auto* thisObject = jsCast(JSValue::decode(thisValue)); + thisObject->m_listener.set(vm, thisObject, JSValue::decode(value)); +} + +extern "C" EncodedJSValue FSWatcherPrototype__listenerGetCachedValue(JSC::EncodedJSValue thisValue) +{ + auto* thisObject = jsCast(JSValue::decode(thisValue)); + return JSValue::encode(thisObject->m_listener.get()); +} + +void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSFSWatcher::info(), JSFSWatcherPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSFSWatcher::~JSFSWatcher() +{ + if (m_ctx) { + FSWatcherClass__finalize(m_ctx); + } +} +void JSFSWatcher::destroy(JSCell* cell) +{ + static_cast(cell)->JSFSWatcher::~JSFSWatcher(); +} + +const ClassInfo JSFSWatcher::s_info = { "FSWatcher"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFSWatcher) }; + +void JSFSWatcher::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSFSWatcher* JSFSWatcher::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) +{ + JSFSWatcher* ptr = new (NotNull, JSC::allocateCell(vm)) JSFSWatcher(vm, structure, ctx); + ptr->finishCreation(vm); + return ptr; +} + +extern "C" void* FSWatcher__fromJS(JSC::EncodedJSValue value) +{ + JSC::JSValue decodedValue = JSC::JSValue::decode(value); + if (decodedValue.isEmpty() || !decodedValue.isCell()) + return nullptr; + + JSC::JSCell* cell = decodedValue.asCell(); + JSFSWatcher* object = JSC::jsDynamicCast(cell); + + if (!object) + return nullptr; + + return object->wrapped(); +} + +extern "C" bool FSWatcher__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr) +{ + JSFSWatcher* object = JSC::jsDynamicCast(JSValue::decode(value)); + if (!object) + return false; + + object->m_ctx = ptr; + return true; +} + +extern "C" const size_t FSWatcher__ptrOffset = JSFSWatcher::offsetOfWrapped(); + +void JSFSWatcher::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + if (void* wrapped = thisObject->wrapped()) { + // if (thisObject->scriptExecutionContext()) + // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string()); + } + Base::analyzeHeap(cell, analyzer); +} + +JSObject* JSFSWatcher::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) +{ + return JSFSWatcherPrototype::create(vm, globalObject, JSFSWatcherPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +extern "C" EncodedJSValue FSWatcher__create(Zig::GlobalObject* globalObject, void* ptr) +{ + auto& vm = globalObject->vm(); + JSC::Structure* structure = globalObject->JSFSWatcherStructure(); + JSFSWatcher* instance = JSFSWatcher::create(vm, globalObject, structure, ptr); + + return JSValue::encode(instance); +} + +template +void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitAdditionalChildren(Visitor& visitor) +{ + JSFSWatcher* thisObject = this; + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + visitor.append(thisObject->m_listener); +} + +DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher); + +template +void JSFSWatcher::visitOutputConstraintsImpl(JSCell* cell, Visitor& visitor) +{ + JSFSWatcher* thisObject = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + thisObject->visitAdditionalChildren(visitor); +} + +DEFINE_VISIT_OUTPUT_CONSTRAINTS(JSFSWatcher); class JSFileSystemRouterPrototype final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; @@ -7654,6 +7945,9 @@ JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesCallback); extern "C" EncodedJSValue NodeJSFSPrototype__utimesSync(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback); +extern "C" EncodedJSValue NodeJSFSPrototype__watch(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback); + extern "C" EncodedJSValue NodeJSFSPrototype__write(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback); @@ -7751,6 +8045,7 @@ static const HashTableValue JSNodeJSFSPrototypeTableValues[] = { { "unlinkSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__unlinkSyncCallback, 1 } }, { "utimes"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesCallback, 4 } }, { "utimesSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__utimesSyncCallback, 3 } }, + { "watch"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__watchCallback, 3 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeCallback, 6 } }, { "writeFile"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileCallback, 4 } }, { "writeFileSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, NodeJSFSPrototype__writeFileSyncCallback, 3 } }, @@ -9795,6 +10090,33 @@ JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__utimesSyncCallback, (JSGlobalObject return NodeJSFSPrototype__utimesSync(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__watchCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSNodeJSFS* thisObject = jsDynamicCast(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return NodeJSFSPrototype__watch(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(NodeJSFSPrototype__writeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index 668cd3f6b..3fa0e26d2 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -578,6 +578,62 @@ public: mutable JSC::WriteBarrier m_testValue; }; +class JSFSWatcher final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static JSFSWatcher* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); + + DECLARE_EXPORT_INFO; + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForFSWatcher = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForFSWatcher.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForFSWatcher = std::forward(space); }); + } + + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(0b11101110), StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); + ; + + ~JSFSWatcher(); + + void* wrapped() const { return m_ctx; } + + void detach() + { + m_ctx = nullptr; + } + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSFSWatcher, m_ctx); } + + void* m_ctx { nullptr }; + + JSFSWatcher(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + : Base(vm, structure) + { + m_ctx = sinkPtr; + } + + void finishCreation(JSC::VM&); + + DECLARE_VISIT_CHILDREN; + template void visitAdditionalChildren(Visitor&); + DECLARE_VISIT_OUTPUT_CONSTRAINTS; + + mutable JSC::WriteBarrier m_listener; +}; + class JSFileSystemRouter final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 0ec65a469..74e30cd83 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -1406,6 +1406,96 @@ pub const JSExpectStringMatching = struct { } } }; +pub const JSFSWatcher = struct { + const FSWatcher = Classes.FSWatcher; + const GetterType = fn (*FSWatcher, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const GetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const SetterType = fn (*FSWatcher, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const SetterTypeWithThisValue = fn (*FSWatcher, JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const CallbackType = fn (*FSWatcher, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue; + + /// Return the pointer to the wrapped object. + /// If the object does not match the type, return null. + pub fn fromJS(value: JSC.JSValue) ?*FSWatcher { + JSC.markBinding(@src()); + return FSWatcher__fromJS(value); + } + + extern fn FSWatcherPrototype__listenerSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; + + extern fn FSWatcherPrototype__listenerGetCachedValue(JSC.JSValue) JSC.JSValue; + + /// `FSWatcher.listener` setter + /// This value will be visited by the garbage collector. + pub fn listenerSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { + JSC.markBinding(@src()); + FSWatcherPrototype__listenerSetCachedValue(thisValue, globalObject, value); + } + + /// `FSWatcher.listener` getter + /// This value will be visited by the garbage collector. + pub fn listenerGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { + JSC.markBinding(@src()); + const result = FSWatcherPrototype__listenerGetCachedValue(thisValue); + if (result == .zero) + return null; + + return result; + } + + /// Create a new instance of FSWatcher + pub fn toJS(this: *FSWatcher, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(@src()); + if (comptime Environment.allow_assert) { + const value__ = FSWatcher__create(globalObject, this); + std.debug.assert(value__.as(FSWatcher).? == this); // If this fails, likely a C ABI issue. + return value__; + } else { + return FSWatcher__create(globalObject, this); + } + } + + /// Modify the internal ptr to point to a new instance of FSWatcher. + pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*FSWatcher) bool { + JSC.markBinding(@src()); + return FSWatcher__dangerouslySetPtr(value, ptr); + } + + /// Detach the ptr from the thisValue + pub fn detachPtr(_: *FSWatcher, value: JSC.JSValue) void { + JSC.markBinding(@src()); + std.debug.assert(FSWatcher__dangerouslySetPtr(value, null)); + } + + extern fn FSWatcher__fromJS(JSC.JSValue) ?*FSWatcher; + extern fn FSWatcher__getConstructor(*JSC.JSGlobalObject) JSC.JSValue; + + extern fn FSWatcher__create(globalObject: *JSC.JSGlobalObject, ptr: ?*FSWatcher) JSC.JSValue; + + extern fn FSWatcher__dangerouslySetPtr(JSC.JSValue, ?*FSWatcher) bool; + + comptime { + if (@TypeOf(FSWatcher.finalize) != (fn (*FSWatcher) callconv(.C) void)) { + @compileLog("FSWatcher.finalize is not a finalizer"); + } + + if (@TypeOf(FSWatcher.doClose) != CallbackType) + @compileLog("Expected FSWatcher.doClose to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doClose))); + if (@TypeOf(FSWatcher.hasRef) != CallbackType) + @compileLog("Expected FSWatcher.hasRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.hasRef))); + if (@TypeOf(FSWatcher.doRef) != CallbackType) + @compileLog("Expected FSWatcher.doRef to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doRef))); + if (@TypeOf(FSWatcher.doUnref) != CallbackType) + @compileLog("Expected FSWatcher.doUnref to be a callback but received " ++ @typeName(@TypeOf(FSWatcher.doUnref))); + if (!JSC.is_bindgen) { + @export(FSWatcher.doClose, .{ .name = "FSWatcherPrototype__doClose" }); + @export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" }); + @export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" }); + @export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" }); + @export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" }); + } + } +}; pub const JSFileSystemRouter = struct { const FileSystemRouter = Classes.FileSystemRouter; const GetterType = fn (*FileSystemRouter, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; @@ -2312,6 +2402,8 @@ pub const JSNodeJSFS = struct { @compileLog("Expected NodeJSFS.utimes to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimes))); if (@TypeOf(NodeJSFS.utimesSync) != CallbackType) @compileLog("Expected NodeJSFS.utimesSync to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.utimesSync))); + if (@TypeOf(NodeJSFS.watch) != CallbackType) + @compileLog("Expected NodeJSFS.watch to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.watch))); if (@TypeOf(NodeJSFS.write) != CallbackType) @compileLog("Expected NodeJSFS.write to be a callback but received " ++ @typeName(@TypeOf(NodeJSFS.write))); if (@TypeOf(NodeJSFS.writeFile) != CallbackType) @@ -2402,6 +2494,7 @@ pub const JSNodeJSFS = struct { @export(NodeJSFS.unlinkSync, .{ .name = "NodeJSFSPrototype__unlinkSync" }); @export(NodeJSFS.utimes, .{ .name = "NodeJSFSPrototype__utimes" }); @export(NodeJSFS.utimesSync, .{ .name = "NodeJSFSPrototype__utimesSync" }); + @export(NodeJSFS.watch, .{ .name = "NodeJSFSPrototype__watch" }); @export(NodeJSFS.write, .{ .name = "NodeJSFSPrototype__write" }); @export(NodeJSFS.writeFile, .{ .name = "NodeJSFSPrototype__writeFile" }); @export(NodeJSFS.writeFileSync, .{ .name = "NodeJSFSPrototype__writeFileSync" }); @@ -4855,6 +4948,7 @@ comptime { _ = JSExpectAnything; _ = JSExpectStringContaining; _ = JSExpectStringMatching; + _ = JSFSWatcher; _ = JSFileSystemRouter; _ = JSListener; _ = JSMD4; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index c54965093..d90267337 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -37,4 +37,5 @@ pub const Classes = struct { pub const BuildArtifact = JSC.API.BuildArtifact; pub const BuildMessage = JSC.BuildMessage; pub const ResolveMessage = JSC.ResolveMessage; + pub const FSWatcher = JSC.Node.FSWatcher.JSObject; }; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 0a3459d64..a3ccd16ad 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -224,6 +224,7 @@ pub const CppTask = opaque { const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const MicrotaskForDefaultGlobalObject = JSC.MicrotaskForDefaultGlobalObject; const HotReloadTask = JSC.HotReloader.HotReloadTask; +const FSWatchTask = JSC.Node.FSWatcher.FSWatchTask; const PollPendingModulesTask = JSC.ModuleLoader.AsyncModule.Queue; // const PromiseTask = JSInternalPromise.Completion.PromiseTask; const GetAddrInfoRequestTask = JSC.DNS.GetAddrInfoRequest.Task; @@ -242,6 +243,7 @@ pub const Task = TaggedPointerUnion(.{ HotReloadTask, PollPendingModulesTask, GetAddrInfoRequestTask, + FSWatchTask, // PromiseTask, // TimeoutTasklet, }); @@ -467,6 +469,11 @@ pub const EventLoop = struct { // special case: we return return 0; }, + .FSWatchTask => { + var transform_task: *FSWatchTask = task.get(FSWatchTask).?; + transform_task.*.run(); + transform_task.deinit(); + }, @field(Task.Tag, typeBaseName(@typeName(AnyTask))) => { var any: *AnyTask = task.get(AnyTask).?; any.run(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index bebfbeb18..3baa25e22 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -2609,6 +2609,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime return this.tombstones.get(key); } + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } + pub fn onFileUpdate( this: *@This(), events: []watcher.WatchEvent, diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig new file mode 100644 index 000000000..a3fba5441 --- /dev/null +++ b/src/bun.js/node/fs_events.zig @@ -0,0 +1,609 @@ +const std = @import("std"); +const bun = @import("root").bun; +const Environment = bun.Environment; +const Mutex = @import("../../lock.zig").Lock; +const sync = @import("../../sync.zig"); +const Semaphore = sync.Semaphore; +const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; +const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion; +const string = bun.string; + +pub const CFAbsoluteTime = f64; +pub const CFTimeInterval = f64; +pub const CFArrayCallBacks = anyopaque; + +pub const FSEventStreamEventFlags = c_int; +pub const OSStatus = c_int; +pub const CFIndex = c_long; + +pub const FSEventStreamCreateFlags = u32; +pub const FSEventStreamEventId = u64; + +pub const CFStringEncoding = c_uint; + +pub const CFArrayRef = ?*anyopaque; +pub const CFAllocatorRef = ?*anyopaque; +pub const CFBundleRef = ?*anyopaque; +pub const CFDictionaryRef = ?*anyopaque; +pub const CFRunLoopRef = ?*anyopaque; +pub const CFRunLoopSourceRef = ?*anyopaque; +pub const CFStringRef = ?*anyopaque; +pub const CFTypeRef = ?*anyopaque; +pub const FSEventStreamRef = ?*anyopaque; +pub const FSEventStreamCallback = *const fn (FSEventStreamRef, ?*anyopaque, usize, ?*anyopaque, *FSEventStreamEventFlags, *FSEventStreamEventId) callconv(.C) void; + +// we only care about info and perform +pub const CFRunLoopSourceContext = extern struct { + version: CFIndex = 0, + info: *anyopaque, + retain: ?*anyopaque = null, + release: ?*anyopaque = null, + copyDescription: ?*anyopaque = null, + equal: ?*anyopaque = null, + hash: ?*anyopaque = null, + schedule: ?*anyopaque = null, + cancel: ?*anyopaque = null, + perform: *const fn (?*anyopaque) callconv(.C) void, +}; + +pub const FSEventStreamContext = extern struct { + version: CFIndex = 0, + info: ?*anyopaque = null, + pad: [3]?*anyopaque = .{ null, null, null }, +}; + +pub const kCFStringEncodingUTF8: CFStringEncoding = 0x8000100; +pub const noErr: OSStatus = 0; + +pub const kFSEventStreamCreateFlagNoDefer: c_int = 2; +pub const kFSEventStreamCreateFlagFileEvents: c_int = 16; + +pub const kFSEventStreamEventFlagEventIdsWrapped: c_int = 8; +pub const kFSEventStreamEventFlagHistoryDone: c_int = 16; +pub const kFSEventStreamEventFlagItemChangeOwner: c_int = 0x4000; +pub const kFSEventStreamEventFlagItemCreated: c_int = 0x100; +pub const kFSEventStreamEventFlagItemFinderInfoMod: c_int = 0x2000; +pub const kFSEventStreamEventFlagItemInodeMetaMod: c_int = 0x400; +pub const kFSEventStreamEventFlagItemIsDir: c_int = 0x20000; +pub const kFSEventStreamEventFlagItemModified: c_int = 0x1000; +pub const kFSEventStreamEventFlagItemRemoved: c_int = 0x200; +pub const kFSEventStreamEventFlagItemRenamed: c_int = 0x800; +pub const kFSEventStreamEventFlagItemXattrMod: c_int = 0x8000; +pub const kFSEventStreamEventFlagKernelDropped: c_int = 4; +pub const kFSEventStreamEventFlagMount: c_int = 64; +pub const kFSEventStreamEventFlagRootChanged: c_int = 32; +pub const kFSEventStreamEventFlagUnmount: c_int = 128; +pub const kFSEventStreamEventFlagUserDropped: c_int = 2; + +// Lazy function call binding. +const RTLD_LAZY = 0x1; +// Symbols exported from this image (dynamic library or bundle) +// are generally hidden and only availble to dlsym() when +// directly using the handle returned by this call to dlopen(). +const RTLD_LOCAL = 0x4; + +pub const kFSEventsModified: c_int = + kFSEventStreamEventFlagItemChangeOwner | + kFSEventStreamEventFlagItemFinderInfoMod | + kFSEventStreamEventFlagItemInodeMetaMod | + kFSEventStreamEventFlagItemModified | + kFSEventStreamEventFlagItemXattrMod; + +pub const kFSEventsRenamed: c_int = + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed; + +pub const kFSEventsSystem: c_int = + kFSEventStreamEventFlagUserDropped | + kFSEventStreamEventFlagKernelDropped | + kFSEventStreamEventFlagEventIdsWrapped | + kFSEventStreamEventFlagHistoryDone | + kFSEventStreamEventFlagMount | + kFSEventStreamEventFlagUnmount | + kFSEventStreamEventFlagRootChanged; + +var fsevents_mutex: Mutex = Mutex.init(); +var fsevents_default_loop_mutex: Mutex = Mutex.init(); +var fsevents_default_loop: ?*FSEventsLoop = null; + +fn dlsym(handle: ?*anyopaque, comptime Type: type, comptime symbol: [:0]const u8) ?Type { + if (std.c.dlsym(handle, symbol)) |ptr| { + return bun.cast(Type, ptr); + } + return null; +} + +pub const CoreFoundation = struct { + handle: ?*anyopaque, + ArrayCreate: *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, + Release: *fn (CFTypeRef) callconv(.C) void, + + RunLoopAddSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopGetCurrent: *fn () callconv(.C) CFRunLoopRef, + RunLoopRemoveSource: *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, + RunLoopRun: *fn () callconv(.C) void, + RunLoopSourceCreate: *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, + RunLoopSourceSignal: *fn (CFRunLoopSourceRef) callconv(.C) void, + RunLoopStop: *fn (CFRunLoopRef) callconv(.C) void, + RunLoopWakeUp: *fn (CFRunLoopRef) callconv(.C) void, + StringCreateWithFileSystemRepresentation: *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, + RunLoopDefaultMode: *CFStringRef, + + pub fn get() CoreFoundation { + if (fsevents_cf) |cf| return cf; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cf) |cf| return cf; + + InitLibrary(); + + return fsevents_cf.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreFoundation) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +pub const CoreServices = struct { + handle: ?*anyopaque, + FSEventStreamCreate: *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, + FSEventStreamInvalidate: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamRelease: *fn (FSEventStreamRef) callconv(.C) void, + FSEventStreamScheduleWithRunLoop: *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, + FSEventStreamStart: *fn (FSEventStreamRef) callconv(.C) c_int, + FSEventStreamStop: *fn (FSEventStreamRef) callconv(.C) void, + // libuv set it to -1 so the actual value is this + kFSEventStreamEventIdSinceNow: FSEventStreamEventId = 18446744073709551615, + + pub fn get() CoreServices { + if (fsevents_cs) |cs| return cs; + fsevents_mutex.lock(); + defer fsevents_mutex.unlock(); + if (fsevents_cs) |cs| return cs; + + InitLibrary(); + + return fsevents_cs.?; + } + + // We Actually never deinit it + // pub fn deinit(this: *CoreServices) void { + // if(this.handle) | ptr| { + // this.handle = null; + // _ = std.c.dlclose(this.handle); + // } + // } + +}; + +var fsevents_cf: ?CoreFoundation = null; +var fsevents_cs: ?CoreServices = null; + +fn InitLibrary() void { + const fsevents_cf_handle = std.c.dlopen("/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cf_handle == null) @panic("Cannot Load CoreFoundation"); + + fsevents_cf = CoreFoundation{ + .handle = fsevents_cf_handle, + .ArrayCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]?*anyopaque, CFIndex, ?*CFArrayCallBacks) callconv(.C) CFArrayRef, "CFArrayCreate") orelse @panic("Cannot Load CoreFoundation"), + .Release = dlsym(fsevents_cf_handle, *fn (CFTypeRef) callconv(.C) void, "CFRelease") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopAddSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopAddSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopGetCurrent = dlsym(fsevents_cf_handle, *fn () callconv(.C) CFRunLoopRef, "CFRunLoopGetCurrent") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRemoveSource = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef, CFRunLoopSourceRef, CFStringRef) callconv(.C) void, "CFRunLoopRemoveSource") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopRun = dlsym(fsevents_cf_handle, *fn () callconv(.C) void, "CFRunLoopRun") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceCreate = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, CFIndex, *CFRunLoopSourceContext) callconv(.C) CFRunLoopSourceRef, "CFRunLoopSourceCreate") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopSourceSignal = dlsym(fsevents_cf_handle, *fn (CFRunLoopSourceRef) callconv(.C) void, "CFRunLoopSourceSignal") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopStop = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopStop") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopWakeUp = dlsym(fsevents_cf_handle, *fn (CFRunLoopRef) callconv(.C) void, "CFRunLoopWakeUp") orelse @panic("Cannot Load CoreFoundation"), + .StringCreateWithFileSystemRepresentation = dlsym(fsevents_cf_handle, *fn (CFAllocatorRef, [*]const u8) callconv(.C) CFStringRef, "CFStringCreateWithFileSystemRepresentation") orelse @panic("Cannot Load CoreFoundation"), + .RunLoopDefaultMode = dlsym(fsevents_cf_handle, *CFStringRef, "kCFRunLoopDefaultMode") orelse @panic("Cannot Load CoreFoundation"), + }; + + const fsevents_cs_handle = std.c.dlopen("/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", RTLD_LAZY | RTLD_LOCAL); + if (fsevents_cs_handle == null) @panic("Cannot Load CoreServices"); + + fsevents_cs = CoreServices{ + .handle = fsevents_cs_handle, + .FSEventStreamCreate = dlsym(fsevents_cs_handle, *fn (CFAllocatorRef, FSEventStreamCallback, *FSEventStreamContext, CFArrayRef, FSEventStreamEventId, CFTimeInterval, FSEventStreamCreateFlags) callconv(.C) FSEventStreamRef, "FSEventStreamCreate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamInvalidate = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamInvalidate") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamRelease = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamRelease") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamScheduleWithRunLoop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef, CFRunLoopRef, CFStringRef) callconv(.C) void, "FSEventStreamScheduleWithRunLoop") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStart = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) c_int, "FSEventStreamStart") orelse @panic("Cannot Load CoreServices"), + .FSEventStreamStop = dlsym(fsevents_cs_handle, *fn (FSEventStreamRef) callconv(.C) void, "FSEventStreamStop") orelse @panic("Cannot Load CoreServices"), + }; +} + +pub const FSEventsLoop = struct { + signal_source: CFRunLoopSourceRef, + mutex: Mutex, + loop: CFRunLoopRef = null, + sem: Semaphore, + thread: std.Thread = undefined, + tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{}, + watchers: bun.BabyList(?*FSEventsWatcher) = .{}, + watcher_count: u32 = 0, + fsevent_stream: FSEventStreamRef = null, + paths: ?[]?*anyopaque = null, + cf_paths: CFArrayRef = null, + has_scheduled_watchers: bool = false, + + pub const Task = struct { + ctx: ?*anyopaque, + callback: *const (fn (*anyopaque) void), + + pub fn run(this: *Task) void { + var callback = this.callback; + var ctx = this.ctx; + callback(ctx.?); + } + + pub fn New(comptime Type: type, comptime Callback: anytype) type { + return struct { + pub fn init(ctx: *Type) Task { + return Task{ + .callback = wrap, + .ctx = ctx, + }; + } + + pub fn wrap(this: ?*anyopaque) void { + @call(.always_inline, Callback, .{@ptrCast(*Type, @alignCast(@alignOf(Type), this.?))}); + } + }; + } + }; + + pub const ConcurrentTask = struct { + task: Task = undefined, + next: ?*ConcurrentTask = null, + auto_delete: bool = false, + + pub const Queue = UnboundedQueue(ConcurrentTask, .next); + + pub fn from(this: *ConcurrentTask, task: Task) *ConcurrentTask { + this.* = .{ + .task = task, + .next = null, + }; + return this; + } + }; + + pub fn CFThreadLoop(this: *FSEventsLoop) void { + bun.Output.Source.configureNamedThread("CFThreadLoop"); + + const CF = CoreFoundation.get(); + + this.loop = CF.RunLoopGetCurrent(); + + CF.RunLoopAddSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.sem.post(); + + CF.RunLoopRun(); + CF.RunLoopRemoveSource(this.loop, this.signal_source, CF.RunLoopDefaultMode.*); + + this.loop = null; + } + + // Runs in CF thread, executed after `enqueueTaskConcurrent()` + fn CFLoopCallback(arg: ?*anyopaque) callconv(.C) void { + if (arg) |self| { + const this = bun.cast(*FSEventsLoop, self); + + var concurrent = this.tasks.popBatch(); + const count = concurrent.count; + if (count == 0) + return; + + var iter = concurrent.iterator(); + while (iter.next()) |task| { + task.task.run(); + if (task.auto_delete) bun.default_allocator.destroy(task); + } + } + } + + pub fn init() !*FSEventsLoop { + const this = bun.default_allocator.create(FSEventsLoop) catch unreachable; + + const CF = CoreFoundation.get(); + + var ctx = CFRunLoopSourceContext{ + .info = this, + .perform = CFLoopCallback, + }; + + const signal_source = CF.RunLoopSourceCreate(null, 0, &ctx); + if (signal_source == null) { + return error.FailedToCreateCoreFoudationSourceLoop; + } + + var fs_loop = FSEventsLoop{ .sem = Semaphore.init(0), .mutex = Mutex.init(), .signal_source = signal_source }; + + this.* = fs_loop; + this.thread = try std.Thread.spawn(.{}, FSEventsLoop.CFThreadLoop, .{this}); + + // sync threads + this.sem.wait(); + return this; + } + + fn enqueueTaskConcurrent(this: *FSEventsLoop, task: Task) void { + const CF = CoreFoundation.get(); + var concurrent = bun.default_allocator.create(ConcurrentTask) catch unreachable; + concurrent.auto_delete = true; + this.tasks.push(concurrent.from(task)); + CF.RunLoopSourceSignal(this.signal_source); + CF.RunLoopWakeUp(this.loop); + } + + // Runs in CF thread, when there're events in FSEventStream + fn _events_cb(_: FSEventStreamRef, info: ?*anyopaque, numEvents: usize, eventPaths: ?*anyopaque, eventFlags: *FSEventStreamEventFlags, _: *FSEventStreamEventId) callconv(.C) void { + const paths_ptr = bun.cast([*][*:0]const u8, eventPaths); + const paths = paths_ptr[0..numEvents]; + var loop = bun.cast(*FSEventsLoop, info); + const event_flags = bun.cast([*]FSEventStreamEventFlags, eventFlags); + + for (loop.watchers.slice()) |watcher| { + if (watcher) |handle| { + for (paths, 0..) |path_ptr, i| { + var flags = event_flags[i]; + var path = path_ptr[0..bun.len(path_ptr)]; + // Filter out paths that are outside handle's request + if (path.len < handle.path.len or !bun.strings.startsWith(path, handle.path)) { + continue; + } + const is_file = (flags & kFSEventStreamEventFlagItemIsDir) == 0; + + // Remove common prefix, unless the watched folder is "/" + if (!(handle.path.len == 1 and handle.path[0] == '/')) { + path = path[handle.path.len..]; + + // Ignore events with path equal to directory itself + if (path.len <= 1 and is_file) { + continue; + } + if (path.len == 0) { + // Since we're using fsevents to watch the file itself, path == handle.path, and we now need to get the basename of the file back + while (path.len > 0) { + if (bun.strings.startsWithChar(path, '/')) { + path = path[1..]; + break; + } else { + path = path[1..]; + } + } + + // Created and Removed seem to be always set, but don't make sense + flags &= ~kFSEventsRenamed; + } else { + // Skip forward slash + path = path[1..]; + } + } + + // Do not emit events from subdirectories (without option set) + if (path.len == 0 or (bun.strings.containsChar(path, '/') and !handle.recursive)) { + continue; + } + + var is_rename = true; + + if ((flags & kFSEventsRenamed) == 0) { + if ((flags & kFSEventsModified) != 0 or is_file) { + is_rename = false; + } + } + + handle.callback(handle.ctx, path, is_file, is_rename); + } + } + } + } + + // Runs on CF Thread + pub fn _schedule(this: *FSEventsLoop) void { + this.mutex.lock(); + defer this.mutex.unlock(); + this.has_scheduled_watchers = false; + + var watchers = this.watchers.slice(); + + const CF = CoreFoundation.get(); + const CS = CoreServices.get(); + + if (this.fsevent_stream) |stream| { + // Stop emitting events + CS.FSEventStreamStop(stream); + + // Release stream + CS.FSEventStreamInvalidate(stream); + CS.FSEventStreamRelease(stream); + this.fsevent_stream = null; + } + // clean old paths + if (this.paths) |p| { + this.paths = null; + bun.default_allocator.destroy(p); + } + if (this.cf_paths) |cf| { + this.cf_paths = null; + CF.Release(cf); + } + + const paths = bun.default_allocator.alloc(?*anyopaque, this.watcher_count) catch unreachable; + var count: u32 = 0; + for (watchers) |w| { + if (w) |watcher| { + const path = CF.StringCreateWithFileSystemRepresentation(null, watcher.path.ptr); + paths[count] = path; + count += 1; + } + } + + const cf_paths = CF.ArrayCreate(null, paths.ptr, count, null); + var ctx: FSEventStreamContext = .{ + .info = this, + }; + + const latency: CFAbsoluteTime = 0.05; + // Explanation of selected flags: + // 1. NoDefer - without this flag, events that are happening continuously + // (i.e. each event is happening after time interval less than `latency`, + // counted from previous event), will be deferred and passed to callback + // once they'll either fill whole OS buffer, or when this continuous stream + // will stop (i.e. there'll be delay between events, bigger than + // `latency`). + // Specifying this flag will invoke callback after `latency` time passed + // since event. + // 2. FileEvents - fire callback for file changes too (by default it is firing + // it only for directory changes). + // + const flags: FSEventStreamCreateFlags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; + + // + // NOTE: It might sound like a good idea to remember last seen StreamEventId, + // but in reality one dir might have last StreamEventId less than, the other, + // that is being watched now. Which will cause FSEventStream API to report + // changes to files from the past. + // + const ref = CS.FSEventStreamCreate(null, _events_cb, &ctx, cf_paths, CS.kFSEventStreamEventIdSinceNow, latency, flags); + + CS.FSEventStreamScheduleWithRunLoop(ref, this.loop, CF.RunLoopDefaultMode.*); + if (CS.FSEventStreamStart(ref) == 0) { + //clean in case of failure + bun.default_allocator.destroy(paths); + CF.Release(cf_paths); + CS.FSEventStreamInvalidate(ref); + CS.FSEventStreamRelease(ref); + return; + } + this.fsevent_stream = ref; + this.paths = paths; + this.cf_paths = cf_paths; + } + + fn registerWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + if (this.watcher_count == this.watchers.len) { + this.watcher_count += 1; + this.watchers.push(bun.default_allocator, watcher) catch unreachable; + } else { + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w == null) { + watchers[i] = watcher; + this.watcher_count += 1; + break; + } + } + } + + if (this.has_scheduled_watchers == false) { + this.has_scheduled_watchers = true; + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, _schedule).init(this)); + } + } + + fn unregisterWatcher(this: *FSEventsLoop, watcher: *FSEventsWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w) |item| { + if (item == watcher) { + watchers[i] = null; + // if is the last one just pop + if (i == watchers.len - 1) { + this.watchers.len -= 1; + } + this.watcher_count -= 1; + break; + } + } + } + } + + // Runs on CF loop to close the loop + fn _stop(this: *FSEventsLoop) void { + const CF = CoreFoundation.get(); + CF.RunLoopStop(this.loop); + } + fn deinit(this: *FSEventsLoop) void { + // signal close and wait + this.enqueueTaskConcurrent(Task.New(FSEventsLoop, FSEventsLoop._stop).init(this)); + this.thread.join(); + const CF = CoreFoundation.get(); + + CF.Release(this.signal_source); + this.signal_source = null; + + this.sem.deinit(); + this.mutex.deinit(); + if (this.watcher_count > 0) { + while (this.watchers.popOrNull()) |watcher| { + if (watcher) |w| { + // unlink watcher + w.loop = null; + } + } + } + + this.watchers.deinitWithAllocator(bun.default_allocator); + + bun.default_allocator.destroy(this); + } +}; + +pub const FSEventsWatcher = struct { + path: string, + callback: Callback, + loop: ?*FSEventsLoop, + recursive: bool, + ctx: ?*anyopaque, + + const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, is_rename: bool) void; + + pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, ctx: ?*anyopaque) *FSEventsWatcher { + var this = bun.default_allocator.create(FSEventsWatcher) catch unreachable; + this.* = FSEventsWatcher{ + .path = path, + .callback = callback, + .loop = loop, + .recursive = recursive, + .ctx = ctx, + }; + + loop.registerWatcher(this); + return this; + } + + pub fn deinit(this: *FSEventsWatcher) void { + if (this.loop) |loop| { + loop.unregisterWatcher(this); + } + bun.default_allocator.destroy(this); + } +}; + +pub fn watch(path: string, recursive: bool, callback: FSEventsWatcher.Callback, ctx: ?*anyopaque) !*FSEventsWatcher { + if (fsevents_default_loop) |loop| { + return FSEventsWatcher.init(loop, path, recursive, callback, ctx); + } else { + fsevents_default_loop_mutex.lock(); + defer fsevents_default_loop_mutex.unlock(); + if (fsevents_default_loop == null) { + fsevents_default_loop = try FSEventsLoop.init(); + } + return FSEventsWatcher.init(fsevents_default_loop.?, path, recursive, callback, ctx); + } +} diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index f984077e4..ce35c940a 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -1,6 +1,34 @@ import { define } from "../scripts/class-definitions"; export default [ + define({ + name: "FSWatcher", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + klass: {}, + JSType: "0b11101110", + proto: { + ref: { + fn: "doRef", + length: 0, + }, + unref: { + fn: "doUnref", + length: 0, + }, + hasRef: { + fn: "hasRef", + length: 0, + }, + close: { + fn: "doClose", + length: 0, + }, + }, + values: ["listener"], + }), define({ name: "Timeout", construct: false, @@ -300,7 +328,7 @@ export default [ utimes: { fn: "utimes", length: 4 }, utimesSync: { fn: "utimesSync", length: 3 }, // TODO: - // watch: { fn: "watch", length: 3 }, + watch: { fn: "watch", length: 3 }, // watchFile: { fn: "watchFile", length: 3 }, writeFile: { fn: "writeFile", length: 4 }, writeFileSync: { fn: "writeFileSync", length: 3 }, diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 3ea0822e6..21a65251a 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -34,7 +34,6 @@ const Mode = JSC.Node.Mode; const uid_t = std.os.uid_t; const gid_t = std.os.gid_t; - /// u63 to allow one null bit const ReadPosition = u63; @@ -2313,7 +2312,7 @@ pub const Arguments = struct { }; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.Node.FSWatcher.Arguments; pub const WatchFile = void; pub const Fsync = struct { fd: FileDescriptor, @@ -2475,7 +2474,7 @@ const Return = struct { pub const Truncate = void; pub const Unlink = void; pub const UnwatchFile = void; - pub const Watch = void; + pub const Watch = JSC.JSValue; pub const WatchFile = void; pub const Utimes = void; @@ -4181,8 +4180,12 @@ pub const NodeFS = struct { return Maybe(Return.Lutimes).todo; } - pub fn watch(_: *NodeFS, _: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - return Maybe(Return.Watch).todo; + pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { + const watcher = args.createFSWatcher() catch |err| { + args.global_this.throwError(err, "Failed to watch filename"); + return Maybe(Return.Watch){ .result = JSC.JSValue.jsUndefined() }; + }; + return Maybe(Return.Watch){ .result = watcher }; } pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) { return Maybe(Return.CreateReadStream).todo; diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index 74b769bf6..f178f0355 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -241,6 +241,8 @@ pub const NodeJSFS = struct { return JSC.Node.Stats.getConstructor(globalThis); } + pub const watch = callSync(.watch); + // Not implemented yet: const notimpl = fdatasync; pub const opendir = notimpl; diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig new file mode 100644 index 000000000..397d51916 --- /dev/null +++ b/src/bun.js/node/node_fs_watcher.zig @@ -0,0 +1,913 @@ +const std = @import("std"); +const JSC = @import("root").bun.JSC; +const bun = @import("root").bun; +const Fs = @import("../../fs.zig"); +const Path = @import("../../resolver/resolve_path.zig"); +const Encoder = JSC.WebCore.Encoder; + +const FSEvents = @import("./fs_events.zig"); + +const VirtualMachine = JSC.VirtualMachine; +const EventLoop = JSC.EventLoop; +const PathLike = JSC.Node.PathLike; +const ArgumentsSlice = JSC.Node.ArgumentsSlice; +const Output = bun.Output; +const string = bun.string; +const StoredFileDescriptorType = bun.StoredFileDescriptorType; +const Environment = bun.Environment; + +pub const FSWatcher = struct { + const watcher = @import("../../watcher.zig"); + const options = @import("../../options.zig"); + pub const Watcher = watcher.NewWatcher(*FSWatcher); + const log = Output.scoped(.FSWatcher, false); + + pub const ChangeEvent = struct { + hash: Watcher.HashType = 0, + event_type: FSWatchTask.EventType = .change, + time_stamp: i64 = 0, + }; + + onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, + ctx: *VirtualMachine, + js_watcher: ?*JSObject = null, + watcher_instance: ?*FSWatcher.Watcher = null, + verbose: bool = false, + file_paths: bun.BabyList(string) = .{}, + entry_path: ?string = null, + entry_dir: string = "", + last_change_event: ChangeEvent = .{}, + + pub fn toJS(this: *FSWatcher) JSC.JSValue { + return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined(); + } + + pub fn eventLoop(this: FSWatcher) *EventLoop { + return this.ctx.eventLoop(); + } + + pub fn enqueueTaskConcurrent(this: FSWatcher, task: *JSC.ConcurrentTask) void { + this.eventLoop().enqueueTaskConcurrent(task); + } + + pub fn deinit(this: *FSWatcher) void { + while (this.file_paths.popOrNull()) |file_path| { + bun.default_allocator.destroy(file_path); + } + this.file_paths.deinitWithAllocator(bun.default_allocator); + if (this.entry_path) |path| { + this.entry_path = null; + bun.default_allocator.destroy(path); + } + bun.default_allocator.destroy(this); + } + + pub const FSWatchTask = struct { + ctx: *FSWatcher, + count: u8 = 0, + + entries: [8]Entry = undefined, + concurrent_task: JSC.ConcurrentTask = undefined, + + pub const EventType = enum { + rename, + change, + @"error", + abort, + }; + + pub const EventFreeType = enum { + destroy, + free, + none, + }; + + pub const Entry = struct { + file_path: string, + event_type: EventType, + free_type: EventFreeType, + }; + + pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, free_type: EventFreeType) void { + if (this.count == 8) { + this.enqueue(); + var ctx = this.ctx; + this.* = .{ + .ctx = ctx, + .count = 0, + }; + } + + this.entries[this.count] = .{ + .file_path = file_path, + .event_type = event_type, + .free_type = free_type, + }; + this.count += 1; + } + + pub fn run(this: *FSWatchTask) void { + // this runs on JS Context + if (this.ctx.js_watcher) |js_watcher| { + for (this.entries[0..this.count]) |entry| { + switch (entry.event_type) { + .rename => { + js_watcher.emit(entry.file_path, "rename"); + }, + .change => { + js_watcher.emit(entry.file_path, "change"); + }, + .@"error" => { + // file_path is the error message in this case + js_watcher.emitError(entry.file_path); + }, + .abort => { + js_watcher.emitIfAborted(); + }, + } + } + } + } + + pub fn enqueue(this: *FSWatchTask) void { + if (this.count == 0) + return; + + var that = bun.default_allocator.create(FSWatchTask) catch unreachable; + + that.* = this.*; + this.count = 0; + that.concurrent_task.task = JSC.Task.init(that); + this.ctx.enqueueTaskConcurrent(&that.concurrent_task); + } + + pub fn deinit(this: *FSWatchTask) void { + while (this.count > 0) { + this.count -= 1; + switch (this.entries[this.count].free_type) { + .destroy => bun.default_allocator.destroy(this.entries[this.count].file_path), + .free => bun.default_allocator.free(this.entries[this.count].file_path), + else => {}, + } + } + bun.default_allocator.destroy(this); + } + }; + + fn NewCallback(comptime FunctionSignature: type) type { + return union(enum) { + javascript_callback: JSC.Strong, + zig_callback: struct { + ptr: *anyopaque, + function: *const FunctionSignature, + }, + }; + } + + pub const OnAcceptCallback = NewCallback(fn ( + vm: *JSC.VirtualMachine, + specifier: []const u8, + ) void); + + fn addDirectory(ctx: *FSWatcher, fs_watcher: *FSWatcher.Watcher, fd: StoredFileDescriptorType, file_path: string, recursive: bool, buf: *[bun.MAX_PATH_BYTES + 1]u8, is_entry_path: bool) !void { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + + if (is_entry_path) { + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; + } else { + ctx.file_paths.push(bun.default_allocator, dir_path_clone) catch unreachable; + } + fs_watcher.addDirectory(fd, dir_path_clone, FSWatcher.Watcher.getHash(file_path), false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + var iter = (std.fs.IterableDir{ .dir = std.fs.Dir{ + .fd = fd, + } }).iterate(); + + while (iter.next() catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }) |entry| { + var parts = [2]string{ dir_path_clone, entry.name }; + var entry_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), + buf, + &parts, + .auto, + ); + + buf[entry_path.len] = 0; + var entry_path_z = buf[0..entry_path.len :0]; + + var fs_info = fdFromAbsolutePathZ(entry_path_z) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + if (fs_info.is_file) { + const file_path_clone = bun.default_allocator.dupeZ(u8, entry_path) catch unreachable; + + ctx.file_paths.push(bun.default_allocator, file_path_clone) catch unreachable; + + fs_watcher.addFile(fs_info.fd, file_path_clone, FSWatcher.Watcher.getHash(entry_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } else { + if (recursive) { + addDirectory(ctx, fs_watcher, fs_info.fd, entry_path, recursive, buf, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } + } + } + } + + pub fn onError( + this: *FSWatcher, + err: anyerror, + ) void { + var current_task: FSWatchTask = .{ + .ctx = this, + }; + current_task.append(@errorName(err), .@"error", .none); + current_task.enqueue(); + } + + pub fn onFSEventUpdate( + ctx: ?*anyopaque, + path: string, + _: bool, + is_rename: bool, + ) void { + const this = bun.cast(*FSWatcher, ctx.?); + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + const relative_path = bun.default_allocator.dupe(u8, path) catch unreachable; + const event_type: FSWatchTask.EventType = if (is_rename) .rename else .change; + + current_task.append(relative_path, event_type, .destroy); + } + + pub fn onFileUpdate( + this: *FSWatcher, + events: []watcher.WatchEvent, + changed_files: []?[:0]u8, + watchlist: watcher.Watchlist, + ) void { + var slice = watchlist.slice(); + const file_paths = slice.items(.file_path); + + var counts = slice.items(.count); + const kinds = slice.items(.kind); + var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + var ctx = this.watcher_instance.?; + defer ctx.flushEvictions(); + defer Output.flush(); + + var bundler = if (@TypeOf(this.ctx.bundler) == *bun.Bundler) + this.ctx.bundler + else + &this.ctx.bundler; + + var fs: *Fs.FileSystem = bundler.fs; + + var current_task: FSWatchTask = .{ + .ctx = this, + }; + defer current_task.enqueue(); + + const time_stamp = std.time.milliTimestamp(); + const time_diff = time_stamp - this.last_change_event.time_stamp; + + for (events) |event| { + const file_path = file_paths[event.index]; + const update_count = counts[event.index] + 1; + counts[event.index] = update_count; + const kind = kinds[event.index]; + + if (comptime Environment.isDebug) { + if (this.verbose) { + Output.prettyErrorln("[watch] {s} ({s}, {})", .{ file_path, @tagName(kind), event.op }); + } + } + + switch (kind) { + .file => { + if (event.op.delete) { + ctx.removeAtIndex( + event.index, + 0, + &.{}, + .file, + ); + } + + var file_hash: FSWatcher.Watcher.HashType = FSWatcher.Watcher.getHash(file_path); + + if (event.op.write or event.op.delete or event.op.rename) { + const event_type: FSWatchTask.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; + // skip consecutive duplicates + if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) { + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + const relative_slice = fs.relative(this.entry_dir, file_path); + + if (this.verbose) + Output.prettyErrorln("File changed: {s}", .{relative_slice}); + + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; + + current_task.append(relative_path, event_type, .destroy); + } + } + }, + .directory => { + // macOS should use FSEvents for directories + if (comptime Environment.isMac) { + @panic("Unexpected directory watch"); + } + + const affected = event.names(changed_files); + + for (affected) |changed_name_| { + const changed_name: []const u8 = bun.asByteSlice(changed_name_.?); + if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue; + + var file_hash: FSWatcher.Watcher.HashType = 0; + const relative_slice: string = brk: { + var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str); + + @memcpy(_on_file_update_path_buf[0..file_path_without_trailing_slash.len], file_path_without_trailing_slash); + + _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep; + + @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..][0..changed_name.len], changed_name); + const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; + file_hash = FSWatcher.Watcher.getHash(path_slice); + + const relative = fs.relative(this.entry_dir, path_slice); + + break :brk relative; + }; + + // skip consecutive duplicates + const event_type: FSWatchTask.EventType = .rename; // renaming folders, creating folder or files will be always be rename + if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != file_hash) { + const relative_path = bun.default_allocator.dupe(u8, relative_slice) catch unreachable; + + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + this.last_change_event.hash = file_hash; + + current_task.append(relative_path, event_type, .destroy); + + if (this.verbose) + Output.prettyErrorln(" Dir change: {s}", .{relative_path}); + } + } + + if (this.verbose and affected.len == 0) { + Output.prettyErrorln(" Dir change: {s}", .{fs.relative(this.entry_dir, file_path)}); + } + }, + } + } + } + + pub const Arguments = struct { + path: PathLike, + listener: JSC.JSValue, + global_this: JSC.C.JSContextRef, + signal: ?*JSC.AbortSignal, + persistent: bool, + recursive: bool, + encoding: JSC.Node.Encoding, + verbose: bool, + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Arguments { + const vm = ctx.vm(); + const path = PathLike.fromJS(ctx, arguments, exception) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "filename must be a string or TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + + if (exception.* != null) return null; + var listener: JSC.JSValue = .zero; + var signal: ?*JSC.AbortSignal = null; + var persistent: bool = true; + var recursive: bool = false; + var encoding: JSC.Node.Encoding = .utf8; + var verbose = false; + if (arguments.nextEat()) |options_or_callable| { + + // options + if (options_or_callable.isObject()) { + if (options_or_callable.get(ctx, "persistent")) |persistent_| { + if (!persistent_.isBoolean()) { + JSC.throwInvalidArguments( + "persistent must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + persistent = persistent_.toBoolean(); + } + + if (options_or_callable.get(ctx, "verbose")) |verbose_| { + if (!verbose_.isBoolean()) { + JSC.throwInvalidArguments( + "verbose must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + verbose = verbose_.toBoolean(); + } + + if (options_or_callable.get(ctx, "encoding")) |encoding_| { + if (!encoding_.isString()) { + JSC.throwInvalidArguments( + "encoding must be a string.", + .{}, + ctx, + exception, + ); + return null; + } + if (JSC.Node.Encoding.fromJS(encoding_, ctx.ptr())) |node_encoding| { + encoding = node_encoding; + } else { + JSC.throwInvalidArguments( + "invalid encoding.", + .{}, + ctx, + exception, + ); + return null; + } + } + + if (options_or_callable.get(ctx, "recursive")) |recursive_| { + if (!recursive_.isBoolean()) { + JSC.throwInvalidArguments( + "recursive must be a boolean.", + .{}, + ctx, + exception, + ); + return null; + } + recursive = recursive_.toBoolean(); + } + + // abort signal + if (options_or_callable.get(ctx, "signal")) |signal_| { + if (JSC.AbortSignal.fromJS(signal_)) |signal_obj| { + //Keep it alive + signal_.ensureStillAlive(); + signal = signal_obj; + } else { + JSC.throwInvalidArguments( + "signal is not of type AbortSignal.", + .{}, + ctx, + exception, + ); + + return null; + } + } + + // listener + if (arguments.nextEat()) |callable| { + if (!callable.isCell() or !callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = callable; + } + } else { + if (!options_or_callable.isCell() or !options_or_callable.isCallable(vm)) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback to be a function", .{}, ctx).asObjectRef(); + return null; + } + listener = options_or_callable; + } + } + if (listener == .zero) { + exception.* = JSC.toInvalidArguments("Expected \"listener\" callback", .{}, ctx).asObjectRef(); + return null; + } + + return Arguments{ + .path = path, + .listener = listener, + .global_this = ctx, + .signal = signal, + .persistent = persistent, + .recursive = recursive, + .encoding = encoding, + .verbose = verbose, + }; + } + + pub fn createFSWatcher(this: Arguments) !JSC.JSValue { + const obj = try FSWatcher.init(this); + return obj.toJS(); + } + }; + + pub const JSObject = struct { + signal: ?*JSC.AbortSignal, + persistent: bool, + manager: ?*FSWatcher.Watcher, + fsevents_watcher: ?*FSEvents.FSEventsWatcher, + poll_ref: JSC.PollRef = .{}, + globalThis: ?*JSC.JSGlobalObject, + js_this: JSC.JSValue, + encoding: JSC.Node.Encoding, + closed: bool, + + pub usingnamespace JSC.Codegen.JSFSWatcher; + + pub fn getFSWatcher(this: *JSObject) *FSWatcher { + if (this.manager) |manager| return manager.ctx; + if (this.fsevents_watcher) |manager| return bun.cast(*FSWatcher, manager.ctx.?); + + @panic("No context attached to JSFSWatcher"); + } + + pub fn init(globalThis: *JSC.JSGlobalObject, manager: ?*FSWatcher.Watcher, fsevents_watcher: ?*FSEvents.FSEventsWatcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { + var obj = try globalThis.allocator().create(JSObject); + obj.* = .{ + .signal = null, + .persistent = persistent, + .manager = manager, + .fsevents_watcher = fsevents_watcher, + .globalThis = globalThis, + .js_this = .zero, + .encoding = encoding, + .closed = false, + }; + const instance = obj.getFSWatcher(); + + if (persistent) { + obj.poll_ref.ref(instance.ctx); + } + + var js_this = JSObject.toJS(obj, globalThis); + JSObject.listenerSetCached(js_this, globalThis, listener); + obj.js_this = js_this; + obj.js_this.protect(); + + if (signal) |s| { + + // already aborted? + if (s.aborted()) { + obj.signal = s.ref(); + // abort next tick + var current_task: FSWatchTask = .{ + .ctx = instance, + }; + current_task.append("", .abort, .none); + current_task.enqueue(); + } else { + // watch for abortion + obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); + } + } + return obj; + } + + pub fn emitIfAborted(this: *JSObject) void { + if (this.signal) |s| { + if (s.aborted()) { + const err = s.abortReason(); + this.emitAbort(err); + } + } + } + + pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void { + if (this.closed) return; + defer this.close(true); + + err.ensureStillAlive(); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, globalThis) else err, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + pub fn emitError(this: *JSObject, err: string) void { + if (this.closed) return; + defer this.close(true); + + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(globalThis), + JSC.ZigString.fromUTF8(err).toErrorInstance(globalThis), + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn emit(this: *JSObject, file_name: string, comptime eventType: string) void { + if (this.globalThis) |globalThis| { + if (this.js_this != .zero) { + if (JSObject.listenerGetCached(this.js_this)) |listener| { + var filename: JSC.JSValue = JSC.JSValue.jsUndefined(); + if (file_name.len > 0) { + if (this.encoding == .buffer) + filename = JSC.ArrayBuffer.createBuffer(globalThis, file_name) + else if (this.encoding == .utf8) { + filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis); + } else { + // convert to desired encoding + filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding); + } + } + var args = [_]JSC.JSValue{ + JSC.ZigString.static(eventType).toValue(globalThis), + filename, + }; + _ = listener.callWithGlobalThis( + globalThis, + &args, + ); + } + } + } + } + + pub fn ref(this: *JSObject) void { + if (this.closed) return; + + if (!this.persistent) { + this.persistent = true; + this.poll_ref.ref(this.getFSWatcher().ctx); + } + } + + pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.ref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn unref(this: *JSObject) void { + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.getFSWatcher().ctx); + } + } + + pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.unref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.persistent); + } + + pub fn close( + this: *JSObject, + emitEvent: bool, + ) void { + if (!this.closed) { + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + this.closed = true; + if (emitEvent) { + this.emit("", "close"); + } + + this.detach(); + } + } + + pub fn detach(this: *JSObject) void { + this.unref(); + + if (this.js_this != .zero) { + this.js_this.unprotect(); + this.js_this = .zero; + } + + this.globalThis = null; + + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); + } + if (this.manager) |manager| { + var ctx = manager.ctx; + this.manager = null; + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(true); + } + + if (this.fsevents_watcher) |manager| { + var ctx = bun.cast(*FSWatcher, manager.ctx.?); + ctx.js_watcher = null; + ctx.deinit(); + manager.deinit(); + } + } + + pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.close(true); + return JSC.JSValue.jsUndefined(); + } + + pub fn finalize(this: *JSObject) callconv(.C) void { + if (!this.closed) { + this.detach(); + } + + bun.default_allocator.destroy(this); + } + }; + + const PathResult = struct { + fd: StoredFileDescriptorType = 0, + is_file: bool = true, + }; + + fn fdFromAbsolutePathZ( + absolute_path_z: [:0]const u8, + ) !PathResult { + var stat = try bun.C.lstat_absolute(absolute_path_z); + var result = PathResult{}; + + switch (stat.kind) { + .sym_link => { + var file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); + result.fd = file.handle; + const _stat = try file.stat(); + + result.is_file = _stat.kind == .directory; + }, + .directory => { + const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{ + .access_sub_paths = true, + })).dir; + result.fd = dir.fd; + result.is_file = false; + }, + else => { + const file = try std.fs.openFileAbsoluteZ(absolute_path_z, .{ .mode = .read_only }); + result.fd = file.handle; + result.is_file = true; + }, + } + return result; + } + + pub fn init(args: Arguments) !*FSWatcher { + var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + var slice = args.path.slice(); + if (bun.strings.startsWith(slice, "file://")) { + slice = slice[6..]; + } + var parts = [_]string{ + slice, + }; + + var file_path = Path.joinAbsStringBuf( + Fs.FileSystem.instance.top_level_dir, + &buf, + &parts, + .auto, + ); + + buf[file_path.len] = 0; + var file_path_z = buf[0..file_path.len :0]; + + var fs_type = try fdFromAbsolutePathZ(file_path_z); + + var ctx = try bun.default_allocator.create(FSWatcher); + const vm = args.global_this.bunVM(); + ctx.* = .{ + .ctx = vm, + .verbose = args.verbose, + .file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| { + ctx.deinit(); + return err; + }, + }; + + if (comptime Environment.isMac) { + if (!fs_type.is_file) { + var dir_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + ctx.entry_path = dir_path_clone; + ctx.entry_dir = dir_path_clone; + + var fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, null, fsevents_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + fsevents_watcher.deinit(); + return err; + }; + + return ctx; + } + } + + var fs_watcher = FSWatcher.Watcher.init( + ctx, + vm.bundler.fs, + bun.default_allocator, + ) catch |err| { + ctx.deinit(); + return err; + }; + + ctx.watcher_instance = fs_watcher; + + if (fs_type.is_file) { + var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; + + ctx.entry_path = file_path_clone; + ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone; + + fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } else { + addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + } + + fs_watcher.start() catch |err| { + ctx.deinit(); + + fs_watcher.deinit(true); + return err; + }; + + ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, null, args.signal, args.listener, args.persistent, args.encoding) catch |err| { + ctx.deinit(); + fs_watcher.deinit(true); + return err; + }; + + return ctx; + } +}; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index e2de35706..659ac31bb 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -93,6 +93,10 @@ pub fn Maybe(comptime ResultType: type) type { return JSC.JSValue.jsUndefined(); } + if (comptime ReturnType == JSC.JSValue) { + return r; + } + if (comptime ReturnType == JSC.ArrayBuffer) { return r.toJS(globalThis, null); } diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 5c8221128..061a25eed 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -802,7 +802,20 @@ pub const Encoder = struct { // pub fn writeUTF16AsUTF8(utf16: [*]const u16, len: usize, to: [*]u8, to_len: usize) callconv(.C) i32 { // return @intCast(i32, strings.copyUTF16IntoUTF8(to[0..to_len], []const u16, utf16[0..len], true).written); // } - + pub fn toStringAtRuntime(input: [*]const u8, len: usize, globalObject: *JSGlobalObject, encoding: JSC.Node.Encoding) JSValue { + return switch (encoding) { + .ucs2 => toString(input, len, globalObject, .utf16le), + .utf16le => toString(input, len, globalObject, .utf16le), + .utf8 => toString(input, len, globalObject, .utf8), + .ascii => toString(input, len, globalObject, .ascii), + .hex => toString(input, len, globalObject, .hex), + .base64 => toString(input, len, globalObject, .base64), + .base64url => toString(input, len, globalObject, .base64url), + .latin1 => toString(input, len, globalObject, .latin1), + // treat everything else as utf8 + else => toString(input, len, globalObject, .utf8), + }; + } pub fn toString(input_ptr: [*]const u8, len: usize, global: *JSGlobalObject, comptime encoding: JSC.Node.Encoding) JSValue { if (len == 0) return ZigString.Empty.toValue(global); diff --git a/src/fs.zig b/src/fs.zig index e87d931df..98174fac3 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1109,6 +1109,60 @@ pub const FileSystem = struct { return File{ .path = Path.init(path), .contents = file_contents }; } + pub fn kindFromAbsolute( + fs: *RealFS, + absolute_path: [:0]const u8, + existing_fd: StoredFileDescriptorType, + store_fd: bool, + ) !Entry.Cache { + var outpath: [bun.MAX_PATH_BYTES]u8 = undefined; + + var stat = try C.lstat_absolute(absolute_path); + const is_symlink = stat.kind == std.fs.File.Kind.SymLink; + var _kind = stat.kind; + var cache = Entry.Cache{ + .kind = Entry.Kind.file, + .symlink = PathString.empty, + }; + var symlink: []const u8 = ""; + + if (is_symlink) { + var file = try if (existing_fd != 0) + std.fs.File{ .handle = existing_fd } + else if (store_fd) + std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only }) + else + bun.openFileForPath(absolute_path); + setMaxFd(file.handle); + + defer { + if ((!store_fd or fs.needToCloseFiles()) and existing_fd == 0) { + file.close(); + } else if (comptime FeatureFlags.store_file_descriptors) { + cache.fd = file.handle; + } + } + const _stat = try file.stat(); + + symlink = try bun.getFdPath(file.handle, &outpath); + + _kind = _stat.kind; + } + + std.debug.assert(_kind != .SymLink); + + if (_kind == .Directory) { + cache.kind = .dir; + } else { + cache.kind = .file; + } + if (symlink.len > 0) { + cache.symlink = PathString.init(try FilenameStore.instance.append([]const u8, symlink)); + } + + return cache; + } + pub fn kind( fs: *RealFS, _dir: string, diff --git a/src/http.zig b/src/http.zig index 827bfa6de..80718db2f 100644 --- a/src/http.zig +++ b/src/http.zig @@ -3238,7 +3238,12 @@ pub const Server = struct { threadlocal var filechange_buf: [32]u8 = undefined; threadlocal var filechange_buf_hinted: [32]u8 = undefined; - + pub fn onError( + _: *@This(), + err: anyerror, + ) void { + Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + } pub fn onFileUpdate( ctx: *Server, events: []watcher.WatchEvent, diff --git a/src/js/node/fs.js b/src/js/node/fs.js index f117020dd..6b0e3954e 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -1,3 +1,5 @@ +import { EventEmitter } from "stream"; + // Hardcoded module "node:fs" var { direct, isPromise, isCallable } = import.meta.primordials; var promises = import.meta.require("node:fs/promises"); @@ -7,6 +9,63 @@ var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means var fs = Bun.fs(); var debug = process.env.DEBUG ? console.log : () => {}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + + if (typeof options === "function") { + listener = options; + options = {}; + } else if (typeof options === "string") { + options = { encoding: options }; + } + + if (typeof listener !== "function") { + listener = () => {}; + } + + this.#listener = listener; + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) { + throw e; + } + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + notFound.code = "ENOENT"; + notFound.errno = -2; + notFound.path = path; + notFound.syscall = "watch"; + notFound.filename = path; + throw notFound; + } + } + + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") { + this.emit(eventType, filenameOrError); + } else { + this.emit("change", eventType, filenameOrError); + this.#listener(eventType, filenameOrError); + } + } + + close() { + this.#watcher?.close(); + this.#watcher = null; + } + + ref() { + this.#watcher?.ref(); + } + + unref() { + this.#watcher?.unref(); + } +} export var access = function access(...args) { callbackify(fs.accessSync, args); }, @@ -153,6 +212,9 @@ export var access = function access(...args) { rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, + watch = function watch(path, options, listener) { + return new FSWatcher(path, options, listener); + }, promises = import.meta.require("node:fs/promises"); function callbackify(fsFunction, args) { @@ -1002,7 +1064,8 @@ export default { writeSync, WriteStream, ReadStream, - + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass, diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index de802928b..7df446ccb 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -1,4 +1,5 @@ // Hardcoded module "node:fs/promises" + // Note: `constants` is injected into the top of this file declare var constants: typeof import("node:fs/promises").constants; @@ -38,6 +39,55 @@ var promisify = { }, }[notrace]; +export function watch( + filename: string | Buffer | URL, + options: { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal } = {}, +) { + type Event = { + eventType: string; + filename: string | Buffer | undefined; + }; + const events: Array = []; + if (filename instanceof URL) { + throw new TypeError("Watch URLs are not supported yet"); + } else if (Buffer.isBuffer(filename)) { + filename = filename.toString(); + } else if (typeof filename !== "string") { + throw new TypeError("Expected path to be a string or Buffer"); + } + let nextEventResolve: Function | null = null; + if (typeof options === "string") { + options = { encoding: options }; + } + fs.watch(filename, options || {}, (eventType: string, filename: string | Buffer | undefined) => { + events.push({ eventType, filename }); + if (nextEventResolve) { + const resolve = nextEventResolve; + nextEventResolve = null; + resolve(); + } + }); + return { + async *[Symbol.asyncIterator]() { + let closed = false; + while (!closed) { + while (events.length) { + let event = events.shift() as Event; + if (event.eventType === "close") { + closed = true; + break; + } + if (event.eventType === "error") { + closed = true; + throw event.filename; + } + yield event; + } + await new Promise((resolve: Function) => (nextEventResolve = resolve)); + } + }, + }; +} export var access = promisify(fs.accessSync), appendFile = promisify(fs.appendFileSync), close = promisify(fs.closeSync), @@ -112,6 +162,7 @@ export default { lutimes, rm, rmdir, + watch, constants, [Symbol.for("CommonJS")]: 0, }; diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js index cc1e14d2b..cc3763cfc 100644 --- a/src/js/out/modules/node/fs.js +++ b/src/js/out/modules/node/fs.js @@ -1,3 +1,4 @@ +var {EventEmitter } = import.meta.require("node:stream"); var callbackify = function(fsFunction, args) { try { const result = fsFunction.apply(fs, args.slice(0, args.length - 1)), callback = args[args.length - 1]; @@ -16,7 +17,47 @@ function createWriteStream(path, options) { return new WriteStream(path, options); } var { direct, isPromise, isCallable } = import.meta.primordials, promises = import.meta.require("node:fs/promises"), { Readable, NativeWritable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream"), NativeReadable = _getNativeReadableStreamPrototype(2, Readable), fs = Bun.fs(), debug = process.env.DEBUG ? console.log : () => { -}, access = function access2(...args) { +}; + +class FSWatcher extends EventEmitter { + #watcher; + #listener; + constructor(path, options, listener) { + super(); + if (typeof options === "function") + listener = options, options = {}; + else if (typeof options === "string") + options = { encoding: options }; + if (typeof listener !== "function") + listener = () => { + }; + this.#listener = listener; + try { + this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); + } catch (e) { + if (!e.message?.startsWith("FileNotFound")) + throw e; + const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); + throw notFound.code = "ENOENT", notFound.errno = -2, notFound.path = path, notFound.syscall = "watch", notFound.filename = path, notFound; + } + } + #onEvent(eventType, filenameOrError) { + if (eventType === "error" || eventType === "close") + this.emit(eventType, filenameOrError); + else + this.emit("change", eventType, filenameOrError), this.#listener(eventType, filenameOrError); + } + close() { + this.#watcher?.close(), this.#watcher = null; + } + ref() { + this.#watcher?.ref(); + } + unref() { + this.#watcher?.unref(); + } +} +var access = function access2(...args) { callbackify(fs.accessSync, args); }, appendFile = function appendFile2(...args) { callbackify(fs.appendFileSync, args); @@ -88,7 +129,9 @@ var { direct, isPromise, isCallable } = import.meta.primordials, promises = impo callbackify(fs.utimesSync, args); }, lutimes = function lutimes2(...args) { callbackify(fs.lutimesSync, args); -}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { +}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) { + return new FSWatcher(path, options, listener); +}, promises = import.meta.require("node:fs/promises"), readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { file: void 0, fd: void 0, flags: "r", @@ -590,6 +633,8 @@ var fs_default = { writeSync, WriteStream, ReadStream, + watch, + FSWatcher, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass @@ -600,6 +645,7 @@ export { writeFileSync, writeFile, write, + watch, utimesSync, utimes, unlinkSync, diff --git a/src/js/out/modules/node/fs.promises.js b/src/js/out/modules/node/fs.promises.js index 2780ff166..ef3330771 100644 --- a/src/js/out/modules/node/fs.promises.js +++ b/src/js/out/modules/node/fs.promises.js @@ -1 +1 @@ -var D=Bun.fs(),B="::bunternal::",E={[B]:(S)=>{var b={[B]:function(C,J,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,J(A);return}C(z)}}[B];return async function(...C){return await new Promise((J,q)=>{process.nextTick(b,J,q,C)})}}}[B],G=E(D.accessSync),H=E(D.appendFileSync),I=E(D.closeSync),K=E(D.copyFileSync),L=E(D.existsSync),M=E(D.chownSync),N=E(D.chmodSync),O=E(D.fchmodSync),P=E(D.fchownSync),Q=E(D.fstatSync),R=E(D.fsyncSync),T=E(D.ftruncateSync),U=E(D.futimesSync),V=E(D.lchmodSync),W=E(D.lchownSync),X=E(D.linkSync),Y=E(D.lstatSync),Z=E(D.mkdirSync),_=E(D.mkdtempSync),$=E(D.openSync),x=E(D.readSync),j=E(D.writeSync),v=E(D.readdirSync),w=E(D.readFileSync),k=E(D.writeFileSync),F=E(D.readlinkSync),h=E(D.realpathSync),g=E(D.renameSync),u=E(D.statSync),d=E(D.symlinkSync),n=E(D.truncateSync),l=E(D.unlinkSync),a=E(D.utimesSync),c=E(D.lutimesSync),t=E(D.rmSync),y=E(D.rmdirSync),p={access:G,appendFile:H,close:I,copyFile:K,exists:L,chown:M,chmod:N,fchmod:O,fchown:P,fstat:Q,fsync:R,ftruncate:T,futimes:U,lchmod:V,lchown:W,link:X,lstat:Y,mkdir:Z,mkdtemp:_,open:$,read:x,write:j,readdir:v,readFile:w,writeFile:k,readlink:F,realpath:h,rename:g,stat:u,symlink:d,truncate:n,unlink:l,utimes:a,lutimes:c,rm:t,rmdir:y,constants,[Symbol.for("CommonJS")]:0};export{k as writeFile,j as write,a as utimes,l as unlink,n as truncate,d as symlink,u as stat,y as rmdir,t as rm,g as rename,h as realpath,F as readlink,v as readdir,w as readFile,x as read,$ as open,_ as mkdtemp,Z as mkdir,c as lutimes,Y as lstat,X as link,W as lchown,V as lchmod,U as futimes,T as ftruncate,R as fsync,Q as fstat,P as fchown,O as fchmod,L as exists,p as default,K as copyFile,I as close,M as chown,N as chmod,H as appendFile,G as access}; +function H(S,C={}){const J=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let b=null;if(typeof C==="string")C={encoding:C};return D.watch(S,C||{},(q,z)=>{if(J.push({eventType:q,filename:z}),b){const A=b;b=null,A()}}),{async*[Symbol.asyncIterator](){let q=!1;while(!q){while(J.length){let z=J.shift();if(z.eventType==="close"){q=!0;break}if(z.eventType==="error")throw q=!0,z.filename;yield z}await new Promise((z)=>b=z)}}}}var D=Bun.fs(),B="::bunternal::",G={[B]:(S)=>{var C={[B]:function(J,b,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,b(A);return}J(z)}}[B];return async function(...J){return await new Promise((b,q)=>{process.nextTick(C,b,q,J)})}}}[B],I=G(D.accessSync),K=G(D.appendFileSync),L=G(D.closeSync),M=G(D.copyFileSync),N=G(D.existsSync),O=G(D.chownSync),P=G(D.chmodSync),Q=G(D.fchmodSync),U=G(D.fchownSync),V=G(D.fstatSync),W=G(D.fsyncSync),X=G(D.ftruncateSync),Y=G(D.futimesSync),Z=G(D.lchmodSync),_=G(D.lchownSync),$=G(D.linkSync),T=G(D.lstatSync),E=G(D.mkdirSync),j=G(D.mkdtempSync),R=G(D.openSync),k=G(D.readSync),x=G(D.writeSync),F=G(D.readdirSync),u=G(D.readFileSync),w=G(D.writeFileSync),g=G(D.readlinkSync),h=G(D.realpathSync),d=G(D.renameSync),c=G(D.statSync),v=G(D.symlinkSync),a=G(D.truncateSync),y=G(D.unlinkSync),l=G(D.utimesSync),t=G(D.lutimesSync),p=G(D.rmSync),n=G(D.rmdirSync),m={access:I,appendFile:K,close:L,copyFile:M,exists:N,chown:O,chmod:P,fchmod:Q,fchown:U,fstat:V,fsync:W,ftruncate:X,futimes:Y,lchmod:Z,lchown:_,link:$,lstat:T,mkdir:E,mkdtemp:j,open:R,read:k,write:x,readdir:F,readFile:u,writeFile:w,readlink:g,realpath:h,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:t,rm:p,rmdir:n,watch:H,constants,[Symbol.for("CommonJS")]:0};export{w as writeFile,x as write,H as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,p as rm,d as rename,h as realpath,g as readlink,F as readdir,u as readFile,k as read,R as open,j as mkdtemp,E as mkdir,t as lutimes,T as lstat,$ as link,_ as lchown,Z as lchmod,Y as futimes,X as ftruncate,W as fsync,V as fstat,U as fchown,Q as fchmod,N as exists,m as default,M as copyFile,L as close,O as chown,P as chmod,K as appendFile,I as access}; diff --git a/src/js/private.d.ts b/src/js/private.d.ts index b6ed64801..b689c208e 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -6,11 +6,95 @@ */ declare function $bundleError(error: string); +type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal }; + +type BunWatchEventType = "rename" | "change" | "error" | "close"; +type BunWatchListener = (event: WatchEventType, filename: T | Error | undefined) => void; + +interface BunFSWatcher { + /** + * Stop watching for changes on the given `BunFSWatcher`. Once stopped, the `BunFSWatcher` object is no longer usable. + * @since v0.6.8 + */ + close(): void; + + /** + * When called, requests that the Node.js event loop not exit so long as the is active. Calling watcher.ref() multiple times will have no effect. + */ + ref(): void; + + /** + * When called, the active object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the object's callback is invoked. Calling watcher.unref() multiple times will have no effect. + */ + unref(): void; +} +type BunFS = Omit & { + /** + * Watch for changes on `filename`, where `filename` is either a file or a + * directory. + * + * The second argument is optional. If `options` is provided as a string, it + * specifies the `encoding`. Otherwise `options` should be passed as an object. + * + * The listener callback gets two arguments `(eventType, filename)`. `eventType`is either `'rename'`, `'change', 'error' or 'close'`, and `filename` is the name of the file + * which triggered the event, the error when `eventType` is 'error' or undefined when eventType is 'close'. + * + * On most platforms, `'rename'` is emitted whenever a filename appears or + * disappears in the directory. + * + * + * If a `signal` is passed, aborting the corresponding AbortController will close + * the returned `BunFSWatcher`. + * @since v0.6.8 + * @param listener + */ + watch( + filename: string, + options: + | (WatchOptions & { + encoding: "buffer"; + }) + | "buffer", + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options?: WatchOptions | BufferEncoding | null, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + * @param options Either the encoding for the filename provided to the listener, or an object optionally specifying encoding, persistent, and recursive options. + * If `encoding` is not supplied, the default of `'utf8'` is used. + * If `persistent` is not supplied, the default of `true` is used. + * If `recursive` is not supplied, the default of `false` is used. + */ + watch( + filename: string, + options: BunWatchListener | string, + listener?: BunWatchListener, + ): BunFSWatcher; + /** + * Watch for changes on `filename`, where `filename` is either a file or a directory, returning an `BunFSWatcher`. + * @param filename A path to a file or directory. If a URL is provided, it must use the `file:` protocol. + */ + watch(filename: string, listener?: BunWatchListener): BunFSWatcher; +}; + declare module "bun" { var TOML: { parse(contents: string): any; }; - function fs(): typeof import("node:fs"); + function fs(): BunFS; function _Os(): typeof import("node:os"); function jest(): typeof import("bun:test"); var main: string; diff --git a/src/jsc.zig b/src/jsc.zig index 67cf3f05c..ca31d5f1a 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -50,6 +50,7 @@ pub const FFI = @import("./bun.js/api/ffi.zig").FFI; pub const Node = struct { pub usingnamespace @import("./bun.js/node/types.zig"); pub usingnamespace @import("./bun.js/node/node_fs.zig"); + pub usingnamespace @import("./bun.js/node/node_fs_watcher.zig"); pub usingnamespace @import("./bun.js/node/node_fs_binding.zig"); pub usingnamespace @import("./bun.js/node/node_os.zig"); pub const Syscall = @import("./bun.js/node/syscall.zig"); diff --git a/src/watcher.zig b/src/watcher.zig index 155c0b473..044770dc4 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -108,6 +108,10 @@ pub const INotify = struct { std.os.inotify_rm_watch(inotify_fd, wd); } + pub fn isRunning() bool { + return loaded_inotify; + } + var coalesce_interval: isize = 100_000; pub fn init() !void { std.debug.assert(!loaded_inotify); @@ -229,6 +233,10 @@ const DarwinWatcher = struct { if (fd == 0) return error.KQueueError; } + pub fn isRunning() bool { + return fd != 0; + } + pub fn stop() void { if (fd != 0) { std.os.close(fd); @@ -361,6 +369,8 @@ pub fn NewWatcher(comptime ContextType: type) type { watchloop_handle: ?std.Thread.Id = null, cwd: string, thread: std.Thread = undefined, + running: bool = true, + close_descriptors: bool = false, pub const HashType = u32; @@ -372,7 +382,9 @@ pub fn NewWatcher(comptime ContextType: type) type { pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { var watcher = try allocator.create(Watcher); - try PlatformWatcher.init(); + if (!PlatformWatcher.isRunning()) { + try PlatformWatcher.init(); + } watcher.* = Watcher{ .fs = fs, @@ -393,6 +405,26 @@ pub fn NewWatcher(comptime ContextType: type) type { this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } + pub fn deinit(this: *Watcher, close_descriptors: bool) void { + this.mutex.lock(); + defer this.mutex.unlock(); + + this.close_descriptors = close_descriptors; + if (this.watchloop_handle != null) { + this.running = false; + } else { + if (this.close_descriptors and this.running) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); + const allocator = this.allocator; + allocator.destroy(this); + } + } + // This must only be called from the watcher thread pub fn watchLoop(this: *Watcher) !void { this.watchloop_handle = std.Thread.getCurrentId(); @@ -402,12 +434,24 @@ pub fn NewWatcher(comptime ContextType: type) type { if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); this._watchLoop() catch |err| { - Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); - this.watchloop_handle = null; PlatformWatcher.stop(); - return; + if (this.running) { + this.ctx.onError(err); + } }; + + // deinit and close descriptors if needed + if (this.close_descriptors) { + const fds = this.watchlist.items(.fd); + for (fds) |fd| { + std.os.close(fd); + } + } + this.watchlist.deinit(this.allocator); + + const allocator = this.allocator; + allocator.destroy(this); } var evict_list_i: WatchItemIndex = 0; @@ -475,7 +519,7 @@ pub fn NewWatcher(comptime ContextType: type) type { var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); var changelist = &changelist_array; - while (true) { + while (this.running) { defer Output.flush(); var count_ = std.os.system.kevent( @@ -530,11 +574,12 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + if (this.running) { + this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + } } } else if (Environment.isLinux) { - restart: while (true) { + restart: while (this.running) { defer Output.flush(); var events = try INotify.read(); @@ -600,9 +645,10 @@ pub fn NewWatcher(comptime ContextType: type) type { this.mutex.lock(); defer this.mutex.unlock(); - - this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); - remaining_events -= slice.len; + if (this.running) { + this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); + remaining_events -= slice.len; + } } } } diff --git a/test/js/node/watch/fixtures/close.js b/test/js/node/watch/fixtures/close.js new file mode 100644 index 000000000..8eeeb79a3 --- /dev/null +++ b/test/js/node/watch/fixtures/close.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .close(); diff --git a/test/js/node/watch/fixtures/persistent.js b/test/js/node/watch/fixtures/persistent.js new file mode 100644 index 000000000..72a2b6564 --- /dev/null +++ b/test/js/node/watch/fixtures/persistent.js @@ -0,0 +1,5 @@ +import fs from "fs"; +fs.watch(import.meta.path, { persistent: false, signal: AbortSignal.timeout(4000) }).on("error", err => { + console.error(err.message); + process.exit(1); +}); diff --git a/test/js/node/watch/fixtures/relative.js b/test/js/node/watch/fixtures/relative.js new file mode 100644 index 000000000..26e09da1a --- /dev/null +++ b/test/js/node/watch/fixtures/relative.js @@ -0,0 +1,23 @@ +import fs from "fs"; +const watcher = fs.watch("relative.txt", { signal: AbortSignal.timeout(2000) }); + +watcher.on("change", function (event, filename) { + if (filename !== "relative.txt" && event !== "change") { + console.error("fail"); + clearInterval(interval); + watcher.close(); + process.exit(1); + } else { + clearInterval(interval); + watcher.close(); + } +}); +watcher.on("error", err => { + clearInterval(interval); + console.error(err.message); + process.exit(1); +}); + +const interval = setInterval(() => { + fs.writeFileSync("relative.txt", "world"); +}, 10); diff --git a/test/js/node/watch/fixtures/unref.js b/test/js/node/watch/fixtures/unref.js new file mode 100644 index 000000000..a0c506a04 --- /dev/null +++ b/test/js/node/watch/fixtures/unref.js @@ -0,0 +1,7 @@ +import fs from "fs"; +fs.watch(import.meta.path, { signal: AbortSignal.timeout(4000) }) + .on("error", err => { + console.error(err.message); + process.exit(1); + }) + .unref(); diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js new file mode 100644 index 000000000..56e1798f1 --- /dev/null +++ b/test/js/node/watch/fs.watch.test.js @@ -0,0 +1,424 @@ +import fs from "fs"; +import path from "path"; +import { tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; +import { pathToFileURL } from "bun"; + +import { describe, expect, test } from "bun:test"; +// Because macOS (and possibly other operating systems) can return a watcher +// before it is actually watching, we need to repeat the operation to avoid +// a race condition. +function repeat(fn) { + const interval = setInterval(fn, 20); + return interval; +} +const encodingFileName = `新建文夹件.txt`; +const testDir = tempDirWithFiles("watch", { + "watch.txt": "hello", + "relative.txt": "hello", + "abort.txt": "hello", + "url.txt": "hello", + [encodingFileName]: "hello", +}); + +describe("fs.watch", () => { + test("non-persistent watcher should not block the event loop", done => { + try { + // https://github.com/joyent/node/issues/2293 - non-persistent watcher should not block the event loop + bunRun(path.join(import.meta.dir, "fixtures", "persistent.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("watcher should close and not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "close.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("unref watcher should not block the event loop", done => { + try { + bunRun(path.join(import.meta.dir, "fixtures", "unref.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("should work with relative files", done => { + try { + bunRunAsScript(testDir, path.join(import.meta.dir, "fixtures", "relative.js")); + done(); + } catch (e) { + done(e); + } + }); + + test("add file/folder to folder", done => { + let count = 0; + const root = path.join(testDir, "add-directory"); + try { + fs.mkdirSync(root); + } catch {} + let err = undefined; + const watcher = fs.watch(root, { signal: AbortSignal.timeout(3000) }); + watcher.on("change", (event, filename) => { + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(filename); + if (count >= 2) { + watcher.close(); + } + } catch (e) { + err = e; + watcher.close(); + } + }); + + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + }); + + test("add file/folder to subfolder", done => { + let count = 0; + const root = path.join(testDir, "add-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + const watcher = fs.watch(root, { recursive: true, signal: AbortSignal.timeout(3000) }); + let err = undefined; + watcher.on("change", (event, filename) => { + const basename = path.basename(filename); + if (basename === "subfolder") return; + count++; + try { + expect(event).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + if (count >= 2) { + watcher.close(); + } + } catch (e) { + err = e; + watcher.close(); + } + }); + watcher.on("error", e => (err = e)); + watcher.on("close", () => { + clearInterval(interval); + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + }); + + test("should emit event when file is deleted", done => { + const testsubdir = tempDirWithFiles("subdir", { + "deleted.txt": "hello", + }); + const filepath = path.join(testsubdir, "deleted.txt"); + let err = undefined; + const watcher = fs.watch(testsubdir, function (event, filename) { + try { + expect(event).toBe("rename"); + expect(filename).toBe("deleted.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.rmSync(filepath, { force: true }); + const fd = fs.openSync(filepath, "w"); + fs.closeSync(fd); + }); + }); + + test("should emit 'change' event when file is modified", done => { + const filepath = path.join(testDir, "watch.txt"); + + const watcher = fs.watch(filepath); + let err = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("watch.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + }); + + test("should error on invalid path", done => { + try { + fs.watch(path.join(testDir, "404.txt")); + done(new Error("should not reach here")); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.code).toBe("ENOENT"); + expect(err.syscall).toBe("watch"); + done(); + } + }); + + const encodings = ["utf8", "buffer", "hex", "ascii", "base64", "utf16le", "ucs2", "latin1", "binary"]; + + test(`should work with encodings ${encodings.join(", ")}`, async () => { + const watchers = []; + const filepath = path.join(testDir, encodingFileName); + + const promises = []; + encodings.forEach(name => { + const encoded_filename = + name !== "buffer" ? Buffer.from(encodingFileName, "utf8").toString(name) : Buffer.from(encodingFileName); + + promises.push( + new Promise((resolve, reject) => { + watchers.push( + fs.watch(filepath, { encoding: name }, (event, filename) => { + try { + expect(event).toBe("change"); + + if (name !== "buffer") { + expect(filename).toBe(encoded_filename); + } else { + expect(filename).toBeInstanceOf(Buffer); + expect(filename.toString("utf8")).toBe(encodingFileName); + } + + resolve(); + } catch (e) { + reject(e); + } + }), + ); + }), + ); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + + try { + await Promise.all(promises); + } finally { + clearInterval(interval); + watchers.forEach(watcher => watcher.close()); + } + }); + + test("should work with url", done => { + const filepath = path.join(testDir, "url.txt"); + try { + const watcher = fs.watch(pathToFileURL(filepath)); + let err = undefined; + watcher.on("change", function (event, filename) { + try { + expect(event).toBe("change"); + expect(filename).toBe("url.txt"); + } catch (e) { + err = e; + } finally { + clearInterval(interval); + watcher.close(); + } + }); + + watcher.once("close", () => { + done(err); + }); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "world"); + }); + } catch (e) { + done(e); + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const promise = new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal: ac.signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + await new Promise((resolve, reject) => { + const watcher = fs.watch(filepath, { signal }); + watcher.once("error", err => (err.message === "The operation was aborted." ? resolve() : reject(err))); + watcher.once("close", () => reject()); + }); + }); +}); + +describe("fs.promises.watchFile", () => { + test("add file/folder to folder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-directory"); + try { + fs.mkdirSync(root); + } catch {} + let success = false; + let err = undefined; + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(root, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(root, "new-folder.txt")); + fs.rmdirSync(path.join(root, "new-folder.txt")); + }); + + for await (const event of watcher) { + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(event.filename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); + + test("add file/folder to subfolder", async () => { + let count = 0; + const root = path.join(testDir, "add-promise-subdirectory"); + try { + fs.mkdirSync(root); + } catch {} + const subfolder = path.join(root, "subfolder"); + fs.mkdirSync(subfolder); + let success = false; + let err = undefined; + + try { + const ac = new AbortController(); + const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal }); + + const interval = repeat(() => { + fs.writeFileSync(path.join(subfolder, "new-file.txt"), "hello"); + fs.mkdirSync(path.join(subfolder, "new-folder.txt")); + fs.rmdirSync(path.join(subfolder, "new-folder.txt")); + }); + for await (const event of watcher) { + const basename = path.basename(event.filename); + if (basename === "subfolder") continue; + + count++; + try { + expect(event.eventType).toBe("rename"); + expect(["new-file.txt", "new-folder.txt"]).toContain(basename); + + if (count >= 2) { + success = true; + clearInterval(interval); + ac.abort(); + } + } catch (e) { + err = e; + clearInterval(interval); + ac.abort(); + } + } + } catch (e) { + if (!success) { + throw err || e; + } + } + }); + + test("Signal aborted after creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const ac = new AbortController(); + const watcher = fs.promises.watch(filepath, { signal: ac.signal }); + + const promise = (async () => { + try { + for await (const _ of watcher); + } catch (e) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + await Bun.sleep(10); + ac.abort(); + await promise; + }); + + test("Signal aborted before creating the watcher", async () => { + const filepath = path.join(testDir, "abort.txt"); + + const signal = AbortSignal.abort(); + const watcher = fs.promises.watch(filepath, { signal }); + await (async () => { + try { + for await (const _ of watcher); + } catch (e) { + expect(e.message).toBe("The operation was aborted."); + } + })(); + }); +}); -- cgit v1.2.3 From 3ed28f2828a29129a1791b7a4f6935d842d6493c Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Sun, 25 Jun 2023 20:16:25 -0300 Subject: [fs.watch] fix reference/deinit (#3396) * fix js reference * fix close oops * refactor + hasPendingActivity * fmt * fix race conditions * fixup * add test calling close on error event * fix close inside close + test * cleanup --- src/bun.js/WebKit | 2 +- src/bun.js/bindings/JSSink.cpp | 2 +- src/bun.js/bindings/JSSink.h | 2 +- src/bun.js/bindings/ZigGeneratedClasses.cpp | 10 + src/bun.js/bindings/ZigGeneratedClasses.h | 27 ++ src/bun.js/bindings/generated_classes.zig | 1 + src/bun.js/bindings/generated_classes_list.zig | 2 +- src/bun.js/node/node.classes.ts | 1 + src/bun.js/node/node_fs_watcher.zig | 500 +++++++++++++------------ src/watcher.zig | 20 +- test/js/node/watch/fs.watch.test.js | 45 ++- 11 files changed, 352 insertions(+), 260 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 4c8ab8fdf..b2f1006a0 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 4c8ab8fdfb102522fdd8e55d4eea53e8ce2755c2 +Subproject commit b2f1006a06f81bc860c89dd4c7cec3e7117c4c4c diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index 4acf01ff7..19bf05599 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z +// Generated by 'make generate-sink' at 2023-06-25T17:34:54.187Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 37c458e9b..9bf5554c4 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-06-14T21:38:04.394Z +// Generated by 'make generate-sink' at 2023-06-25T17:34:54.186Z // #pragma once diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index e0a3f33d6..387580d54 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -5565,6 +5565,12 @@ void JSFSWatcherPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* glob JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } +extern "C" bool FSWatcher__hasPendingActivity(void* ptr); +bool JSFSWatcher::hasPendingActivity(void* ctx) +{ + return FSWatcher__hasPendingActivity(ctx); +} + JSFSWatcher::~JSFSWatcher() { if (m_ctx) { @@ -5649,6 +5655,8 @@ void JSFSWatcher::visitChildrenImpl(JSCell* cell, Visitor& visitor) ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_listener); + + visitor.addOpaqueRoot(thisObject->wrapped()); } DEFINE_VISIT_CHILDREN(JSFSWatcher); @@ -5659,6 +5667,8 @@ void JSFSWatcher::visitAdditionalChildren(Visitor& visitor) JSFSWatcher* thisObject = this; ASSERT_GC_OBJECT_INHERITS(thisObject, info()); visitor.append(thisObject->m_listener); + + visitor.addOpaqueRoot(this->wrapped()); } DEFINE_VISIT_ADDITIONAL_CHILDREN(JSFSWatcher); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index 3fa0e26d2..1631f960e 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -623,10 +623,37 @@ public: : Base(vm, structure) { m_ctx = sinkPtr; + m_weakThis = JSC::Weak(this, getOwner()); } void finishCreation(JSC::VM&); + JSC::Weak m_weakThis; + + static bool hasPendingActivity(void* ctx); + + class Owner final : public JSC::WeakHandleOwner { + public: + bool isReachableFromOpaqueRoots(JSC::Handle handle, void* context, JSC::AbstractSlotVisitor& visitor, const char** reason) final + { + auto* controller = JSC::jsCast(handle.slot()->asCell()); + if (JSFSWatcher::hasPendingActivity(controller->wrapped())) { + if (UNLIKELY(reason)) + *reason = "has pending activity"; + return true; + } + + return visitor.containsOpaqueRoot(context); + } + void finalize(JSC::Handle, void* context) final {} + }; + + static JSC::WeakHandleOwner* getOwner() + { + static NeverDestroyed m_owner; + return &m_owner.get(); + } + DECLARE_VISIT_CHILDREN; template void visitAdditionalChildren(Visitor&); DECLARE_VISIT_OUTPUT_CONSTRAINTS; diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 74e30cd83..bdde69c1a 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -1492,6 +1492,7 @@ pub const JSFSWatcher = struct { @export(FSWatcher.doRef, .{ .name = "FSWatcherPrototype__doRef" }); @export(FSWatcher.doUnref, .{ .name = "FSWatcherPrototype__doUnref" }); @export(FSWatcher.finalize, .{ .name = "FSWatcherClass__finalize" }); + @export(FSWatcher.hasPendingActivity, .{ .name = "FSWatcher__hasPendingActivity" }); @export(FSWatcher.hasRef, .{ .name = "FSWatcherPrototype__hasRef" }); } } diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index d90267337..543d492b5 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -37,5 +37,5 @@ pub const Classes = struct { pub const BuildArtifact = JSC.API.BuildArtifact; pub const BuildMessage = JSC.BuildMessage; pub const ResolveMessage = JSC.ResolveMessage; - pub const FSWatcher = JSC.Node.FSWatcher.JSObject; + pub const FSWatcher = JSC.Node.FSWatcher; }; diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index ce35c940a..2efad5245 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -7,6 +7,7 @@ export default [ noConstructor: true, finalize: true, configurable: false, + hasPendingActivity: true, klass: {}, JSType: "0b11101110", proto: { diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 397d51916..b1f4ec8a9 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -4,6 +4,7 @@ const bun = @import("root").bun; const Fs = @import("../../fs.zig"); const Path = @import("../../resolver/resolve_path.zig"); const Encoder = JSC.WebCore.Encoder; +const Mutex = @import("../../lock.zig").Lock; const FSEvents = @import("./fs_events.zig"); @@ -30,17 +31,28 @@ pub const FSWatcher = struct { onAccept: std.ArrayHashMapUnmanaged(FSWatcher.Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, ctx: *VirtualMachine, - js_watcher: ?*JSObject = null, - watcher_instance: ?*FSWatcher.Watcher = null, verbose: bool = false, file_paths: bun.BabyList(string) = .{}, entry_path: ?string = null, entry_dir: string = "", last_change_event: ChangeEvent = .{}, - pub fn toJS(this: *FSWatcher) JSC.JSValue { - return if (this.js_watcher) |js| js.js_this else JSC.JSValue.jsUndefined(); - } + // JSObject + mutex: Mutex, + signal: ?*JSC.AbortSignal, + persistent: bool, + default_watcher: ?*FSWatcher.Watcher, + fsevents_watcher: ?*FSEvents.FSEventsWatcher, + poll_ref: JSC.PollRef = .{}, + globalThis: *JSC.JSGlobalObject, + js_this: JSC.JSValue, + encoding: JSC.Node.Encoding, + // user can call close and pre-detach so we need to track this + closed: bool, + // counts pending tasks so we only deinit after all tasks are done + task_count: u32, + has_pending_activity: std.atomic.Atomic(bool), + pub usingnamespace JSC.Codegen.JSFSWatcher; pub fn eventLoop(this: FSWatcher) *EventLoop { return this.ctx.eventLoop(); @@ -51,6 +63,9 @@ pub const FSWatcher = struct { } pub fn deinit(this: *FSWatcher) void { + // stop all managers and signals + this.detach(); + while (this.file_paths.popOrNull()) |file_path| { bun.default_allocator.destroy(file_path); } @@ -107,41 +122,47 @@ pub const FSWatcher = struct { } pub fn run(this: *FSWatchTask) void { - // this runs on JS Context - if (this.ctx.js_watcher) |js_watcher| { - for (this.entries[0..this.count]) |entry| { - switch (entry.event_type) { - .rename => { - js_watcher.emit(entry.file_path, "rename"); - }, - .change => { - js_watcher.emit(entry.file_path, "change"); - }, - .@"error" => { - // file_path is the error message in this case - js_watcher.emitError(entry.file_path); - }, - .abort => { - js_watcher.emitIfAborted(); - }, - } + // this runs on JS Context Thread + + for (this.entries[0..this.count]) |entry| { + switch (entry.event_type) { + .rename => { + this.ctx.emit(entry.file_path, "rename"); + }, + .change => { + this.ctx.emit(entry.file_path, "change"); + }, + .@"error" => { + // file_path is the error message in this case + this.ctx.emitError(entry.file_path); + }, + .abort => { + this.ctx.emitIfAborted(); + }, } } + + this.ctx.unrefTask(); } pub fn enqueue(this: *FSWatchTask) void { if (this.count == 0) return; - var that = bun.default_allocator.create(FSWatchTask) catch unreachable; + // if false is closed or detached (can still contain valid refs but will not create a new one) + if (this.ctx.refTask()) { + var that = bun.default_allocator.create(FSWatchTask) catch unreachable; - that.* = this.*; - this.count = 0; - that.concurrent_task.task = JSC.Task.init(that); - this.ctx.enqueueTaskConcurrent(&that.concurrent_task); + that.* = this.*; + this.count = 0; + that.concurrent_task.task = JSC.Task.init(that); + this.ctx.enqueueTaskConcurrent(&that.concurrent_task); + return; + } + // closed or detached so just cleanEntries + this.cleanEntries(); } - - pub fn deinit(this: *FSWatchTask) void { + pub fn cleanEntries(this: *FSWatchTask) void { while (this.count > 0) { this.count -= 1; switch (this.entries[this.count].free_type) { @@ -150,6 +171,10 @@ pub const FSWatcher = struct { else => {}, } } + } + + pub fn deinit(this: *FSWatchTask) void { + this.cleanEntries(); bun.default_allocator.destroy(this); } }; @@ -275,7 +300,7 @@ pub const FSWatcher = struct { const kinds = slice.items(.kind); var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var ctx = this.watcher_instance.?; + var ctx = this.default_watcher.?; defer ctx.flushEvictions(); defer Output.flush(); @@ -540,241 +565,225 @@ pub const FSWatcher = struct { pub fn createFSWatcher(this: Arguments) !JSC.JSValue { const obj = try FSWatcher.init(this); - return obj.toJS(); + if (obj.js_this != .zero) { + return obj.js_this; + } + return JSC.JSValue.jsUndefined(); } }; - pub const JSObject = struct { - signal: ?*JSC.AbortSignal, - persistent: bool, - manager: ?*FSWatcher.Watcher, - fsevents_watcher: ?*FSEvents.FSEventsWatcher, - poll_ref: JSC.PollRef = .{}, - globalThis: ?*JSC.JSGlobalObject, - js_this: JSC.JSValue, - encoding: JSC.Node.Encoding, - closed: bool, - - pub usingnamespace JSC.Codegen.JSFSWatcher; - - pub fn getFSWatcher(this: *JSObject) *FSWatcher { - if (this.manager) |manager| return manager.ctx; - if (this.fsevents_watcher) |manager| return bun.cast(*FSWatcher, manager.ctx.?); - - @panic("No context attached to JSFSWatcher"); + pub fn initJS(this: *FSWatcher, listener: JSC.JSValue) void { + if (this.persistent) { + this.poll_ref.ref(this.ctx); } - pub fn init(globalThis: *JSC.JSGlobalObject, manager: ?*FSWatcher.Watcher, fsevents_watcher: ?*FSEvents.FSEventsWatcher, signal: ?*JSC.AbortSignal, listener: JSC.JSValue, persistent: bool, encoding: JSC.Node.Encoding) !*JSObject { - var obj = try globalThis.allocator().create(JSObject); - obj.* = .{ - .signal = null, - .persistent = persistent, - .manager = manager, - .fsevents_watcher = fsevents_watcher, - .globalThis = globalThis, - .js_this = .zero, - .encoding = encoding, - .closed = false, - }; - const instance = obj.getFSWatcher(); - - if (persistent) { - obj.poll_ref.ref(instance.ctx); - } - - var js_this = JSObject.toJS(obj, globalThis); - JSObject.listenerSetCached(js_this, globalThis, listener); - obj.js_this = js_this; - obj.js_this.protect(); - - if (signal) |s| { - - // already aborted? - if (s.aborted()) { - obj.signal = s.ref(); - // abort next tick - var current_task: FSWatchTask = .{ - .ctx = instance, - }; - current_task.append("", .abort, .none); - current_task.enqueue(); - } else { - // watch for abortion - obj.signal = s.ref().listen(JSObject, obj, JSObject.emitAbort); - } + const js_this = FSWatcher.toJS(this, this.globalThis); + js_this.ensureStillAlive(); + this.js_this = js_this; + FSWatcher.listenerSetCached(js_this, this.globalThis, listener); + + if (this.signal) |s| { + // already aborted? + if (s.aborted()) { + // safely abort next tick + var current_task: FSWatchTask = .{ + .ctx = this, + }; + current_task.append("", .abort, .none); + current_task.enqueue(); + } else { + // watch for abortion + this.signal = s.listen(FSWatcher, this, FSWatcher.emitAbort); } - return obj; } + } - pub fn emitIfAborted(this: *JSObject) void { - if (this.signal) |s| { - if (s.aborted()) { - const err = s.abortReason(); - this.emitAbort(err); - } + pub fn emitIfAborted(this: *FSWatcher) void { + if (this.signal) |s| { + if (s.aborted()) { + const err = s.abortReason(); + this.emitAbort(err); } } + } - pub fn emitAbort(this: *JSObject, err: JSC.JSValue) void { - if (this.closed) return; - defer this.close(true); - - err.ensureStillAlive(); - - if (this.globalThis) |globalThis| { - if (this.js_this != .zero) { - if (JSObject.listenerGetCached(this.js_this)) |listener| { - var args = [_]JSC.JSValue{ - JSC.ZigString.static("error").toValue(globalThis), - if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, globalThis) else err, - }; - _ = listener.callWithGlobalThis( - globalThis, - &args, - ); - } - } + pub fn emitAbort(this: *FSWatcher, err: JSC.JSValue) void { + if (this.closed) return; + defer this.close(); + + err.ensureStillAlive(); + if (this.js_this != .zero) { + const js_this = this.js_this; + js_this.ensureStillAlive(); + if (FSWatcher.listenerGetCached(js_this)) |listener| { + listener.ensureStillAlive(); + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(this.globalThis), + if (err.isEmptyOrUndefinedOrNull()) JSC.WebCore.AbortSignal.createAbortError(JSC.ZigString.static("The user aborted a request"), &JSC.ZigString.Empty, this.globalThis) else err, + }; + _ = listener.callWithGlobalThis( + this.globalThis, + &args, + ); } } - pub fn emitError(this: *JSObject, err: string) void { - if (this.closed) return; - defer this.close(true); - - if (this.globalThis) |globalThis| { - if (this.js_this != .zero) { - if (JSObject.listenerGetCached(this.js_this)) |listener| { - var args = [_]JSC.JSValue{ - JSC.ZigString.static("error").toValue(globalThis), - JSC.ZigString.fromUTF8(err).toErrorInstance(globalThis), - }; - _ = listener.callWithGlobalThis( - globalThis, - &args, - ); - } - } + } + pub fn emitError(this: *FSWatcher, err: string) void { + if (this.closed) return; + defer this.close(); + + if (this.js_this != .zero) { + const js_this = this.js_this; + js_this.ensureStillAlive(); + if (FSWatcher.listenerGetCached(js_this)) |listener| { + listener.ensureStillAlive(); + var args = [_]JSC.JSValue{ + JSC.ZigString.static("error").toValue(this.globalThis), + JSC.ZigString.fromUTF8(err).toErrorInstance(this.globalThis), + }; + _ = listener.callWithGlobalThis( + this.globalThis, + &args, + ); } } + } - pub fn emit(this: *JSObject, file_name: string, comptime eventType: string) void { - if (this.globalThis) |globalThis| { - if (this.js_this != .zero) { - if (JSObject.listenerGetCached(this.js_this)) |listener| { - var filename: JSC.JSValue = JSC.JSValue.jsUndefined(); - if (file_name.len > 0) { - if (this.encoding == .buffer) - filename = JSC.ArrayBuffer.createBuffer(globalThis, file_name) - else if (this.encoding == .utf8) { - filename = JSC.ZigString.fromUTF8(file_name).toValueGC(globalThis); - } else { - // convert to desired encoding - filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, globalThis, this.encoding); - } - } - var args = [_]JSC.JSValue{ - JSC.ZigString.static(eventType).toValue(globalThis), - filename, - }; - _ = listener.callWithGlobalThis( - globalThis, - &args, - ); + pub fn emit(this: *FSWatcher, file_name: string, comptime eventType: string) void { + if (this.js_this != .zero) { + const js_this = this.js_this; + js_this.ensureStillAlive(); + if (FSWatcher.listenerGetCached(js_this)) |listener| { + listener.ensureStillAlive(); + var filename: JSC.JSValue = JSC.JSValue.jsUndefined(); + if (file_name.len > 0) { + if (this.encoding == .buffer) + filename = JSC.ArrayBuffer.createBuffer(this.globalThis, file_name) + else if (this.encoding == .utf8) { + filename = JSC.ZigString.fromUTF8(file_name).toValueGC(this.globalThis); + } else { + // convert to desired encoding + filename = Encoder.toStringAtRuntime(file_name.ptr, file_name.len, this.globalThis, this.encoding); } } + var args = [_]JSC.JSValue{ + JSC.ZigString.static(eventType).toValue(this.globalThis), + filename, + }; + _ = listener.callWithGlobalThis( + this.globalThis, + &args, + ); } } + } - pub fn ref(this: *JSObject) void { - if (this.closed) return; - - if (!this.persistent) { - this.persistent = true; - this.poll_ref.ref(this.getFSWatcher().ctx); - } - } - - pub fn doRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - this.ref(); - return JSC.JSValue.jsUndefined(); + pub fn doRef(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + if (!this.closed and !this.persistent) { + this.persistent = true; + this.poll_ref.ref(this.ctx); } + return JSC.JSValue.jsUndefined(); + } - pub fn unref(this: *JSObject) void { - if (this.persistent) { - this.persistent = false; - this.poll_ref.unref(this.getFSWatcher().ctx); - } + pub fn doUnref(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.ctx); } + return JSC.JSValue.jsUndefined(); + } - pub fn doUnref(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - this.unref(); - return JSC.JSValue.jsUndefined(); - } + pub fn hasRef(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + return JSC.JSValue.jsBoolean(this.persistent); + } - pub fn hasRef(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - return JSC.JSValue.jsBoolean(this.persistent); - } + // this can be called from Watcher Thread or JS Context Thread + pub fn refTask(this: *FSWatcher) bool { + this.mutex.lock(); + defer this.mutex.unlock(); + // stop new references + if (this.closed) return false; + this.task_count += 1; + return true; + } - pub fn close( - this: *JSObject, - emitEvent: bool, - ) void { - if (!this.closed) { - if (this.signal) |signal| { - this.signal = null; - signal.detach(this); - } - this.closed = true; - if (emitEvent) { - this.emit("", "close"); - } + pub fn hasPendingActivity(this: *FSWatcher) callconv(.C) bool { + @fence(.Acquire); + return this.has_pending_activity.load(.Acquire); + } + // only called from Main Thread + pub fn updateHasPendingActivity(this: *FSWatcher) void { + @fence(.Release); + this.has_pending_activity.store(false, .Release); + } - this.detach(); - } + // unref is always called on main JS Context Thread + pub fn unrefTask(this: *FSWatcher) void { + this.mutex.lock(); + defer this.mutex.unlock(); + this.task_count -= 1; + if (this.closed and this.task_count == 0) { + this.updateHasPendingActivity(); } + } - pub fn detach(this: *JSObject) void { - this.unref(); + pub fn close( + this: *FSWatcher, + ) void { + this.mutex.lock(); + if (!this.closed) { + this.closed = true; - if (this.js_this != .zero) { - this.js_this.unprotect(); - this.js_this = .zero; - } + // emit should only be called unlocked + this.mutex.unlock(); - this.globalThis = null; + this.emit("", "close"); + // we immediately detach here + this.detach(); - if (this.signal) |signal| { - this.signal = null; - signal.detach(this); - } - if (this.manager) |manager| { - var ctx = manager.ctx; - this.manager = null; - ctx.js_watcher = null; - ctx.deinit(); - manager.deinit(true); + // no need to lock again, because ref checks closed and unref is only called on main thread + if (this.task_count == 0) { + this.updateHasPendingActivity(); } + } else { + this.mutex.unlock(); + } + } - if (this.fsevents_watcher) |manager| { - var ctx = bun.cast(*FSWatcher, manager.ctx.?); - ctx.js_watcher = null; - ctx.deinit(); - manager.deinit(); - } + // this can be called multiple times + pub fn detach(this: *FSWatcher) void { + if (this.persistent) { + this.persistent = false; + this.poll_ref.unref(this.ctx); } - pub fn doClose(this: *JSObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - this.close(true); - return JSC.JSValue.jsUndefined(); + if (this.signal) |signal| { + this.signal = null; + signal.detach(this); } - pub fn finalize(this: *JSObject) callconv(.C) void { - if (!this.closed) { - this.detach(); - } + if (this.default_watcher) |default_watcher| { + this.default_watcher = null; + default_watcher.deinit(true); + } - bun.default_allocator.destroy(this); + if (this.fsevents_watcher) |fsevents_watcher| { + this.fsevents_watcher = null; + fsevents_watcher.deinit(); } - }; + + this.js_this = .zero; + } + + pub fn doClose(this: *FSWatcher, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { + this.close(); + return JSC.JSValue.jsUndefined(); + } + + pub fn finalize(this: *FSWatcher) callconv(.C) void { + this.deinit(); + } const PathResult = struct { fd: StoredFileDescriptorType = 0, @@ -837,6 +846,17 @@ pub const FSWatcher = struct { const vm = args.global_this.bunVM(); ctx.* = .{ .ctx = vm, + .mutex = Mutex.init(), + .signal = if (args.signal) |s| s.ref() else null, + .persistent = args.persistent, + .default_watcher = null, + .fsevents_watcher = null, + .globalThis = args.global_this, + .js_this = .zero, + .encoding = args.encoding, + .closed = false, + .task_count = 0, + .has_pending_activity = std.atomic.Atomic(bool).init(true), .verbose = args.verbose, .file_paths = bun.BabyList(string).initCapacity(bun.default_allocator, 1) catch |err| { ctx.deinit(); @@ -850,22 +870,17 @@ pub const FSWatcher = struct { ctx.entry_path = dir_path_clone; ctx.entry_dir = dir_path_clone; - var fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| { + ctx.fsevents_watcher = FSEvents.watch(dir_path_clone, args.recursive, onFSEventUpdate, bun.cast(*anyopaque, ctx)) catch |err| { ctx.deinit(); return err; }; - ctx.js_watcher = JSObject.init(args.global_this, null, fsevents_watcher, args.signal, args.listener, args.persistent, args.encoding) catch |err| { - ctx.deinit(); - fsevents_watcher.deinit(); - return err; - }; - + ctx.initJS(args.listener); return ctx; } } - var fs_watcher = FSWatcher.Watcher.init( + var default_watcher = FSWatcher.Watcher.init( ctx, vm.bundler.fs, bun.default_allocator, @@ -874,7 +889,7 @@ pub const FSWatcher = struct { return err; }; - ctx.watcher_instance = fs_watcher; + ctx.default_watcher = default_watcher; if (fs_type.is_file) { var file_path_clone = bun.default_allocator.dupeZ(u8, file_path) catch unreachable; @@ -882,32 +897,23 @@ pub const FSWatcher = struct { ctx.entry_path = file_path_clone; ctx.entry_dir = std.fs.path.dirname(file_path_clone) orelse file_path_clone; - fs_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { + default_watcher.addFile(fs_type.fd, file_path_clone, FSWatcher.Watcher.getHash(file_path), options.Loader.file, 0, null, false) catch |err| { ctx.deinit(); - fs_watcher.deinit(true); return err; }; } else { - addDirectory(ctx, fs_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| { + addDirectory(ctx, default_watcher, fs_type.fd, file_path, args.recursive, &buf, true) catch |err| { ctx.deinit(); - fs_watcher.deinit(true); return err; }; } - fs_watcher.start() catch |err| { + default_watcher.start() catch |err| { ctx.deinit(); - - fs_watcher.deinit(true); - return err; - }; - - ctx.js_watcher = JSObject.init(args.global_this, fs_watcher, null, args.signal, args.listener, args.persistent, args.encoding) catch |err| { - ctx.deinit(); - fs_watcher.deinit(true); return err; }; + ctx.initJS(args.listener); return ctx; } }; diff --git a/src/watcher.zig b/src/watcher.zig index 044770dc4..e3b3600ad 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -519,7 +519,7 @@ pub fn NewWatcher(comptime ContextType: type) type { var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); var changelist = &changelist_array; - while (this.running) { + while (true) { defer Output.flush(); var count_ = std.os.system.kevent( @@ -576,10 +576,12 @@ pub fn NewWatcher(comptime ContextType: type) type { defer this.mutex.unlock(); if (this.running) { this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); + } else { + break; } } } else if (Environment.isLinux) { - restart: while (this.running) { + restart: while (true) { defer Output.flush(); var events = try INotify.read(); @@ -588,14 +590,14 @@ pub fn NewWatcher(comptime ContextType: type) type { // TODO: is this thread safe? var remaining_events = events.len; - var name_off: u8 = 0; - var temp_name_list: [128]?[:0]u8 = undefined; - var temp_name_off: u8 = 0; - const eventlist_index = this.watchlist.items(.eventlist_index); while (remaining_events > 0) { - const slice = events[0..@min(remaining_events, this.watch_events.len)]; + var name_off: u8 = 0; + var temp_name_list: [128]?[:0]u8 = undefined; + var temp_name_off: u8 = 0; + + const slice = events[0..@min(128, remaining_events, this.watch_events.len)]; var watchevents = this.watch_events[0..slice.len]; var watch_event_id: u32 = 0; for (slice) |event| { @@ -647,8 +649,10 @@ pub fn NewWatcher(comptime ContextType: type) type { defer this.mutex.unlock(); if (this.running) { this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0 .. name_off + 1], this.watchlist); - remaining_events -= slice.len; + } else { + break; } + remaining_events -= slice.len; } } } diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index 56e1798f1..33d05df29 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -17,6 +17,8 @@ const testDir = tempDirWithFiles("watch", { "relative.txt": "hello", "abort.txt": "hello", "url.txt": "hello", + "close.txt": "hello", + "close-close.txt": "hello", [encodingFileName]: "hello", }); @@ -105,6 +107,7 @@ describe("fs.watch", () => { let err = undefined; watcher.on("change", (event, filename) => { const basename = path.basename(filename); + if (basename === "subfolder") return; count++; try { @@ -274,6 +277,46 @@ describe("fs.watch", () => { } }); + test("calling close from error event should not throw", done => { + const filepath = path.join(testDir, "close.txt"); + try { + const ac = new AbortController(); + const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal }); + watcher.once("error", () => { + try { + watcher.close(); + done(); + } catch (e) { + done("Should not error when calling close from error event"); + } + }); + ac.abort(); + } catch (e) { + done(e); + } + }); + + test("calling close from close event should not throw", done => { + const filepath = path.join(testDir, "close-close.txt"); + try { + const ac = new AbortController(); + const watcher = fs.watch(pathToFileURL(filepath), { signal: ac.signal }); + + watcher.once("close", () => { + try { + watcher.close(); + done(); + } catch (e) { + done("Should not error when calling close from close event"); + } + }); + + ac.abort(); + } catch (e) { + done(e); + } + }); + test("Signal aborted after creating the watcher", async () => { const filepath = path.join(testDir, "abort.txt"); @@ -300,7 +343,7 @@ describe("fs.watch", () => { }); }); -describe("fs.promises.watchFile", () => { +describe("fs.promises.watch", () => { test("add file/folder to folder", async () => { let count = 0; const root = path.join(testDir, "add-promise-directory"); -- cgit v1.2.3 From fdfbb18531828fc5dec329d5d9e5c828a3c83921 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jun 2023 16:32:27 -0700 Subject: Support reading embedded files in compiled executables (#3405) * Support reading embedded files in compiled executables * :nail_care: --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/bun.zig | 3 +++ src/bun.js/api/server.zig | 22 +++++++++------------- src/bun.js/javascript.zig | 5 ++++- src/bun.js/node/node_fs.zig | 30 ++++++++++++++++++++++++++++++ src/bun.js/webcore/blob.zig | 36 ++++++++++++++++++++++++++++++++++-- src/cli/build_command.zig | 1 + src/standalone_bun.zig | 44 +++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 124 insertions(+), 17 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 034aaa81f..2e6381c74 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -896,6 +896,9 @@ pub fn createNodeFS( ) js.JSValueRef { var module = ctx.allocator().create(JSC.Node.NodeJSFS) catch unreachable; module.* = .{}; + var vm = ctx.bunVM(); + if (vm.standalone_module_graph != null) + module.node_fs.vm = vm; return module.toJS(ctx).asObjectRef(); } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index a56ff971f..ebfacdcc9 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2744,19 +2744,15 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // 1. Bun.file("foo") // 2. The content-disposition header is not present if (!has_content_disposition and content_type.category.autosetFilename()) { - if (this.blob.store()) |store| { - if (store.data == .file) { - if (store.data.file.pathlike == .path) { - const basename = std.fs.path.basename(store.data.file.pathlike.path.slice()); - if (basename.len > 0) { - var filename_buf: [1024]u8 = undefined; - - resp.writeHeader( - "content-disposition", - std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", - ); - } - } + if (this.blob.getFileName()) |filename| { + const basename = std.fs.path.basename(filename); + if (basename.len > 0) { + var filename_buf: [1024]u8 = undefined; + + resp.writeHeader( + "content-disposition", + std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@min(basename.len, 1024 - 32)]}) catch "", + ); } } } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 3baa25e22..cb1a50f1d 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -593,7 +593,10 @@ pub const VirtualMachine = struct { pub inline fn nodeFS(this: *VirtualMachine) *Node.NodeFS { return this.node_fs orelse brk: { this.node_fs = bun.default_allocator.create(Node.NodeFS) catch unreachable; - this.node_fs.?.* = Node.NodeFS{}; + this.node_fs.?.* = Node.NodeFS{ + // only used when standalone module graph is enabled + .vm = if (this.standalone_module_graph != null) this else null, + }; break :brk this.node_fs.?; }; } diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 21a65251a..35c616a89 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -2492,6 +2492,7 @@ pub const NodeFS = struct { /// That means a stack-allocated buffer won't suffice. Instead, we re-use /// the heap allocated buffer on the NodefS struct sync_error_buf: [bun.MAX_PATH_BYTES]u8 = undefined, + vm: ?*JSC.VirtualMachine = null, pub const ReturnType = Return; @@ -3442,6 +3443,35 @@ pub const NodeFS = struct { const fd = switch (args.path) { .path => brk: { path = args.path.path.sliceZ(&this.sync_error_buf); + if (this.vm) |vm| { + if (vm.standalone_module_graph) |graph| { + if (graph.find(path)) |file| { + if (args.encoding == .buffer) { + return .{ + .result = .{ + .buffer = Buffer.fromBytes( + bun.default_allocator.dupe(u8, file.contents) catch @panic("out of memory"), + bun.default_allocator, + .Uint8Array, + ), + }, + }; + } else if (comptime string_type == .default) + .{ + .result = .{ + .string = bun.default_allocator.dupe(u8, file.contents) catch @panic("out of memory"), + }, + } + else + .{ + .result = .{ + .null_terminated = bun.default_allocator.dupeZ(u8, file.contents) catch @panic("out of memory"), + }, + }; + } + } + } + break :brk switch (Syscall.open( path, os.O.RDONLY | os.O.NOCTTY, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 1e63ea3a2..868acbb80 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -952,6 +952,13 @@ pub const Blob = struct { switch (path_) { .path => { const slice = path_.path.slice(); + + if (vm.standalone_module_graph) |graph| { + if (graph.find(slice)) |file| { + return file.blob(globalThis).dupe(); + } + } + var cloned = (allocator.dupeZ(u8, slice) catch unreachable)[0..slice.len]; break :brk .{ @@ -2195,6 +2202,9 @@ pub const Blob = struct { cap: SizeType = 0, allocator: std.mem.Allocator, + /// Used by standalone module graph + stored_name: bun.PathString = bun.PathString.empty, + pub fn init(bytes: []u8, allocator: std.mem.Allocator) ByteStore { return .{ .ptr = bytes.ptr, @@ -2528,17 +2538,31 @@ pub const Blob = struct { this: *Blob, globalThis: *JSC.JSGlobalObject, ) callconv(.C) JSValue { + if (this.getFileName()) |path| { + var str = bun.String.create(path); + return str.toJS(globalThis); + } + + return JSValue.undefined; + } + + pub fn getFileName( + this: *const Blob, + ) ?[]const u8 { if (this.store) |store| { if (store.data == .file) { if (store.data.file.pathlike == .path) { - return ZigString.fromUTF8(store.data.file.pathlike.path.slice()).toValueGC(globalThis); + return store.data.file.pathlike.path.slice(); } // we shouldn't return Number here. + } else if (store.data == .bytes) { + if (store.data.bytes.stored_name.slice().len > 0) + return store.data.bytes.stored_name.slice(); } } - return JSC.JSValue.jsUndefined(); + return null; } // TODO: Move this to a separate `File` object or BunFile @@ -3469,6 +3493,14 @@ pub const AnyBlob = union(enum) { InternalBlob: InternalBlob, WTFStringImpl: bun.WTF.StringImpl, + pub fn getFileName(this: *const AnyBlob) ?[]const u8 { + return switch (this.*) { + .Blob => this.Blob.getFileName(), + .WTFStringImpl => null, + .InternalBlob => null, + }; + } + pub inline fn fastSize(this: *const AnyBlob) Blob.SizeType { return switch (this.*) { .Blob => this.Blob.size, diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 44e512996..ef99f7765 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -107,6 +107,7 @@ pub const BuildCommand = struct { // We never want to hit the filesystem for these files // This "compiled" protocol is specially handled by the module resolver. this_bundler.options.public_path = "compiled://root/"; + this_bundler.resolver.opts.public_path = "compiled://root/"; if (outfile.len == 0) { outfile = std.fs.path.basename(this_bundler.options.entry_points[0]); diff --git a/src/standalone_bun.zig b/src/standalone_bun.zig index e7363fb58..b18fe384e 100644 --- a/src/standalone_bun.zig +++ b/src/standalone_bun.zig @@ -18,6 +18,14 @@ pub const StandaloneModuleGraph = struct { return &this.files.values()[this.entry_point_id]; } + pub fn find(this: *const StandaloneModuleGraph, name: []const u8) ?*File { + if (!bun.strings.hasPrefixComptime(name, "compiled://root/")) { + return null; + } + + return this.files.getPtr(name); + } + pub const CompiledModuleGraphFile = struct { name: Schema.StringPointer = .{}, loader: bun.options.Loader = .file, @@ -30,6 +38,32 @@ pub const StandaloneModuleGraph = struct { loader: bun.options.Loader, contents: []const u8 = "", sourcemap: LazySourceMap, + blob_: ?*bun.JSC.WebCore.Blob = null, + + pub fn blob(this: *File, globalObject: *bun.JSC.JSGlobalObject) *bun.JSC.WebCore.Blob { + if (this.blob_ == null) { + var store = bun.JSC.WebCore.Blob.Store.init(@constCast(this.contents), bun.default_allocator) catch @panic("out of memory"); + // make it never free + store.ref(); + + var blob_ = bun.default_allocator.create(bun.JSC.WebCore.Blob) catch @panic("out of memory"); + blob_.* = bun.JSC.WebCore.Blob.initWithStore(store, globalObject); + blob_.allocator = bun.default_allocator; + + if (bun.HTTP.MimeType.byExtensionNoDefault(bun.strings.trimLeadingChar(std.fs.path.extension(this.name), '.'))) |mime| { + store.mime_type = mime; + blob_.content_type = mime.value; + blob_.content_type_was_set = true; + blob_.content_type_allocated = false; + } + + store.data.bytes.stored_name = bun.PathString.init(this.name); + + this.blob_ = blob_; + } + + return this.blob_.?; + } }; pub const LazySourceMap = union(enum) { @@ -152,8 +186,16 @@ pub const StandaloneModuleGraph = struct { continue; } + var dest_path = output_file.dest_path; + if (bun.strings.hasPrefixComptime(dest_path, "./")) { + dest_path = dest_path[2..]; + } + var module = CompiledModuleGraphFile{ - .name = string_builder.fmtAppendCount("{s}{s}", .{ prefix, output_file.dest_path }), + .name = string_builder.fmtAppendCount("{s}{s}", .{ + prefix, + dest_path, + }), .loader = output_file.loader, .contents = string_builder.appendCount(output_file.value.buffer.bytes), }; -- cgit v1.2.3 From 15ac08474ef0b18b94bbf4863b2497e18e968379 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Sun, 25 Jun 2023 16:40:51 -0700 Subject: fixup --- src/bun.js/node/node_fs.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 35c616a89..1298c5d45 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3457,13 +3457,13 @@ pub const NodeFS = struct { }, }; } else if (comptime string_type == .default) - .{ + return .{ .result = .{ .string = bun.default_allocator.dupe(u8, file.contents) catch @panic("out of memory"), }, } else - .{ + return .{ .result = .{ .null_terminated = bun.default_allocator.dupeZ(u8, file.contents) catch @panic("out of memory"), }, -- cgit v1.2.3 From f2285a6d71eba0abc27bc449e9a50116447decf8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jun 2023 17:54:55 -0700 Subject: Use bun.String in mkdir (#3404) Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/node/node_fs.zig | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 1298c5d45..13d785e97 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -2368,7 +2368,7 @@ const Return = struct { pub const Lchown = void; pub const Link = void; pub const Lstat = Stats; - pub const Mkdir = string; + pub const Mkdir = bun.String; pub const Mkdtemp = JSC.ZigString; pub const Open = FileDescriptor; pub const WriteFile = void; @@ -2997,7 +2997,7 @@ pub const NodeFS = struct { .sync => { const path = args.path.sliceZ(&this.sync_error_buf); return switch (Syscall.mkdir(path, args.mode)) { - .result => Maybe(Return.Mkdir){ .result = "" }, + .result => Maybe(Return.Mkdir){ .result = bun.String.empty }, .err => |err| Maybe(Return.Mkdir){ .err = err }, }; }, @@ -3031,14 +3031,19 @@ pub const NodeFS = struct { }, .EXIST => { - return Option{ .result = "" }; + return Option{ .result = bun.String.empty }; }, // continue .NOENT => {}, } }, .result => { - return Option{ .result = args.path.slice() }; + return Option{ + .result = if (args.path == .slice_with_underlying_string) + args.path.slice_with_underlying_string.underlying + else + bun.String.create(args.path.slice()), + }; }, } @@ -3111,10 +3116,9 @@ pub const NodeFS = struct { switch (err.getErrno()) { // handle the race condition .EXIST => { - var display_path: []const u8 = ""; + var display_path = bun.String.empty; if (first_match != std.math.maxInt(u16)) { - // TODO: this leaks memory - display_path = bun.default_allocator.dupe(u8, display_path[0..first_match]) catch unreachable; + display_path = bun.String.create(working_mem[0..first_match]); } return Option{ .result = display_path }; }, @@ -3126,12 +3130,14 @@ pub const NodeFS = struct { } }, .result => { - var display_path = args.path.slice(); - if (first_match != std.math.maxInt(u16)) { - // TODO: this leaks memory - display_path = bun.default_allocator.dupe(u8, display_path[0..first_match]) catch unreachable; - } - return Option{ .result = display_path }; + return Option{ + .result = if (first_match != std.math.maxInt(u16)) + bun.String.create(working_mem[0..first_match]) + else if (args.path == .slice_with_underlying_string) + args.path.slice_with_underlying_string.underlying + else + bun.String.create(args.path.slice()), + }; }, } }, -- cgit v1.2.3 From 50e872fc761db50ae2804f780ea9cd655600a7e2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 26 Jun 2023 11:56:53 -0700 Subject: Implement writev & readv (#3419) * [node:fs] Implement `writev` and `readv` * writev & readv tests * cast to const type * woops * cast --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/node/node_fs.zig | 235 +++++++++++++++++++++++++++++++++ src/bun.js/node/node_fs_binding.zig | 8 +- src/bun.js/node/syscall.zig | 152 +++++++++++++++++++++ src/bun.js/node/types.zig | 44 ++++++ src/io/io_darwin.zig | 4 + src/js/node/fs.js | 41 +++++- src/js/node/fs.promises.ts | 34 ++++- src/js/out/modules/node/fs.js | 38 +++++- src/js/out/modules/node/fs.promises.js | 2 +- test/js/node/fs/fs.test.ts | 65 +++++++++ 10 files changed, 611 insertions(+), 12 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 13d785e97..fa33a575b 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -135,6 +135,154 @@ pub const Arguments = struct { } }; + pub const Writev = struct { + fd: FileDescriptor, + buffers: JSC.Node.VectorArrayBuffer, + position: ?u52 = 0, + + pub fn deinit(_: *const @This()) void {} + + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Writev { + const fd_value = arguments.nextEat() orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "file descriptor is required", + .{}, + ctx, + exception, + ); + } + return null; + }; + + const fd = JSC.Node.fileDescriptorFromJS(ctx, fd_value, exception) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "file descriptor must be a number", + .{}, + ctx, + exception, + ); + } + return null; + }; + + const buffers = JSC.Node.VectorArrayBuffer.fromJS( + ctx, + arguments.protectEatNext() orelse { + JSC.throwInvalidArguments("Expected an ArrayBufferView[]", .{}, ctx, exception); + return null; + }, + exception, + arguments.arena.allocator(), + ) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "buffers must be an array of TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + + var position: ?u52 = null; + + if (arguments.nextEat()) |pos_value| { + if (!pos_value.isUndefinedOrNull()) { + if (pos_value.isNumber()) { + position = pos_value.to(u52); + } else { + JSC.throwInvalidArguments( + "position must be a number", + .{}, + ctx, + exception, + ); + return null; + } + } + } + + return Writev{ .fd = fd, .buffers = buffers, .position = position }; + } + }; + + pub const Readv = struct { + fd: FileDescriptor, + buffers: JSC.Node.VectorArrayBuffer, + position: ?u52 = 0, + + pub fn deinit(_: *const @This()) void {} + + pub fn fromJS(ctx: JSC.C.JSContextRef, arguments: *ArgumentsSlice, exception: JSC.C.ExceptionRef) ?Readv { + const fd_value = arguments.nextEat() orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "file descriptor is required", + .{}, + ctx, + exception, + ); + } + return null; + }; + + const fd = JSC.Node.fileDescriptorFromJS(ctx, fd_value, exception) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "file descriptor must be a number", + .{}, + ctx, + exception, + ); + } + return null; + }; + + const buffers = JSC.Node.VectorArrayBuffer.fromJS( + ctx, + arguments.protectEatNext() orelse { + JSC.throwInvalidArguments("Expected an ArrayBufferView[]", .{}, ctx, exception); + return null; + }, + exception, + arguments.arena.allocator(), + ) orelse { + if (exception.* == null) { + JSC.throwInvalidArguments( + "buffers must be an array of TypedArray", + .{}, + ctx, + exception, + ); + } + return null; + }; + + var position: ?u52 = null; + + if (arguments.nextEat()) |pos_value| { + if (!pos_value.isUndefinedOrNull()) { + if (pos_value.isNumber()) { + position = pos_value.to(u52); + } else { + JSC.throwInvalidArguments( + "position must be a number", + .{}, + ctx, + exception, + ); + return null; + } + } + } + + return Readv{ .fd = fd, .buffers = buffers, .position = position }; + } + }; + pub const FTruncate = struct { fd: FileDescriptor, len: ?JSC.WebCore.Blob.SizeType = null, @@ -2372,6 +2520,7 @@ const Return = struct { pub const Mkdtemp = JSC.ZigString; pub const Open = FileDescriptor; pub const WriteFile = void; + pub const Readv = Read; pub const Read = struct { bytes_read: u52, @@ -2480,6 +2629,8 @@ const Return = struct { pub const Chown = void; pub const Lutimes = void; + + pub const Writev = Write; }; /// Bun's implementation of the Node.js "fs" module @@ -3256,6 +3407,14 @@ pub const NodeFS = struct { ); } + pub fn readv(this: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Read) { + return if (args.position != null) _preadv(this, args, flavor) else _readv(this, args, flavor); + } + + pub fn writev(this: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { + return if (args.position != null) _pwritev(this, args, flavor) else _writev(this, args, flavor); + } + pub fn write(this: *NodeFS, args: Arguments.Write, comptime flavor: Flavor) Maybe(Return.Write) { return if (args.position != null) _pwrite(this, args, flavor) else _write(this, args, flavor); } @@ -3307,6 +3466,82 @@ pub const NodeFS = struct { return Maybe(Return.Write).todo; } + fn _preadv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { + const position = args.position.?; + + switch (comptime flavor) { + .sync => { + return switch (Syscall.preadv(args.fd, args.buffers.buffers.items, position)) { + .err => |err| .{ + .err = err, + }, + .result => |amt| .{ .result = .{ + .bytes_read = @truncate(u52, amt), + } }, + }; + }, + else => {}, + } + + return Maybe(Return.Write).todo; + } + + fn _readv(_: *NodeFS, args: Arguments.Readv, comptime flavor: Flavor) Maybe(Return.Readv) { + switch (comptime flavor) { + .sync => { + return switch (Syscall.readv(args.fd, args.buffers.buffers.items)) { + .err => |err| .{ + .err = err, + }, + .result => |amt| .{ .result = .{ + .bytes_read = @truncate(u52, amt), + } }, + }; + }, + else => {}, + } + + return Maybe(Return.Write).todo; + } + + fn _pwritev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { + const position = args.position.?; + + switch (comptime flavor) { + .sync => { + return switch (Syscall.pwritev(args.fd, args.buffers.buffers.items, position)) { + .err => |err| .{ + .err = err, + }, + .result => |amt| .{ .result = .{ + .bytes_written = @truncate(u52, amt), + } }, + }; + }, + else => {}, + } + + return Maybe(Return.Write).todo; + } + + fn _writev(_: *NodeFS, args: Arguments.Writev, comptime flavor: Flavor) Maybe(Return.Write) { + switch (comptime flavor) { + .sync => { + return switch (Syscall.writev(args.fd, args.buffers.buffers.items)) { + .err => |err| .{ + .err = err, + }, + .result => |amt| .{ .result = .{ + .bytes_written = @truncate(u52, amt), + } }, + }; + }, + else => {}, + } + + return Maybe(Return.Write).todo; + } + pub fn readdir(this: *NodeFS, args: Arguments.Readdir, comptime flavor: Flavor) Maybe(Return.Readdir) { return switch (args.encoding) { .buffer => _readdir( diff --git a/src/bun.js/node/node_fs_binding.zig b/src/bun.js/node/node_fs_binding.zig index f178f0355..a4cc62cd3 100644 --- a/src/bun.js/node/node_fs_binding.zig +++ b/src/bun.js/node/node_fs_binding.zig @@ -229,6 +229,10 @@ pub const NodeJSFS = struct { pub const lutimesSync = callSync(.lutimes); pub const rmSync = callSync(.rm); pub const rmdirSync = callSync(.rmdir); + pub const writev = call(.writev); + pub const writevSync = callSync(.writev); + pub const readv = call(.readv); + pub const readvSync = callSync(.readv); pub const fdatasyncSync = callSync(.fdatasync); pub const fdatasync = call(.fdatasync); @@ -247,8 +251,4 @@ pub const NodeJSFS = struct { const notimpl = fdatasync; pub const opendir = notimpl; pub const opendirSync = notimpl; - pub const readv = notimpl; - pub const readvSync = notimpl; - pub const writev = notimpl; - pub const writevSync = notimpl; }; diff --git a/src/bun.js/node/syscall.zig b/src/bun.js/node/syscall.zig index 77bd5b13d..48c5b1305 100644 --- a/src/bun.js/node/syscall.zig +++ b/src/bun.js/node/syscall.zig @@ -106,6 +106,10 @@ pub const Tag = enum(u8) { waitpid, posix_spawn, getaddrinfo, + writev, + pwritev, + readv, + preadv, pub var strings = std.EnumMap(Tag, JSC.C.JSStringRef).initFull(null); }; const PathString = @import("root").bun.PathString; @@ -302,6 +306,154 @@ pub fn write(fd: os.fd_t, bytes: []const u8) Maybe(usize) { } } +fn veclen(buffers: anytype) usize { + var len: usize = 0; + for (buffers) |buffer| { + len += buffer.iov_len; + } + return len; +} + +pub fn writev(fd: os.fd_t, buffers: []std.os.iovec) Maybe(usize) { + if (comptime Environment.isMac) { + const rc = writev_sym(fd, @ptrCast([*]std.os.iovec_const, buffers.ptr), @intCast(i32, buffers.len)); + if (comptime Environment.allow_assert) + log("writev({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .writev, fd)) |err| { + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } else { + while (true) { + const rc = writev_sym(fd, @ptrCast([*]std.os.iovec_const, buffers.ptr), buffers.len); + if (comptime Environment.allow_assert) + log("writev({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .writev, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } + unreachable; + } +} + +pub fn pwritev(fd: os.fd_t, buffers: []std.os.iovec, position: isize) Maybe(usize) { + if (comptime Environment.isMac) { + const rc = pwritev_sym(fd, @ptrCast([*]std.os.iovec_const, buffers.ptr), @intCast(i32, buffers.len), position); + if (comptime Environment.allow_assert) + log("pwritev({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .pwritev, fd)) |err| { + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } else { + while (true) { + const rc = pwritev_sym(fd, @ptrCast([*]std.os.iovec_const, buffers.ptr), buffers.len, position); + if (comptime Environment.allow_assert) + log("pwritev({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .pwritev, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } + unreachable; + } +} + +pub fn readv(fd: os.fd_t, buffers: []std.os.iovec) Maybe(usize) { + if (comptime Environment.isMac) { + const rc = readv_sym(fd, buffers.ptr, @intCast(i32, buffers.len)); + if (comptime Environment.allow_assert) + log("readv({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .readv, fd)) |err| { + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } else { + while (true) { + const rc = readv_sym(fd, buffers.ptr, buffers.len); + if (comptime Environment.allow_assert) + log("readv({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .readv, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } + unreachable; + } +} + +pub fn preadv(fd: os.fd_t, buffers: []std.os.iovec, position: isize) Maybe(usize) { + if (comptime Environment.isMac) { + const rc = preadv_sym(fd, buffers.ptr, @intCast(i32, buffers.len), position); + if (comptime Environment.allow_assert) + log("preadv({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .preadv, fd)) |err| { + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } else { + while (true) { + const rc = preadv_sym(fd, buffers.ptr, buffers.len, position); + if (comptime Environment.allow_assert) + log("preadv({d}, {d}) = {d}", .{ fd, veclen(buffers), rc }); + + if (Maybe(usize).errnoSysFd(rc, .preadv, fd)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return Maybe(usize){ .result = @intCast(usize, rc) }; + } + unreachable; + } +} + +const preadv_sym = if (builtin.os.tag == .linux and builtin.link_libc) + std.os.linux.preadv +else if (builtin.os.tag.isDarwin()) + system.@"preadv$NOCANCEL" +else + system.preadv; + +const readv_sym = if (builtin.os.tag == .linux and builtin.link_libc) + std.os.linux.readv +else if (builtin.os.tag.isDarwin()) + system.@"readv$NOCANCEL" +else + system.readv; + +const pwritev_sym = if (builtin.os.tag == .linux and builtin.link_libc) + std.os.linux.pwritev +else if (builtin.os.tag.isDarwin()) + system.@"pwritev$NOCANCEL" +else + system.pwritev; + +const writev_sym = if (builtin.os.tag == .linux and builtin.link_libc) + std.os.linux.writev +else if (builtin.os.tag.isDarwin()) + system.@"writev$NOCANCEL" +else + system.writev; + const pread_sym = if (builtin.os.tag == .linux and builtin.link_libc) sys.pread64 else if (builtin.os.tag.isDarwin()) diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 659ac31bb..b01eca8e0 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -813,6 +813,50 @@ pub const Valid = struct { } }; +pub const VectorArrayBuffer = struct { + value: JSC.JSValue, + buffers: std.ArrayList(std.os.iovec), + + pub fn toJS(this: VectorArrayBuffer, _: *JSC.JSGlobalObject) JSC.JSValue { + return this.value; + } + + pub fn fromJS(globalObject: *JSC.JSGlobalObject, val: JSC.JSValue, exception: JSC.C.ExceptionRef, allocator: std.mem.Allocator) ?VectorArrayBuffer { + if (!val.jsType().isArrayLike()) { + JSC.throwInvalidArguments("Expected ArrayBufferView[]", .{}, globalObject, exception); + return null; + } + + var bufferlist = std.ArrayList(std.os.iovec).init(allocator); + var i: usize = 0; + const len = val.getLength(globalObject); + bufferlist.ensureTotalCapacityPrecise(len) catch @panic("Failed to allocate memory for ArrayBuffer[]"); + + while (i < len) { + const element = val.getIndex(globalObject, @truncate(u32, i)); + + if (!element.isCell()) { + JSC.throwInvalidArguments("Expected ArrayBufferView[]", .{}, globalObject, exception); + return null; + } + + const array_buffer = element.asArrayBuffer(globalObject) orelse { + JSC.throwInvalidArguments("Expected ArrayBufferView[]", .{}, globalObject, exception); + return null; + }; + + var buf = array_buffer.byteSlice(); + bufferlist.append(std.os.iovec{ + .iov_base = buf.ptr, + .iov_len = buf.len, + }) catch @panic("Failed to allocate memory for ArrayBuffer[]"); + i += 1; + } + + return VectorArrayBuffer{ .value = val, .buffers = bufferlist }; + } +}; + pub const ArgumentsSlice = struct { remaining: []const JSC.JSValue, vm: *JSC.VirtualMachine, diff --git a/src/io/io_darwin.zig b/src/io/io_darwin.zig index 8045cbdf6..9ebc6f4d6 100644 --- a/src/io/io_darwin.zig +++ b/src/io/io_darwin.zig @@ -274,7 +274,11 @@ pub const darwin = struct { pub extern "c" fn @"openat$NOCANCEL"(fd: c.fd_t, path: [*:0]const u8, oflag: c_uint, ...) c_int; pub extern "c" fn @"read$NOCANCEL"(fd: c.fd_t, buf: [*]u8, nbyte: usize) isize; pub extern "c" fn @"pread$NOCANCEL"(fd: c.fd_t, buf: [*]u8, nbyte: usize, offset: c.off_t) isize; + pub extern "c" fn @"preadv$NOCANCEL"(fd: c.fd_t, uf: [*]std.os.iovec, count: i32, offset: c.off_t) isize; + pub extern "c" fn @"readv$NOCANCEL"(fd: c.fd_t, uf: [*]std.os.iovec, count: i32) isize; pub extern "c" fn @"write$NOCANCEL"(fd: c.fd_t, buf: [*]const u8, nbyte: usize) isize; + pub extern "c" fn @"writev$NOCANCEL"(fd: c.fd_t, buf: [*]std.os.iovec_const, count: i32) isize; + pub extern "c" fn @"pwritev$NOCANCEL"(fd: c.fd_t, buf: [*]std.os.iovec_const, count: i32, offset: c.off_t) isize; }; pub const OpenError = error{ /// In WASI, this error may occur when the file descriptor does diff --git a/src/js/node/fs.js b/src/js/node/fs.js index 8d9f0d235..072102c35 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -5,9 +5,8 @@ import { EventEmitter } from "node:events"; // Hardcoded module "node:fs" var { direct, isPromise, isCallable } = globalThis[Symbol.for("Bun.lazy")]("primordials"); -export { default as promises } from "node:fs/promises"; import promises from "node:fs/promises"; - +export { default as promises } from "node:fs/promises"; import * as Stream from "node:stream"; var fs = Bun.fs(); @@ -213,6 +212,40 @@ export var access = function access(...args) { lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), + writev = (fd, buffers, position, callback) => { + if (typeof position === "function") { + callback = position; + position = null; + } + + queueMicrotask(() => { + try { + var written = fs.writevSync(fd, buffers, position); + } catch (e) { + callback(e); + } + + callback(null, written, buffers); + }); + }, + writevSync = fs.writevSync.bind(fs), + readv = (fd, buffers, position, callback) => { + if (typeof position === "function") { + callback = position; + position = null; + } + + queueMicrotask(() => { + try { + var written = fs.readvSync(fd, buffers, position); + } catch (e) { + callback(e); + } + + callback(null, written, buffers); + }); + }, + readvSync = fs.readvSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch(path, options, listener) { @@ -1069,6 +1102,10 @@ export default { ReadStream, watch, FSWatcher, + writev, + writevSync, + readv, + readvSync, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass, diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index 7df446ccb..12278ef53 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -123,7 +123,37 @@ export var access = promisify(fs.accessSync), utimes = promisify(fs.utimesSync), lutimes = promisify(fs.lutimesSync), rm = promisify(fs.rmSync), - rmdir = promisify(fs.rmdirSync); + rmdir = promisify(fs.rmdirSync), + writev = (fd, buffers, position) => { + return new Promise((resolve, reject) => { + try { + var bytesWritten = fs.writevSync(fd, buffers, position); + } catch (err) { + reject(err); + return; + } + + resolve({ + bytesWritten, + buffers, + }); + }); + }, + readv = (fd, buffers, position) => { + return new Promise((resolve, reject) => { + try { + var bytesRead = fs.readvSync(fd, buffers, position); + } catch (err) { + reject(err); + return; + } + + resolve({ + bytesRead, + buffers, + }); + }); + }; export default { access, @@ -163,6 +193,8 @@ export default { rm, rmdir, watch, + writev, + readv, constants, [Symbol.for("CommonJS")]: 0, }; diff --git a/src/js/out/modules/node/fs.js b/src/js/out/modules/node/fs.js index 6c8269d59..b7457f104 100644 --- a/src/js/out/modules/node/fs.js +++ b/src/js/out/modules/node/fs.js @@ -1,6 +1,6 @@ import {EventEmitter} from "node:events"; -import {default as default2} from "node:fs/promises"; import promises2 from "node:fs/promises"; +import {default as default2} from "node:fs/promises"; import * as Stream from "node:stream"; var callbackify = function(fsFunction, args) { try { @@ -133,7 +133,29 @@ var access = function access2(...args) { callbackify(fs.utimesSync, args); }, lutimes = function lutimes2(...args) { callbackify(fs.lutimesSync, args); -}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) { +}, accessSync = fs.accessSync.bind(fs), appendFileSync = fs.appendFileSync.bind(fs), closeSync = fs.closeSync.bind(fs), copyFileSync = fs.copyFileSync.bind(fs), existsSync = fs.existsSync.bind(fs), chownSync = fs.chownSync.bind(fs), chmodSync = fs.chmodSync.bind(fs), fchmodSync = fs.fchmodSync.bind(fs), fchownSync = fs.fchownSync.bind(fs), fstatSync = fs.fstatSync.bind(fs), fsyncSync = fs.fsyncSync.bind(fs), ftruncateSync = fs.ftruncateSync.bind(fs), futimesSync = fs.futimesSync.bind(fs), lchmodSync = fs.lchmodSync.bind(fs), lchownSync = fs.lchownSync.bind(fs), linkSync = fs.linkSync.bind(fs), lstatSync = fs.lstatSync.bind(fs), mkdirSync = fs.mkdirSync.bind(fs), mkdtempSync = fs.mkdtempSync.bind(fs), openSync = fs.openSync.bind(fs), readSync = fs.readSync.bind(fs), writeSync = fs.writeSync.bind(fs), readdirSync = fs.readdirSync.bind(fs), readFileSync = fs.readFileSync.bind(fs), writeFileSync = fs.writeFileSync.bind(fs), readlinkSync = fs.readlinkSync.bind(fs), realpathSync = fs.realpathSync.bind(fs), renameSync = fs.renameSync.bind(fs), statSync = fs.statSync.bind(fs), symlinkSync = fs.symlinkSync.bind(fs), truncateSync = fs.truncateSync.bind(fs), unlinkSync = fs.unlinkSync.bind(fs), utimesSync = fs.utimesSync.bind(fs), lutimesSync = fs.lutimesSync.bind(fs), rmSync = fs.rmSync.bind(fs), rmdirSync = fs.rmdirSync.bind(fs), writev = (fd, buffers, position, callback) => { + if (typeof position === "function") + callback = position, position = null; + queueMicrotask(() => { + try { + var written = fs.writevSync(fd, buffers, position); + } catch (e) { + callback(e); + } + callback(null, written, buffers); + }); +}, writevSync = fs.writevSync.bind(fs), readv = (fd, buffers, position, callback) => { + if (typeof position === "function") + callback = position, position = null; + queueMicrotask(() => { + try { + var written = fs.readvSync(fd, buffers, position); + } catch (e) { + callback(e); + } + callback(null, written, buffers); + }); +}, readvSync = fs.readvSync.bind(fs), Dirent = fs.Dirent, Stats = fs.Stats, watch = function watch2(path, options, listener) { return new FSWatcher(path, options, listener); }, readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath"), readStreamSymbol = Symbol.for("Bun.NodeReadStream"), readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd"), writeStreamSymbol = Symbol.for("Bun.NodeWriteStream"), writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath"), writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall"), kIoDone = Symbol.for("kIoDone"), defaultReadStreamOptions = { file: void 0, @@ -366,8 +388,8 @@ WriteStream = function(InternalWriteStream) { return WriteStreamClass = InternalWriteStream, Object.defineProperty(WriteStreamClass.prototype, Symbol.toStringTag, { value: "WritesStream", enumerable: !1 - }), Object.defineProperty(function WriteStream(options) { - return new InternalWriteStream(options); + }), Object.defineProperty(function WriteStream(path, options) { + return new InternalWriteStream(path, options); }, Symbol.hasInstance, { value(instance) { return instance instanceof InternalWriteStream; @@ -642,12 +664,18 @@ var fs_default = { ReadStream, watch, FSWatcher, + writev, + writevSync, + readv, + readvSync, [Symbol.for("::bunternal::")]: { ReadStreamClass, WriteStreamClass } }; export { + writevSync, + writev, writeSync, writeFileSync, writeFile, @@ -671,6 +699,8 @@ export { rename, realpathSync, realpath, + readvSync, + readv, readlinkSync, readlink, readdirSync, diff --git a/src/js/out/modules/node/fs.promises.js b/src/js/out/modules/node/fs.promises.js index ef3330771..549ba0c9c 100644 --- a/src/js/out/modules/node/fs.promises.js +++ b/src/js/out/modules/node/fs.promises.js @@ -1 +1 @@ -function H(S,C={}){const J=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let b=null;if(typeof C==="string")C={encoding:C};return D.watch(S,C||{},(q,z)=>{if(J.push({eventType:q,filename:z}),b){const A=b;b=null,A()}}),{async*[Symbol.asyncIterator](){let q=!1;while(!q){while(J.length){let z=J.shift();if(z.eventType==="close"){q=!0;break}if(z.eventType==="error")throw q=!0,z.filename;yield z}await new Promise((z)=>b=z)}}}}var D=Bun.fs(),B="::bunternal::",G={[B]:(S)=>{var C={[B]:function(J,b,q){var z;try{z=S.apply(D,q),q=void 0}catch(A){q=void 0,b(A);return}J(z)}}[B];return async function(...J){return await new Promise((b,q)=>{process.nextTick(C,b,q,J)})}}}[B],I=G(D.accessSync),K=G(D.appendFileSync),L=G(D.closeSync),M=G(D.copyFileSync),N=G(D.existsSync),O=G(D.chownSync),P=G(D.chmodSync),Q=G(D.fchmodSync),U=G(D.fchownSync),V=G(D.fstatSync),W=G(D.fsyncSync),X=G(D.ftruncateSync),Y=G(D.futimesSync),Z=G(D.lchmodSync),_=G(D.lchownSync),$=G(D.linkSync),T=G(D.lstatSync),E=G(D.mkdirSync),j=G(D.mkdtempSync),R=G(D.openSync),k=G(D.readSync),x=G(D.writeSync),F=G(D.readdirSync),u=G(D.readFileSync),w=G(D.writeFileSync),g=G(D.readlinkSync),h=G(D.realpathSync),d=G(D.renameSync),c=G(D.statSync),v=G(D.symlinkSync),a=G(D.truncateSync),y=G(D.unlinkSync),l=G(D.utimesSync),t=G(D.lutimesSync),p=G(D.rmSync),n=G(D.rmdirSync),m={access:I,appendFile:K,close:L,copyFile:M,exists:N,chown:O,chmod:P,fchmod:Q,fchown:U,fstat:V,fsync:W,ftruncate:X,futimes:Y,lchmod:Z,lchown:_,link:$,lstat:T,mkdir:E,mkdtemp:j,open:R,read:k,write:x,readdir:F,readFile:u,writeFile:w,readlink:g,realpath:h,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:t,rm:p,rmdir:n,watch:H,constants,[Symbol.for("CommonJS")]:0};export{w as writeFile,x as write,H as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,p as rm,d as rename,h as realpath,g as readlink,F as readdir,u as readFile,k as read,R as open,j as mkdtemp,E as mkdir,t as lutimes,T as lstat,$ as link,_ as lchown,Z as lchmod,Y as futimes,X as ftruncate,W as fsync,V as fstat,U as fchown,Q as fchmod,N as exists,m as default,M as copyFile,L as close,O as chown,P as chmod,K as appendFile,I as access}; +function J(S,q={}){const z=[];if(S instanceof URL)throw new TypeError("Watch URLs are not supported yet");else if(Buffer.isBuffer(S))S=S.toString();else if(typeof S!=="string")throw new TypeError("Expected path to be a string or Buffer");let A=null;if(typeof q==="string")q={encoding:q};return H.watch(S,q||{},(B,C)=>{if(z.push({eventType:B,filename:C}),A){const D=A;A=null,D()}}),{async*[Symbol.asyncIterator](){let B=!1;while(!B){while(z.length){let C=z.shift();if(C.eventType==="close"){B=!0;break}if(C.eventType==="error")throw B=!0,C.filename;yield C}await new Promise((C)=>A=C)}}}}var H=Bun.fs(),G="::bunternal::",I={[G]:(S)=>{var q={[G]:function(z,A,B){var C;try{C=S.apply(H,B),B=void 0}catch(D){B=void 0,A(D);return}z(C)}}[G];return async function(...z){return await new Promise((A,B)=>{process.nextTick(q,A,B,z)})}}}[G],K=I(H.accessSync),L=I(H.appendFileSync),M=I(H.closeSync),N=I(H.copyFileSync),O=I(H.existsSync),P=I(H.chownSync),Q=I(H.chmodSync),U=I(H.fchmodSync),V=I(H.fchownSync),X=I(H.fstatSync),Y=I(H.fsyncSync),Z=I(H.ftruncateSync),_=I(H.futimesSync),$=I(H.lchmodSync),T=I(H.lchownSync),W=I(H.linkSync),k=I(H.lstatSync),E=I(H.mkdirSync),x=I(H.mkdtempSync),F=I(H.openSync),R=I(H.readSync),g=I(H.writeSync),h=I(H.readdirSync),j=I(H.readFileSync),w=I(H.writeFileSync),b=I(H.readlinkSync),u=I(H.realpathSync),d=I(H.renameSync),c=I(H.statSync),v=I(H.symlinkSync),a=I(H.truncateSync),y=I(H.unlinkSync),l=I(H.utimesSync),p=I(H.lutimesSync),m=I(H.rmSync),n=I(H.rmdirSync),t=(S,q,z)=>{return new Promise((A,B)=>{try{var C=H.writevSync(S,q,z)}catch(D){B(D);return}A({bytesWritten:C,buffers:q})})},o=(S,q,z)=>{return new Promise((A,B)=>{try{var C=H.readvSync(S,q,z)}catch(D){B(D);return}A({bytesRead:C,buffers:q})})},r={access:K,appendFile:L,close:M,copyFile:N,exists:O,chown:P,chmod:Q,fchmod:U,fchown:V,fstat:X,fsync:Y,ftruncate:Z,futimes:_,lchmod:$,lchown:T,link:W,lstat:k,mkdir:E,mkdtemp:x,open:F,read:R,write:g,readdir:h,readFile:j,writeFile:w,readlink:b,realpath:u,rename:d,stat:c,symlink:v,truncate:a,unlink:y,utimes:l,lutimes:p,rm:m,rmdir:n,watch:J,writev:t,readv:o,constants,[Symbol.for("CommonJS")]:0};export{t as writev,w as writeFile,g as write,J as watch,l as utimes,y as unlink,a as truncate,v as symlink,c as stat,n as rmdir,m as rm,d as rename,u as realpath,o as readv,b as readlink,h as readdir,j as readFile,R as read,F as open,x as mkdtemp,E as mkdir,p as lutimes,k as lstat,W as link,T as lchown,$ as lchmod,_ as futimes,Z as ftruncate,Y as fsync,X as fstat,V as fchown,U as fchmod,O as exists,r as default,N as copyFile,M as close,P as chown,Q as chmod,L as appendFile,K as access}; diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 0353968fe..272522fc0 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -29,6 +29,8 @@ import fs, { realpathSync, readlinkSync, symlinkSync, + writevSync, + readvSync, } from "node:fs"; import _promises from "node:fs/promises"; @@ -301,6 +303,69 @@ describe("readSync", () => { }); }); +it("writevSync", () => { + var fd = openSync(`${tmpdir()}/writevSync.txt`, "w"); + fs.ftruncateSync(fd, 0); + const buffers = [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6]), new Uint8Array([7, 8, 9])]; + const result = writevSync(fd, buffers); + expect(result).toBe(9); + closeSync(fd); + + fd = openSync(`${tmpdir()}/writevSync.txt`, "r"); + const buf = new Uint8Array(9); + readSync(fd, buf, 0, 9, 0); + expect(buf).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])); +}); + +it("pwritevSync", () => { + var fd = openSync(`${tmpdir()}/pwritevSync.txt`, "w"); + fs.ftruncateSync(fd, 0); + writeSync(fd, "lalalala", 0); + const buffers = [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6]), new Uint8Array([7, 8, 9])]; + const result = writevSync(fd, buffers, "lalalala".length); + expect(result).toBe(9); + closeSync(fd); + + const out = readFileSync(`${tmpdir()}/pwritevSync.txt`); + expect(out.slice(0, "lalalala".length).toString()).toBe("lalalala"); + expect(out.slice("lalalala".length)).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])); +}); + +it("readvSync", () => { + var fd = openSync(`${tmpdir()}/readv.txt`, "w"); + fs.ftruncateSync(fd, 0); + + const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); + writeSync(fd, buf, 0, 9, 0); + closeSync(fd); + + var fd = openSync(`${tmpdir()}/readv.txt`, "r"); + const buffers = [new Uint8Array(3), new Uint8Array(3), new Uint8Array(3)]; + const result = readvSync(fd, buffers); + expect(result).toBe(9); + expect(buffers[0]).toEqual(new Uint8Array([1, 2, 3])); + expect(buffers[1]).toEqual(new Uint8Array([4, 5, 6])); + expect(buffers[2]).toEqual(new Uint8Array([7, 8, 9])); + closeSync(fd); +}); + +it("preadv", () => { + var fd = openSync(`${tmpdir()}/preadv.txt`, "w"); + fs.ftruncateSync(fd, 0); + + const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + writeSync(fd, buf, 0, buf.byteLength, 0); + closeSync(fd); + + var fd = openSync(`${tmpdir()}/preadv.txt`, "r"); + const buffers = [new Uint8Array(3), new Uint8Array(3), new Uint8Array(3)]; + const result = readvSync(fd, buffers, 3); + expect(result).toBe(9); + expect(buffers[0]).toEqual(new Uint8Array([4, 5, 6])); + expect(buffers[1]).toEqual(new Uint8Array([7, 8, 9])); + expect(buffers[2]).toEqual(new Uint8Array([10, 11, 12])); +}); + describe("writeSync", () => { it("works with a position set to 0", () => { const fd = openSync(import.meta.dir + "/writeFileSync.txt", "w+"); -- cgit v1.2.3 From 70a87e11818d36f5922a495e2aed8618b01e9a73 Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Thu, 29 Jun 2023 16:36:59 -0700 Subject: Make `node:os` `tmpdir` more consistent with Node.js --- src/bun.js/node/node_os.zig | 28 ++-------------------------- src/js/node/os.js | 23 +++++++++++++++++++++-- src/js/out/modules/node/os.js | 14 ++++++++++++-- test/js/node/os/os.test.js | 5 +++++ 4 files changed, 40 insertions(+), 30 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 7f9ea2e28..f71143315 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -16,7 +16,7 @@ pub const Os = struct { pub const code = @embedFile("../os.exports.js"); pub fn create(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { - const module = JSC.JSValue.createEmptyObject(globalObject, 20); + const module = JSC.JSValue.createEmptyObject(globalObject, 22); module.put(globalObject, JSC.ZigString.static("arch"), JSC.NewFunction(globalObject, JSC.ZigString.static("arch"), 0, arch, true)); module.put(globalObject, JSC.ZigString.static("cpus"), JSC.NewFunction(globalObject, JSC.ZigString.static("cpus"), 0, cpus, true)); @@ -31,7 +31,6 @@ pub const Os = struct { module.put(globalObject, JSC.ZigString.static("platform"), JSC.NewFunction(globalObject, JSC.ZigString.static("platform"), 0, platform, true)); module.put(globalObject, JSC.ZigString.static("release"), JSC.NewFunction(globalObject, JSC.ZigString.static("release"), 0, release, true)); module.put(globalObject, JSC.ZigString.static("setPriority"), JSC.NewFunction(globalObject, JSC.ZigString.static("setPriority"), 2, setPriority, true)); - module.put(globalObject, JSC.ZigString.static("tmpdir"), JSC.NewFunction(globalObject, JSC.ZigString.static("tmpdir"), 0, tmpdir, true)); module.put(globalObject, JSC.ZigString.static("totalmem"), JSC.NewFunction(globalObject, JSC.ZigString.static("totalmem"), 0, totalmem, true)); module.put(globalObject, JSC.ZigString.static("type"), JSC.NewFunction(globalObject, JSC.ZigString.static("type"), 0, Os.type, true)); module.put(globalObject, JSC.ZigString.static("uptime"), JSC.NewFunction(globalObject, JSC.ZigString.static("uptime"), 0, uptime, true)); @@ -485,7 +484,7 @@ pub const Os = struct { std.os.AF.INET => JSC.ZigString.static("IPv4"), std.os.AF.INET6 => JSC.ZigString.static("IPv6"), else => JSC.ZigString.static("unknown"), - }).toValue(globalThis)); + }).toValueGC(globalThis)); // mac The MAC address of the network interface { @@ -620,29 +619,6 @@ pub const Os = struct { return JSC.JSValue.jsUndefined(); } - pub fn tmpdir(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - - const dir: []const u8 = brk: { - if (comptime Environment.isWindows) { - if (bun.getenvZ("TEMP") orelse bun.getenvZ("TMP")) |tmpdir_| { - break :brk tmpdir_; - } - - if (bun.getenvZ("SYSTEMROOT") orelse bun.getenvZ("WINDIR")) |systemdir_| { - break :brk systemdir_ ++ "\\temp"; - } - } else { - const dir = bun.asByteSlice(bun.getenvZ("TMPDIR") orelse bun.getenvZ("TMP") orelse bun.getenvZ("TEMP") orelse "/tmp"); - break :brk strings.withoutTrailingSlash(dir); - } - - break :brk "unknown"; - }; - - return JSC.ZigString.init(dir).withEncoding().toValueGC(globalThis); - } - pub fn totalmem(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); diff --git a/src/js/node/os.js b/src/js/node/os.js index 3315708ad..3cd0288bd 100644 --- a/src/js/node/os.js +++ b/src/js/node/os.js @@ -1,4 +1,22 @@ // Hardcoded module "node:os" + +export var tmpdir = function () { + var lazy = Symbol.for("Bun.lazy"); + var primordials = globalThis[lazy]("primordials"); + + var { Bun } = primordials; + var env = Bun.env; + + tmpdir = function () { + var path = env["TMPDIR"] || env["TMP"] || env["TEMP"] || "/tmp"; + const length = path.length; + if (length > 1 && path[length - 1] === "/") path = path.slice(0, -1); + return path; + }; + + return tmpdir(); +}; + function bound(obj) { return { arch: obj.arch.bind(obj), @@ -13,7 +31,9 @@ function bound(obj) { platform: obj.platform.bind(obj), release: obj.release.bind(obj), setPriority: obj.setPriority.bind(obj), - tmpdir: obj.tmpdir.bind(obj), + get tmpdir() { + return tmpdir; + }, totalmem: obj.totalmem.bind(obj), type: obj.type.bind(obj), uptime: obj.uptime.bind(obj), @@ -42,7 +62,6 @@ export var { platform, release, setPriority, - tmpdir, totalmem, type, uptime, diff --git a/src/js/out/modules/node/os.js b/src/js/out/modules/node/os.js index e27464e15..69c112412 100644 --- a/src/js/out/modules/node/os.js +++ b/src/js/out/modules/node/os.js @@ -12,7 +12,9 @@ var bound = function(obj) { platform: obj.platform.bind(obj), release: obj.release.bind(obj), setPriority: obj.setPriority.bind(obj), - tmpdir: obj.tmpdir.bind(obj), + get tmpdir() { + return tmpdir; + }, totalmem: obj.totalmem.bind(obj), type: obj.type.bind(obj), uptime: obj.uptime.bind(obj), @@ -24,6 +26,15 @@ var bound = function(obj) { constants: obj.constants, [Symbol.for("CommonJS")]: 0 }; +}, tmpdir = function() { + var lazy = Symbol.for("Bun.lazy"), primordials = globalThis[lazy]("primordials"), { Bun: Bun2 } = primordials, env = Bun2.env; + return tmpdir = function() { + var path = env["TMPDIR"] || env["TMP"] || env["TEMP"] || "/tmp"; + const length = path.length; + if (length > 1 && path[length - 1] === "/") + path = path.slice(0, -1); + return path; + }, tmpdir(); }, os = bound(Bun._Os()), { arch, cpus, @@ -37,7 +48,6 @@ var bound = function(obj) { platform, release, setPriority, - tmpdir, totalmem, type, uptime, diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index d7229b56d..8b4d54bb7 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -43,11 +43,16 @@ it("tmpdir", () => { expect(os.tmpdir()).toBe(process.env.TEMP || process.env.TMP); expect(os.tmpdir()).toBe(`${process.env.SystemRoot || process.env.windir}\\temp`); } else { + const originalEnv = process.env.TMPDIR; let dir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp"; if (dir.length > 1 && dir.endsWith("/")) { dir = dir.substring(0, dir.length - 1); } expect(realpathSync(os.tmpdir())).toBe(realpathSync(dir)); + + process.env.TMPDIR = "/boop"; + expect(os.tmpdir()).toBe("/boop"); + process.env.TMPDIR = originalEnv; } }); -- cgit v1.2.3 From f00e2be548da21b9feaef178bb0ac22230801d6f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 1 Jul 2023 17:37:44 -0700 Subject: Use `BunString` in `SystemError` (#3485) * Use `BunString` in SystemError * Use Bun::toStringRef when we will de-ref strings * Move `napi_create_error` to C++ to support `code` being a Symbol potentially * Update blob.zig * Make this test less flaky --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/api/bun/dns_resolver.zig | 8 ++-- src/bun.js/api/bun/socket.zig | 12 ++--- src/bun.js/api/ffi.zig | 6 +-- src/bun.js/api/server.zig | 12 ++--- src/bun.js/bindings/BunString.cpp | 48 +++++++++++++++++--- src/bun.js/bindings/bindings.cpp | 66 +++++++++++++--------------- src/bun.js/bindings/bindings.zig | 8 ++-- src/bun.js/bindings/headers-handwritten.h | 12 +++-- src/bun.js/bindings/helpers.h | 4 +- src/bun.js/bindings/napi.cpp | 29 +++++++++--- src/bun.js/bindings/webcore/JSCloseEvent.cpp | 2 +- src/bun.js/node/node_os.zig | 32 +++++++------- src/bun.js/node/syscall.zig | 8 ++-- src/bun.js/webcore/blob.zig | 55 +++++++++++------------ src/bun.js/webcore/response.zig | 14 +++--- src/bun.js/webcore/streams.zig | 6 +-- src/bundler/bundle_v2.zig | 16 +++++-- src/http.zig | 5 ++- src/napi/napi.zig | 11 +---- test/js/bun/util/error-gc-test.test.js | 41 ++++++++++++++++- 20 files changed, 242 insertions(+), 153 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index fee834e5e..d0d4f5b7b 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -1925,8 +1925,8 @@ pub const DNSResolver = struct { .err => |err| { const system_error = JSC.SystemError{ .errno = -1, - .code = JSC.ZigString.init(err.code()), - .message = JSC.ZigString.init(err.label()), + .code = bun.String.static(err.code()), + .message = bun.String.static(err.label()), }; globalThis.throwValue(system_error.toErrorInstance(globalThis)); @@ -1972,8 +1972,8 @@ pub const DNSResolver = struct { .err => |err| { const system_error = JSC.SystemError{ .errno = -1, - .code = JSC.ZigString.init(err.code()), - .message = JSC.ZigString.init(err.label()), + .code = bun.String.static(err.code()), + .message = bun.String.static(err.label()), }; globalThis.throwValue(system_error.toErrorInstance(globalThis)); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 00e34a77d..69d6611cb 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1022,8 +1022,8 @@ fn NewSocket(comptime ssl: bool) type { var globalObject = handlers.globalObject; const err = JSC.SystemError{ .errno = errno, - .message = ZigString.init("Failed to connect"), - .syscall = ZigString.init("connect"), + .message = bun.String.static("Failed to connect"), + .syscall = bun.String.static("connect"), }; if (callback == .zero) { @@ -1232,8 +1232,8 @@ fn NewSocket(comptime ssl: bool) type { const reason = if (ssl_error.reason == null) "" else ssl_error.reason[0..bun.len(ssl_error.reason)]; const fallback = JSC.SystemError{ - .code = ZigString.init(code), - .message = ZigString.init(reason), + .code = bun.String.create(code), + .message = bun.String.create(reason), }; authorization_error = fallback.toErrorInstance(globalObject); @@ -1409,8 +1409,8 @@ fn NewSocket(comptime ssl: bool) type { const reason = if (ssl_error.reason == null) "" else ssl_error.reason[0..bun.len(ssl_error.reason)]; const fallback = JSC.SystemError{ - .code = ZigString.init(code), - .message = ZigString.init(reason), + .code = bun.String.create(code), + .message = bun.String.create(reason), }; return fallback.toErrorInstance(globalObject); diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index e46e054ec..ba31b67ed 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -311,9 +311,9 @@ pub const FFI = struct { break :brk std.DynLib.open(backup_name) catch { // Then, if that fails, report an error. const system_error = JSC.SystemError{ - .code = ZigString.init(@tagName(JSC.Node.ErrorCode.ERR_DLOPEN_FAILED)), - .message = ZigString.init("Failed to open library. This is usually caused by a missing library or an invalid library path."), - .syscall = ZigString.init("dlopen"), + .code = bun.String.create(@tagName(JSC.Node.ErrorCode.ERR_DLOPEN_FAILED)), + .message = bun.String.create("Failed to open library. This is usually caused by a missing library or an invalid library path."), + .syscall = bun.String.create("dlopen"), }; return system_error.toErrorInstance(global); }; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 136737069..140e62ce4 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1796,7 +1796,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .syscall = .sendfile, }; var sys = err.withPathLike(file.pathlike).toSystemError(); - sys.message = ZigString.init("MacOS does not support sending non-regular files"); + sys.message = bun.String.static("MacOS does not support sending non-regular files"); this.runErrorHandler(sys.toErrorInstance( this.server.globalThis, )); @@ -1815,7 +1815,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .syscall = .sendfile, }; var sys = err.withPathLike(file.pathlike).toSystemError(); - sys.message = ZigString.init("File must be regular or FIFO"); + sys.message = bun.String.static("File must be regular or FIFO"); this.runErrorHandler(sys.toErrorInstance( this.server.globalThis, )); @@ -2375,8 +2375,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } const fallback = JSC.SystemError{ - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_UNHANDLED_ERROR))), - .message = ZigString.init("Unhandled error in ReadableStream"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_UNHANDLED_ERROR))), + .message = bun.String.static("Unhandled error in ReadableStream"), }; req.handleReject(fallback.toErrorInstance(globalThis)); } @@ -2422,8 +2422,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (stream.isLocked(this.server.globalThis)) { streamLog("was locked but it shouldn't be", .{}); var err = JSC.SystemError{ - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), - .message = ZigString.init("Stream already used, please create a new one"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), + .message = bun.String.static("Stream already used, please create a new one"), }; stream.value.unprotect(); this.runErrorHandler(err.toErrorInstance(this.server.globalThis)); diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index f590edd35..4c8ff384e 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -86,31 +86,69 @@ BunString toString(JSC::JSGlobalObject* globalObject, JSValue value) return fromJS(globalObject, value); } +BunString toStringRef(JSC::JSGlobalObject* globalObject, JSValue value) +{ + auto str = value.toWTFString(globalObject); + if (str.isEmpty()) { + return { BunStringTag::Empty }; + } + + str.impl()->ref(); + + return { BunStringTag::WTFStringImpl, { .wtf = str.impl() } }; +} + BunString toString(WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; } BunString toString(const WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; } BunString toString(WTF::StringImpl* wtfString) { - if (wtfString->length() == 0) + if (wtfString->isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; } +BunString toStringRef(WTF::String& wtfString) +{ + if (wtfString.isEmpty()) + return { BunStringTag::Empty }; + + wtfString.impl()->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; +} +BunString toStringRef(const WTF::String& wtfString) +{ + if (wtfString.isEmpty()) + return { BunStringTag::Empty }; + + wtfString.impl()->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; +} +BunString toStringRef(WTF::StringImpl* wtfString) +{ + if (wtfString->isEmpty()) + return { BunStringTag::Empty }; + + wtfString->ref(); + + return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; +} + BunString fromString(WTF::String& wtfString) { - if (wtfString.length() == 0) + if (wtfString.isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString.impl() } }; @@ -118,7 +156,7 @@ BunString fromString(WTF::String& wtfString) BunString fromString(WTF::StringImpl* wtfString) { - if (wtfString->length() == 0) + if (wtfString->isEmpty()) return { BunStringTag::Empty }; return { BunStringTag::WTFStringImpl, { .wtf = wtfString } }; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 96fcde303..9f9b20c1e 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1279,15 +1279,14 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC__JSGlobalObject* globalObject) { - static const char* system_error_name = "SystemError"; SystemError err = *arg0; JSC::VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSC::JSValue message = JSC::jsUndefined(); - if (err.message.len > 0) { - message = Zig::toJSString(err.message, globalObject); + if (err.message.tag != BunStringTag::Empty) { + message = Bun::toJS(globalObject, err.message); } JSC::JSValue options = JSC::jsUndefined(); @@ -1297,8 +1296,8 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, auto clientData = WebCore::clientData(vm); - if (err.code.len > 0 && !(err.code.len == 1 and err.code.ptr[0] == 0)) { - JSC::JSValue code = Zig::toJSStringGC(err.code, globalObject); + if (err.code.tag != BunStringTag::Empty) { + JSC::JSValue code = Bun::toJS(globalObject, err.code); result->putDirect(vm, clientData->builtinNames().codePublicName(), code, JSC::PropertyAttribute::DontDelete | 0); @@ -1307,13 +1306,12 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, result->putDirect( vm, vm.propertyNames->name, - JSC::JSValue(JSC::jsOwnedString( - vm, WTF::String(WTF::StringImpl::createWithoutCopying(system_error_name, 11)))), + JSC::JSValue(jsString(vm, String("SystemError"_s))), JSC::PropertyAttribute::DontEnum | 0); } - if (err.path.len > 0) { - JSC::JSValue path = JSC::JSValue(Zig::toJSStringGC(err.path, globalObject)); + if (err.path.tag != BunStringTag::Empty) { + JSC::JSValue path = Bun::toJS(globalObject, err.path); result->putDirect(vm, clientData->builtinNames().pathPublicName(), path, JSC::PropertyAttribute::DontDelete | 0); } @@ -1324,8 +1322,8 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC::PropertyAttribute::DontDelete | 0); } - if (err.syscall.len > 0) { - JSC::JSValue syscall = JSC::JSValue(Zig::toJSString(err.syscall, globalObject)); + if (err.syscall.tag != BunStringTag::Empty) { + JSC::JSValue syscall = Bun::toJS(globalObject, err.syscall); result->putDirect(vm, clientData->builtinNames().syscallPublicName(), syscall, JSC::PropertyAttribute::DontDelete | 0); } @@ -3303,11 +3301,8 @@ bool JSC__JSValue__stringIncludes(JSC__JSValue value, JSC__JSGlobalObject* globa static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stackFrame, ZigStackFrame* frame) { - String str = stackFrame->sourceURL(vm); - if (!str.isEmpty()) - str.impl()->ref(); - frame->source_url = Bun::toString(str); + frame->source_url = Bun::toStringRef(stackFrame->sourceURL(vm)); if (stackFrame->isWasmFrame()) { frame->code_type = ZigStackFrameCodeWasm; @@ -3344,10 +3339,7 @@ static void populateStackFrameMetadata(JSC::VM& vm, const JSC::StackFrame* stack JSC::JSObject* callee = JSC::jsCast(calleeCell); - String displayName = JSC::getCalculatedDisplayName(vm, callee); - if (!displayName.isEmpty()) - displayName.impl()->ref(); - frame->function_name = Bun::toString(displayName); + frame->function_name = Bun::toStringRef(JSC::getCalculatedDisplayName(vm, callee)); } // Based on // https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/src/shim/JSCStackTrace.cpp#L298 @@ -3421,7 +3413,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunStr // Most of the time, when you look at a stack trace, you want a couple lines above - source_lines[0] = Bun::toString(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); + source_lines[0] = Bun::toStringRef(sourceString.substring(lineStart, lineStop - lineStart).toStringWithoutCopying()); source_line_numbers[0] = line; if (lineStart > 0) { @@ -3438,7 +3430,7 @@ static void populateStackFramePosition(const JSC::StackFrame* stackFrame, BunStr } // We are at the beginning of the line - source_lines[source_line_i] = Bun::toString(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); + source_lines[source_line_i] = Bun::toStringRef(sourceString.substring(byte_offset_in_source_string, end_of_line_offset - byte_offset_in_source_string + 1).toStringWithoutCopying()); source_line_numbers[source_line_i] = line - source_line_i; source_line_i++; @@ -3526,30 +3518,32 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, except->code = 8; } if (except->code == SYNTAX_ERROR_CODE) { - except->message = Bun::toString(err->sanitizedMessageString(global)); + except->message = Bun::toStringRef(err->sanitizedMessageString(global)); } else if (JSC::JSValue message = obj->getIfPropertyExists(global, vm.propertyNames->message)) { - except->message = Bun::toString(global, message); + except->message = Bun::toStringRef(global, message); } else { - except->message = Bun::toString(err->sanitizedMessageString(global)); + except->message = Bun::toStringRef(err->sanitizedMessageString(global)); } - except->name = Bun::toString(err->sanitizedNameString(global)); + + except->name = Bun::toStringRef(err->sanitizedNameString(global)); + except->runtime_type = err->runtimeTypeForCause(); auto clientData = WebCore::clientData(vm); if (except->code != SYNTAX_ERROR_CODE) { if (JSC::JSValue syscall = obj->getIfPropertyExists(global, clientData->builtinNames().syscallPublicName())) { - except->syscall = Bun::toString(global, syscall); + except->syscall = Bun::toStringRef(global, syscall); } if (JSC::JSValue code = obj->getIfPropertyExists(global, clientData->builtinNames().codePublicName())) { - except->code_ = Bun::toString(global, code); + except->code_ = Bun::toStringRef(global, code); } if (JSC::JSValue path = obj->getIfPropertyExists(global, clientData->builtinNames().pathPublicName())) { - except->path = Bun::toString(global, path); + except->path = Bun::toStringRef(global, path); } if (JSC::JSValue fd = obj->getIfPropertyExists(global, Identifier::fromString(vm, "fd"_s))) { @@ -3565,7 +3559,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (getFromSourceURL) { if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, vm.propertyNames->sourceURL)) { - except->stack.frames_ptr[0].source_url = Bun::toString(global, sourceURL); + except->stack.frames_ptr[0].source_url = Bun::toStringRef(global, sourceURL); if (JSC::JSValue column = obj->getIfPropertyExists(global, vm.propertyNames->column)) { except->stack.frames_ptr[0].position.column_start = column.toInt32(global); @@ -3577,7 +3571,7 @@ static void fromErrorInstance(ZigException* except, JSC::JSGlobalObject* global, if (JSC::JSValue lineText = obj->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "lineText"_s))) { if (JSC::JSString* jsStr = lineText.toStringOrNull(global)) { auto str = jsStr->value(global); - except->stack.source_lines_ptr[0] = Bun::toString(str); + except->stack.source_lines_ptr[0] = Bun::toStringRef(str); except->stack.source_lines_numbers[0] = except->stack.frames_ptr[0].position.line; except->stack.source_lines_len = 1; except->remapped = true; @@ -3600,7 +3594,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSObject* obj = JSC::jsDynamicCast(value)) { if (obj->hasProperty(global, global->vm().propertyNames->name)) { auto name_str = obj->getIfPropertyExists(global, global->vm().propertyNames->name).toWTFString(global); - except->name = Bun::toString(name_str); + except->name = Bun::toStringRef(name_str); if (name_str == "Error"_s) { except->code = JSErrorCodeError; } else if (name_str == "EvalError"_s) { @@ -3622,14 +3616,14 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal if (JSC::JSValue message = obj->getIfPropertyExists(global, global->vm().propertyNames->message)) { if (message) { - except->message = Bun::toString( + except->message = Bun::toStringRef( message.toWTFString(global)); } } if (JSC::JSValue sourceURL = obj->getIfPropertyExists(global, global->vm().propertyNames->sourceURL)) { if (sourceURL) { - except->stack.frames_ptr[0].source_url = Bun::toString( + except->stack.frames_ptr[0].source_url = Bun::toStringRef( sourceURL.toWTFString(global)); except->stack.frames_len = 1; } @@ -3658,7 +3652,7 @@ void exceptionFromString(ZigException* except, JSC::JSValue value, JSC::JSGlobal } scope.release(); - except->message = Bun::toString(str); + except->message = Bun::toStringRef(str); } void JSC__VM__releaseWeakRefs(JSC__VM* arg0) @@ -3768,8 +3762,8 @@ void JSC__JSValue__toZigException(JSC__JSValue JSValue0, JSC__JSGlobalObject* ar JSC::JSValue value = JSC::JSValue::decode(JSValue0); if (value == JSC::JSValue {}) { exception->code = JSErrorCodeError; - exception->name = Bun::toString("Error"_s); - exception->message = Bun::toString("Unknown error"_s); + exception->name = Bun::toStringRef("Error"_s); + exception->message = Bun::toStringRef("Unknown error"_s); return; } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 1c09378a8..777860d3c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1561,10 +1561,10 @@ pub const FetchHeaders = opaque { pub const SystemError = extern struct { errno: c_int = 0, /// label for errno - code: ZigString = ZigString.init(""), - message: ZigString = ZigString.init(""), - path: ZigString = ZigString.init(""), - syscall: ZigString = ZigString.init(""), + code: String = String.empty, + message: String = String.empty, + path: String = String.empty, + syscall: String = String.empty, fd: i32 = -1, pub fn Maybe(comptime Result: type) type { diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index c7429b633..90c8f86d2 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -84,10 +84,10 @@ typedef struct ErrorableResolvedSource { typedef struct SystemError { int errno_; - ZigString code; - ZigString message; - ZigString path; - ZigString syscall; + BunString code; + BunString message; + BunString path; + BunString syscall; int fd; } SystemError; @@ -246,6 +246,10 @@ BunString toString(WTF::String& wtfString); BunString toString(const WTF::String& wtfString); BunString toString(WTF::StringImpl* wtfString); +BunString toStringRef(JSC::JSGlobalObject* globalObject, JSC::JSValue value); +BunString toStringRef(WTF::String& wtfString); +BunString toStringRef(const WTF::String& wtfString); +BunString toStringRef(WTF::StringImpl* wtfString); } using Uint8Array_alias = JSC::JSUint8Array; diff --git a/src/bun.js/bindings/helpers.h b/src/bun.js/bindings/helpers.h index 402807f3d..00777c304 100644 --- a/src/bun.js/bindings/helpers.h +++ b/src/bun.js/bindings/helpers.h @@ -342,10 +342,10 @@ static const WTF::String toStringStatic(ZigString str) } if (isTaggedUTF16Ptr(str.ptr)) { - return WTF::String(WTF::ExternalStringImpl::createStatic(reinterpret_cast(untag(str.ptr)), str.len)); + return WTF::String(AtomStringImpl::add(reinterpret_cast(untag(str.ptr)), str.len)); } - return WTF::String(WTF::ExternalStringImpl::createStatic( + return WTF::String(AtomStringImpl::add( reinterpret_cast(untag(str.ptr)), str.len)); } diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index a859e3ac5..bb62cb2a0 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -554,7 +554,6 @@ extern "C" napi_status napi_wrap(napi_env env, auto* globalObject = toJS(env); auto& vm = globalObject->vm(); - auto* val = jsDynamicCast(value); @@ -572,7 +571,7 @@ extern "C" napi_status napi_wrap(napi_env env, auto clientData = WebCore::clientData(vm); auto* ref = new NapiRef(globalObject, 1); - ref->strongRef.set(globalObject->vm(), value.getObject()); + ref->strongRef.set(globalObject->vm(), value.getObject()); if (finalize_cb) { ref->finalizer.finalize_cb = finalize_cb; @@ -816,7 +815,7 @@ extern "C" napi_status napi_create_reference(napi_env env, napi_value value, } } - if(object) { + if (object) { object->napiRef = ref; } @@ -1029,7 +1028,26 @@ extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, auto error = JSC::createTypeError(globalObject, messageValue.toWTFString(globalObject)); if (codeValue) { - error->putDirect(vm, Identifier::fromString(vm, "code"_s), codeValue, 0); + error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), codeValue, 0); + } + + *result = reinterpret_cast(JSC::JSValue::encode(error)); + return napi_ok; +} + +extern "C" napi_status napi_create_error(napi_env env, napi_value code, + napi_value msg, + napi_value* result) +{ + Zig::GlobalObject* globalObject = toJS(env); + JSC::VM& vm = globalObject->vm(); + + JSC::JSValue codeValue = JSC::JSValue::decode(reinterpret_cast(code)); + JSC::JSValue messageValue = JSC::JSValue::decode(reinterpret_cast(msg)); + + auto error = JSC::createError(globalObject, messageValue.toWTFString(globalObject)); + if (codeValue) { + error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), codeValue, 0); } *result = reinterpret_cast(JSC::JSValue::encode(error)); @@ -1474,7 +1492,8 @@ extern "C" napi_status napi_get_property_names(napi_env env, napi_value object, return napi_ok; } -extern "C" napi_status napi_create_object(napi_env env, napi_value* result){ +extern "C" napi_status napi_create_object(napi_env env, napi_value* result) +{ if (UNLIKELY(result == nullptr)) { return napi_invalid_arg; diff --git a/src/bun.js/bindings/webcore/JSCloseEvent.cpp b/src/bun.js/bindings/webcore/JSCloseEvent.cpp index be07cbcfe..ad7b6ed57 100644 --- a/src/bun.js/bindings/webcore/JSCloseEvent.cpp +++ b/src/bun.js/bindings/webcore/JSCloseEvent.cpp @@ -99,7 +99,7 @@ template<> CloseEvent::Init convertDictionary(JSGlobalObject& if (isNullOrUndefined) codeValue = jsUndefined(); else { - codeValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "code"_s)); + codeValue = object->get(&lexicalGlobalObject, WebCore::builtinNames(vm).codePublicName()); RETURN_IF_EXCEPTION(throwScope, {}); } if (!codeValue.isUndefined()) { diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index f71143315..483acb3e2 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -78,8 +78,8 @@ pub const Os = struct { return if (comptime Environment.isLinux) cpusImplLinux(globalThis) catch { const err = JSC.SystemError{ - .message = JSC.ZigString.init("Failed to get cpu information"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("Failed to get cpu information"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -88,8 +88,8 @@ pub const Os = struct { else if (comptime Environment.isMac) cpusImplDarwin(globalThis) catch { const err = JSC.SystemError{ - .message = JSC.ZigString.init("Failed to get cpu information"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("Failed to get cpu information"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -318,11 +318,11 @@ pub const Os = struct { //info.put(globalThis, JSC.ZigString.static("syscall"), JSC.ZigString.init("uv_os_getpriority").withEncoding().toValueGC(globalThis)); const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_getpriority returned ESRCH (no such process)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -3, - .syscall = JSC.ZigString.init("uv_os_getpriority"), + .syscall = bun.String.static("uv_os_getpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -377,10 +377,10 @@ pub const Os = struct { const rc = C.getifaddrs(&interface_start); if (rc != 0) { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: getifaddrs returned an error"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: getifaddrs returned an error"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), .errno = @intFromEnum(std.os.errno(rc)), - .syscall = JSC.ZigString.init("getifaddrs"), + .syscall = bun.String.static("getifaddrs"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -591,11 +591,11 @@ pub const Os = struct { switch (errcode) { .SRCH => { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_setpriority returned ESRCH (no such process)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -3, - .syscall = JSC.ZigString.init("uv_os_setpriority"), + .syscall = bun.String.static("uv_os_setpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); @@ -603,11 +603,11 @@ pub const Os = struct { }, .ACCES => { const err = JSC.SystemError{ - .message = JSC.ZigString.init("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), - .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .message = bun.String.static("A system error occurred: uv_os_setpriority returned EACCESS (permission denied)"), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), //.info = info, .errno = -13, - .syscall = JSC.ZigString.init("uv_os_setpriority"), + .syscall = bun.String.static("uv_os_setpriority"), }; globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); diff --git a/src/bun.js/node/syscall.zig b/src/bun.js/node/syscall.zig index 48c5b1305..5ff0b2f44 100644 --- a/src/bun.js/node/syscall.zig +++ b/src/bun.js/node/syscall.zig @@ -873,20 +873,20 @@ pub const Error = struct { pub fn toSystemError(this: Error) SystemError { var err = SystemError{ .errno = @as(c_int, this.errno) * -1, - .syscall = JSC.ZigString.init(@tagName(this.syscall)), + .syscall = bun.String.static(@tagName(this.syscall)), }; // errno label if (this.errno > 0 and this.errno < C.SystemErrno.max) { const system_errno = @enumFromInt(C.SystemErrno, this.errno); - err.code = JSC.ZigString.init(@tagName(system_errno)); + err.code = bun.String.static(@tagName(system_errno)); if (C.SystemErrno.labels.get(system_errno)) |label| { - err.message = JSC.ZigString.init(label); + err.message = bun.String.static(label); } } if (this.path.len > 0) { - err.path = JSC.ZigString.init(this.path); + err.path = bun.String.create(this.path); } if (this.fd != -1) { diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index faf503a3f..86b5414e3 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1194,9 +1194,6 @@ pub const Blob = struct { .syscall = .open, }).toSystemError(); - // assert we never end up reusing the memory - std.debug.assert(@intFromPtr(this.system_error.?.path.slice().ptr) != @intFromPtr(path_buffer)); - callback(this, null_fd); return; }; @@ -1359,12 +1356,13 @@ pub const Blob = struct { return; } else if (this.store == null) { bun.default_allocator.destroy(this); - cb(cb_ctx, ResultType{ .err = SystemError{ - .code = ZigString.init("INTERNAL_ERROR"), - .path = ZigString.Empty, - .message = ZigString.init("assertion failure - store should not be null"), - .syscall = ZigString.init("read"), - } }); + cb(cb_ctx, ResultType{ + .err = SystemError{ + .code = bun.String.static("INTERNAL_ERROR"), + .message = bun.String.static("assertion failure - store should not be null"), + .syscall = bun.String.static("read"), + }, + }); return; } @@ -1396,12 +1394,12 @@ pub const Blob = struct { }).toSystemError(); } else { this.system_error = JSC.SystemError{ - .code = ZigString.init(bun.asByteSlice(@errorName(err))), + .code = bun.String.static(bun.asByteSlice(@errorName(err))), .path = if (this.file_store.pathlike == .path) - ZigString.init(this.file_store.pathlike.path.slice()) + bun.String.create(this.file_store.pathlike.path.slice()) else - ZigString.Empty, - .syscall = ZigString.init("read"), + bun.String.empty, + .syscall = bun.String.static("read"), }; this.errno = err; @@ -1458,13 +1456,13 @@ pub const Blob = struct { if (std.os.S.ISDIR(stat.mode)) { this.errno = error.EISDIR; this.system_error = JSC.SystemError{ - .code = ZigString.init("EISDIR"), + .code = bun.String.static("EISDIR"), .path = if (this.file_store.pathlike == .path) - ZigString.init(this.file_store.pathlike.path.slice()) + bun.String.create(this.file_store.pathlike.path.slice()) else - ZigString.Empty, - .message = ZigString.init("Directories cannot be read like files"), - .syscall = ZigString.init("read"), + bun.String.empty, + .message = bun.String.static("Directories cannot be read like files"), + .syscall = bun.String.static("read"), }; return; } @@ -1643,8 +1641,8 @@ pub const Blob = struct { this.wrote += @truncate(SizeType, result catch |errno| { this.errno = errno; this.system_error = this.system_error orelse JSC.SystemError{ - .code = ZigString.init(bun.asByteSlice(@errorName(errno))), - .syscall = ZigString.init("write"), + .code = bun.String.static(bun.asByteSlice(@errorName(errno))), + .syscall = bun.String.static("write"), }; this.wrote = 0; @@ -1703,13 +1701,13 @@ pub const Blob = struct { const unsupported_directory_error = SystemError{ .errno = @intCast(c_int, @intFromEnum(bun.C.SystemErrno.EISDIR)), - .message = ZigString.init("That doesn't work on folders"), - .syscall = ZigString.init("fstat"), + .message = bun.String.static("That doesn't work on folders"), + .syscall = bun.String.static("fstat"), }; const unsupported_non_regular_file_error = SystemError{ .errno = @intCast(c_int, @intFromEnum(bun.C.SystemErrno.ENOTSUP)), - .message = ZigString.init("Non-regular files aren't supported yet"), - .syscall = ZigString.init("fstat"), + .message = bun.String.static("Non-regular files aren't supported yet"), + .syscall = bun.String.static("fstat"), }; // blocking, but off the main thread @@ -1777,13 +1775,12 @@ pub const Blob = struct { pub fn reject(this: *CopyFile, promise: *JSC.JSPromise) void { var globalThis = this.globalThis; var system_error: SystemError = this.system_error orelse SystemError{}; - if (this.source_file_store.pathlike == .path and system_error.path.len == 0) { - system_error.path = ZigString.init(this.source_file_store.pathlike.path.slice()); - system_error.path.mark(); + if (this.source_file_store.pathlike == .path and system_error.path.isEmpty()) { + system_error.path = bun.String.create(this.source_file_store.pathlike.path.slice()); } - if (system_error.message.len == 0) { - system_error.message = ZigString.init("Failed to copy file"); + if (system_error.message.isEmpty()) { + system_error.message = bun.String.static("Failed to copy file"); } var instance = system_error.toErrorInstance(this.globalThis); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index b4ea08579..e888ffa5a 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -777,15 +777,15 @@ pub const Fetch = struct { } const fetch_error = JSC.SystemError{ - .code = ZigString.init(@errorName(this.result.fail)), + .code = bun.String.static(@errorName(this.result.fail)), .message = switch (this.result.fail) { - error.ConnectionClosed => ZigString.init("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), - error.FailedToOpenSocket => ZigString.init("Was there a typo in the url or port?"), - error.TooManyRedirects => ZigString.init("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), - error.ConnectionRefused => ZigString.init("Unable to connect. Is the computer able to access the url?"), - else => ZigString.init("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), + error.ConnectionClosed => bun.String.static("The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"), + error.FailedToOpenSocket => bun.String.static("Was there a typo in the url or port?"), + error.TooManyRedirects => bun.String.static("The response redirected too many times. For more information, pass `verbose: true` in the second argument to fetch()"), + error.ConnectionRefused => bun.String.static("Unable to connect. Is the computer able to access the url?"), + else => bun.String.static("fetch() failed. For more information, pass `verbose: true` in the second argument to fetch()"), }, - .path = ZigString.init(this.http.?.url.href), + .path = bun.String.create(this.http.?.url.href), }; return fetch_error.toErrorInstance(this.global_this); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 5986afac7..343ce37ab 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -1964,10 +1964,10 @@ pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { pub const message = std.fmt.comptimePrint("{s} is not constructable", .{SinkType.name}); }; const err = JSC.SystemError{ - .message = ZigString.init(Static.message), - .code = ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), + .message = bun.String.static(Static.message), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), }; - globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); + globalThis.throwValue(err.toErrorInstance(globalThis)); return JSC.JSValue.jsUndefined(); } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 814e49a20..f534e4184 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -9174,8 +9174,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + var message = err.toSystemError().message.toUTF8(bun.default_allocator); + defer message.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing sourcemap for chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(message.slice()), bun.fmt.quote(chunk.final_rel_path), }) catch unreachable; return error.WriteFailed; @@ -9242,8 +9244,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + var message = err.toSystemError().message.toUTF8(bun.default_allocator); + defer message.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(message.slice()), bun.fmt.quote(chunk.final_rel_path), }) catch unreachable; return error.WriteFailed; @@ -9309,8 +9313,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); + defer utf8.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing chunk {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(utf8.slice()), bun.fmt.quote(components_manifest_path), }) catch unreachable; return error.WriteFailed; @@ -9383,8 +9389,10 @@ const LinkerContext = struct { }, )) { .err => |err| { + const utf8 = err.toSystemError().message.toUTF8(bun.default_allocator); + defer utf8.deinit(); c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{} writing file {}", .{ - bun.fmt.quote(err.toSystemError().message.slice()), + bun.fmt.quote(utf8.slice()), bun.fmt.quote(src.src_path.text), }) catch unreachable; return error.WriteFailed; diff --git a/src/http.zig b/src/http.zig index 80718db2f..b1d97c382 100644 --- a/src/http.zig +++ b/src/http.zig @@ -684,8 +684,9 @@ pub const RequestContext = struct { if (erro == error.EBADF or erro == error.ECONNABORTED or erro == error.ECONNREFUSED) { return error.SocketClosed; } - - Output.prettyErrorln("send() error: {s}", .{err.toSystemError().message.slice()}); + const msg = err.toSystemError().message.toUTF8(bun.default_allocator); + defer msg.deinit(); + Output.prettyErrorln("send() error: {s}", .{msg.slice()}); return erro; }, diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 0973ca559..439319489 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -303,16 +303,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: [*]const char16_t, le return .ok; } pub extern fn napi_create_symbol(env: napi_env, description: napi_value, result: *napi_value) napi_status; -pub export fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status { - log("napi_create_error: \"{any}\"", .{msg.getZigString(env)}); - const system_error = JSC.SystemError{ - .code = if (!code.isEmptyOrUndefinedOrNull()) code.getZigString(env) else ZigString.Empty, - .message = msg.getZigString(env), - }; - result.* = system_error.toErrorInstance(env); - return .ok; -} - +pub extern fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_type_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_range_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_typeof(env: napi_env, value: napi_value, result: *napi_valuetype) napi_status; diff --git a/test/js/bun/util/error-gc-test.test.js b/test/js/bun/util/error-gc-test.test.js index 247bd68ef..4a45346b6 100644 --- a/test/js/bun/util/error-gc-test.test.js +++ b/test/js/bun/util/error-gc-test.test.js @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; - +import { readFileSync } from "fs"; // This test checks that printing stack traces increments and decrements // reference-counted strings test("error gc test", () => { @@ -34,7 +34,7 @@ test("error gc test #2", () => { } }); -test("error gc test #2", () => { +test("error gc test #3", () => { for (let i = 0; i < 1000; i++) { var err = new Error(); Error.captureStackTrace(err); @@ -42,3 +42,40 @@ test("error gc test #2", () => { Bun.gc(); } }); + +// This test fails if: +// - it crashes +// - The test failure message gets a non-sensical error +test("error gc test #4", () => { + for (let i = 0; i < 1000; i++) { + let path = + // Use a long-enough string for it to be obvious if we leak memory + "/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/ii/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i/don/t/exist/tmp/i"; + try { + readFileSync(path); + throw new Error("unreachable"); + } catch (e) { + if (e.message === "unreachable") { + throw e; + } + + const inspected = Bun.inspect(e); + Bun.gc(true); + + // Deliberately avoid using .toContain() directly to avoid + // BunString shenanigins. + // + // Only JSC builtin functions to operate on the string after inspecting it. + // + if (!inspected.includes(path)) { + expect(inspected).toContain(path); + } + + if (!inspected.includes("ENOENT")) { + expect(inspected).toContain("ENOENT"); + } + } finally { + Bun.gc(true); + } + } +}); -- cgit v1.2.3 From 6cae6ebafeac4ec2698dc746838b91b2f079f65f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 1 Jul 2023 20:02:50 -0700 Subject: Make `buffer.toString("base64")` 4x faster (#3486) * Add libbase64 * Add bench * Update licensing.md --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- .gitmodules | 8 +++++++- Dockerfile | 22 ++++++++++++++++++++++ Makefile | 10 ++++++++-- bench/snippets/base64-buffer-to-string.mjs | 14 ++++++++++++++ docs/project/licensing.md | 5 +++++ src/base64/base64.zig | 20 +++++++++++++++++++- src/bun.js/node/types.zig | 4 ++-- src/deps/base64 | 1 + 8 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 bench/snippets/base64-buffer-to-string.mjs create mode 160000 src/deps/base64 (limited to 'src/bun.js/node') diff --git a/.gitmodules b/.gitmodules index 9284d53f1..e8b448aa4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -68,4 +68,10 @@ fetchRecurseSubmodules = false [submodule "src/deps/zstd"] path = src/deps/zstd url = https://github.com/facebook/zstd.git - ignore = dirty \ No newline at end of file + ignore = dirty +[submodule "src/deps/base64"] + path = src/deps/base64 + url = https://github.com/aklomp/base64.git + ignore = dirty + depth = 1 + shallow = true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f50ada2d8..5f66a5e04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -295,6 +295,27 @@ WORKDIR $BUN_DIR RUN cd $BUN_DIR && \ make uws && rm -rf src/deps/uws Makefile +FROM bun-base as base64 + +ARG DEBIAN_FRONTEND +ARG GITHUB_WORKSPACE +ARG ZIG_PATH +# Directory extracts to "bun-webkit" +ARG WEBKIT_DIR +ARG BUN_RELEASE_DIR +ARG BUN_DEPS_OUT_DIR +ARG BUN_DIR +ARG CPU_TARGET +ENV CPU_TARGET=${CPU_TARGET} + +COPY Makefile ${BUN_DIR}/Makefile +COPY src/deps/base64 ${BUN_DIR}/src/deps/base64 + +WORKDIR $BUN_DIR + +RUN cd $BUN_DIR && \ + make base64 && rm -rf src/deps/base64 Makefile + FROM bun-base as picohttp ARG DEBIAN_FRONTEND @@ -556,6 +577,7 @@ ENV JSC_BASE_DIR=${WEBKIT_DIR} ENV LIB_ICU_PATH=${WEBKIT_DIR}/lib COPY --from=zlib ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ +COPY --from=base64 ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=libarchive ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=boringssl ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=lolhtml ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ diff --git a/Makefile b/Makefile index b023dc53c..b44aa9ecb 100644 --- a/Makefile +++ b/Makefile @@ -453,7 +453,8 @@ MINIMUM_ARCHIVE_FILES = -L$(BUN_DEPS_OUT_DIR) \ -ldecrepit \ -lssl \ -lcrypto \ - -llolhtml + -llolhtml \ + -lbase64 ARCHIVE_FILES_WITHOUT_LIBCRYPTO = $(MINIMUM_ARCHIVE_FILES) \ -larchive \ @@ -1850,6 +1851,10 @@ copy-to-bun-release-dir-bin: PACKAGE_MAP = --pkg-begin async_io $(BUN_DIR)/src/io/io_darwin.zig --pkg-begin bun $(BUN_DIR)/src/bun_redirect.zig --pkg-end --pkg-end --pkg-begin javascript_core $(BUN_DIR)/src/jsc.zig --pkg-begin bun $(BUN_DIR)/src/bun_redirect.zig --pkg-end --pkg-end --pkg-begin bun $(BUN_DIR)/src/bun_redirect.zig --pkg-end +.PHONY: base64 +base64: + cd $(BUN_DEPS_DIR)/base64 && make clean && cmake $(CMAKE_FLAGS) . && make + cp $(BUN_DEPS_DIR)/base64/libbase64.a $(BUN_DEPS_OUT_DIR)/libbase64.a .PHONY: cold-jsc-start cold-jsc-start: @@ -1868,7 +1873,8 @@ cold-jsc-start: misctools/cold-jsc-start.cpp -o cold-jsc-start .PHONY: vendor-without-npm -vendor-without-npm: node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp zlib boringssl libarchive lolhtml sqlite usockets uws tinycc c-ares zstd +vendor-without-npm: node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp zlib boringssl libarchive lolhtml sqlite usockets uws tinycc c-ares zstd base64 + .PHONY: vendor-without-check vendor-without-check: npm-install vendor-without-npm diff --git a/bench/snippets/base64-buffer-to-string.mjs b/bench/snippets/base64-buffer-to-string.mjs new file mode 100644 index 000000000..a62b76379 --- /dev/null +++ b/bench/snippets/base64-buffer-to-string.mjs @@ -0,0 +1,14 @@ +import { bench, run } from "./runner.mjs"; +import { Buffer } from "node:buffer"; + +const bigBuffer = Buffer.from("hello world".repeat(10000)); +const converted = bigBuffer.toString("base64"); +bench("Buffer.toString('base64')", () => { + return bigBuffer.toString("base64"); +}); + +// bench("Buffer.from(str, 'base64')", () => { +// return Buffer.from(converted, "base64"); +// }); + +await run(); diff --git a/docs/project/licensing.md b/docs/project/licensing.md index ea49acb1d..ac7fef774 100644 --- a/docs/project/licensing.md +++ b/docs/project/licensing.md @@ -85,6 +85,11 @@ Bun statically links these libraries: --- +- [`libbase64`](https://github.com/aklomp/base64/blob/master/LICENSE) +- BSD 2-Clause + +--- + - A fork of [`uWebsockets`](https://github.com/jarred-sumner/uwebsockets) - Apache 2.0 licensed diff --git a/src/base64/base64.zig b/src/base64/base64.zig index bddc44564..8768b9c7b 100644 --- a/src/base64/base64.zig +++ b/src/base64/base64.zig @@ -5,6 +5,22 @@ pub const DecodeResult = struct { fail: bool = false, }; +pub const LibBase64 = struct { + pub const State = extern struct { + eof: c_int, + bytes: c_int, + flags: c_int, + carry: u8, + }; + pub extern fn base64_encode(src: [*]const u8, srclen: usize, out: [*]u8, outlen: *usize, flags: c_int) void; + pub extern fn base64_stream_encode_init(state: *State, flags: c_int) void; + pub extern fn base64_stream_encode(state: *State, src: [*]const u8, srclen: usize, out: [*]u8, outlen: *usize) void; + pub extern fn base64_stream_encode_final(state: *State, out: [*]u8, outlen: *usize) void; + pub extern fn base64_decode(src: [*]const u8, srclen: usize, out: [*]u8, outlen: *usize, flags: c_int) c_int; + pub extern fn base64_stream_decode_init(state: *State, flags: c_int) void; + pub extern fn base64_stream_decode(state: *State, src: [*]const u8, srclen: usize, out: [*]u8, outlen: *usize) c_int; +}; + const mixed_decoder = brk: { var decoder = zig_base64.standard.decoderWithIgnore("\xff \t\r\n" ++ [_]u8{ std.ascii.control_code.vt, @@ -30,7 +46,9 @@ pub fn decode(destination: []u8, source: []const u8) DecodeResult { } pub fn encode(destination: []u8, source: []const u8) usize { - return zig_base64.standard.Encoder.encode(destination, source).len; + var outlen: usize = destination.len; + LibBase64.base64_encode(source.ptr, source.len, destination.ptr, &outlen, 0); + return outlen; } pub fn decodeLenUpperBound(len: usize) usize { diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index b01eca8e0..96d04636e 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -524,8 +524,8 @@ pub const Encoding = enum(u8) { switch (encoding) { .base64 => { var base64: [std.base64.standard.Encoder.calcSize(size)]u8 = undefined; - const result = JSC.ZigString.init(std.base64.standard.Encoder.encode(&base64, input)).toValueGC(globalThis); - return result; + const len = bun.base64.encode(&base64, input); + return JSC.ZigString.init(base64[0..len]).toValueGC(globalThis); }, .base64url => { var buf: [std.base64.url_safe.Encoder.calcSize(size) + "data:;base64,".len]u8 = undefined; diff --git a/src/deps/base64 b/src/deps/base64 new file mode 160000 index 000000000..e77bd70bd --- /dev/null +++ b/src/deps/base64 @@ -0,0 +1 @@ +Subproject commit e77bd70bdd860c52c561568cffb251d88bba064c -- cgit v1.2.3 From 034577c9dad2486324e3bd65907794dd9a3ee217 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari Date: Mon, 3 Jul 2023 18:08:49 -0300 Subject: [fix] patch fs.watch sym link (#3481) * oopsie * add tests --------- Co-authored-by: Jarred Sumner --- src/bun.js/node/node_fs_watcher.zig | 2 +- test/js/node/watch/fs.watch.test.js | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index b1f4ec8a9..d0af350c0 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -802,7 +802,7 @@ pub const FSWatcher = struct { result.fd = file.handle; const _stat = try file.stat(); - result.is_file = _stat.kind == .directory; + result.is_file = _stat.kind != .directory; }, .directory => { const dir = (try std.fs.openIterableDirAbsoluteZ(absolute_path_z, .{ diff --git a/test/js/node/watch/fs.watch.test.js b/test/js/node/watch/fs.watch.test.js index 33d05df29..faf6a8546 100644 --- a/test/js/node/watch/fs.watch.test.js +++ b/test/js/node/watch/fs.watch.test.js @@ -19,6 +19,8 @@ const testDir = tempDirWithFiles("watch", { "url.txt": "hello", "close.txt": "hello", "close-close.txt": "hello", + "sym-sync.txt": "hello", + "sym.txt": "hello", [encodingFileName]: "hello", }); @@ -341,6 +343,36 @@ describe("fs.watch", () => { watcher.once("close", () => reject()); }); }); + + test("should work with symlink", async () => { + const filepath = path.join(testDir, "sym-symlink2.txt"); + await fs.promises.symlink(path.join(testDir, "sym-sync.txt"), filepath); + + const interval = repeat(() => { + fs.writeFileSync(filepath, "hello"); + }); + + const promise = new Promise((resolve, reject) => { + let timeout = null; + const watcher = fs.watch(filepath, event => { + clearTimeout(timeout); + clearInterval(interval); + try { + resolve(event); + } catch (e) { + reject(e); + } finally { + watcher.close(); + } + }); + setTimeout(() => { + clearInterval(interval); + watcher?.close(); + reject("timeout"); + }, 3000); + }); + expect(promise).resolves.toBe("change"); + }); }); describe("fs.promises.watch", () => { @@ -464,4 +496,27 @@ describe("fs.promises.watch", () => { } })(); }); + + test("should work with symlink", async () => { + const filepath = path.join(testDir, "sym-symlink.txt"); + await fs.promises.symlink(path.join(testDir, "sym.txt"), filepath); + + const watcher = fs.promises.watch(filepath); + const interval = repeat(() => { + fs.writeFileSync(filepath, "hello"); + }); + + const promise = (async () => { + try { + for await (const event of watcher) { + return event.eventType; + } + } catch (e) { + expect("unreacheable").toBe(false); + } finally { + clearInterval(interval); + } + })(); + expect(promise).resolves.toBe("change"); + }); }); -- cgit v1.2.3 From 0ecdbf4793f19b3244a3e099bd47e3e81e993811 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 7 Jul 2023 00:33:43 -0700 Subject: [node:fs] `read`, `write` - support large numbers and BigInt (#3556) Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/bindings.zig | 5 ++-- src/bun.js/node/node_fs.zig | 50 +++++++++++++++++++------------------- test/js/node/fs/fs.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 27 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 07882d857..c8d0515e9 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3445,6 +3445,7 @@ pub const JSValue = enum(JSValueReprInt) { c_int => @intCast(c_int, toInt32(this)), ?AnyPromise => asAnyPromise(this), u52 => @truncate(u52, @intCast(u64, @max(this.toInt64(), 0))), + i52 => @truncate(i52, @intCast(i52, this.toInt64())), u64 => toUInt64NoTruncate(this), u8 => @truncate(u8, toU32(this)), i16 => @truncate(i16, toInt32(this)), @@ -4620,11 +4621,11 @@ pub const JSValue = enum(JSValueReprInt) { } pub inline fn toU16(this: JSValue) u16 { - return @truncate(u16, this.toU32()); + return @truncate(u16, @max(this.toInt32(), 0)); } pub inline fn toU32(this: JSValue) u32 { - return @intCast(u32, @max(this.toInt32(), 0)); + return @intCast(u32, @min(@max(this.toInt64(), 0), std.math.maxInt(u32))); } /// This function supports: diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index fa33a575b..74a41c5bd 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -35,7 +35,7 @@ const Mode = JSC.Node.Mode; const uid_t = std.os.uid_t; const gid_t = std.os.gid_t; /// u63 to allow one null bit -const ReadPosition = u63; +const ReadPosition = i64; const Stats = JSC.Node.Stats; const Dirent = JSC.Node.Dirent; @@ -1524,7 +1524,7 @@ pub const Arguments = struct { // fs.write(fd, string[, position[, encoding]], callback) .string => { if (current.isNumber()) { - args.position = current.toU32(); + args.position = current.to(i52); arguments.eat(); current = arguments.next() orelse break :parse; } @@ -1540,18 +1540,18 @@ pub const Arguments = struct { break :parse; } - if (!current.isNumber()) break :parse; - args.offset = current.toU32(); + if (!(current.isNumber() or current.isBigInt())) break :parse; + args.offset = current.to(u52); arguments.eat(); current = arguments.next() orelse break :parse; - if (!current.isNumber()) break :parse; - args.length = current.toU32(); + if (!(current.isNumber() or current.isBigInt())) break :parse; + args.length = current.to(u52); arguments.eat(); current = arguments.next() orelse break :parse; - if (!current.isNumber()) break :parse; - args.position = current.toU32(); + if (!(current.isNumber() or current.isBigInt())) break :parse; + args.position = current.to(i52); arguments.eat(); }, } @@ -1631,8 +1631,8 @@ pub const Arguments = struct { if (arguments.next()) |current| { arguments.eat(); - if (current.isNumber()) { - args.offset = current.toU32(); + if (current.isNumber() or current.isBigInt()) { + args.offset = current.to(u52); if (arguments.remaining.len < 2) { JSC.throwInvalidArguments( @@ -1644,8 +1644,8 @@ pub const Arguments = struct { return null; } - - args.length = arguments.remaining[0].toU32(); + if (arguments.remaining[0].isNumber() or arguments.remaining[0].isBigInt()) + args.length = arguments.remaining[0].to(u52); if (args.length == 0) { JSC.throwInvalidArguments( @@ -1658,26 +1658,26 @@ pub const Arguments = struct { return null; } - const position: i32 = if (arguments.remaining[1].isNumber()) - arguments.remaining[1].toInt32() - else - -1; + if (arguments.remaining[1].isNumber() or arguments.remaining[1].isBigInt()) + args.position = @intCast(ReadPosition, arguments.remaining[1].to(i52)); - args.position = if (position > -1) @intCast(ReadPosition, position) else null; arguments.remaining = arguments.remaining[2..]; } else if (current.isObject()) { - if (current.getIfPropertyExists(ctx.ptr(), "offset")) |num| { - args.offset = num.toU32(); + if (current.getTruthy(ctx.ptr(), "offset")) |num| { + if (num.isNumber() or num.isBigInt()) { + args.offset = num.to(u52); + } } - if (current.getIfPropertyExists(ctx.ptr(), "length")) |num| { - args.length = num.toU32(); + if (current.getTruthy(ctx.ptr(), "length")) |num| { + if (num.isNumber() or num.isBigInt()) { + args.length = num.to(u52); + } } - if (current.getIfPropertyExists(ctx.ptr(), "position")) |num| { - const position: i32 = if (num.isEmptyOrUndefinedOrNull()) -1 else num.coerce(i32, ctx); - if (position > -1) { - args.position = @intCast(ReadPosition, position); + if (current.getTruthy(ctx.ptr(), "position")) |num| { + if (num.isNumber() or num.isBigInt()) { + args.position = num.to(i52); } } } diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 272522fc0..3b1688c15 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -278,6 +278,41 @@ it("readdirSync throws when given a file path with trailing slash", () => { describe("readSync", () => { const firstFourBytes = new Uint32Array(new TextEncoder().encode("File").buffer)[0]; + + it("works on large files", () => { + const dest = join(tmpdir(), "readSync-large-file.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 4_900_000_000); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 4_900_000_000); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + + it("works with bigint on read", () => { + const dest = join(tmpdir(), "readSync-large-file-bigint.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 400); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 400n as any); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + it("works with a position set to 0", () => { const fd = openSync(import.meta.dir + "/readFileSync.txt", "r"); const four = new Uint8Array(4); @@ -367,6 +402,23 @@ it("preadv", () => { }); describe("writeSync", () => { + it("works with bigint", () => { + const dest = join(tmpdir(), "writeSync-large-file-bigint.txt"); + rmSync(dest, { force: true }); + + const writefd = openSync(dest, "w"); + writeSync(writefd, Buffer.from([0x10]), 0, 1, 400n as any); + closeSync(writefd); + + const fd = openSync(dest, "r"); + const out = Buffer.alloc(1); + const bytes = readSync(fd, out, 0, 1, 400 as any); + expect(bytes).toBe(1); + expect(out[0]).toBe(0x10); + closeSync(fd); + rmSync(dest, { force: true }); + }); + it("works with a position set to 0", () => { const fd = openSync(import.meta.dir + "/writeFileSync.txt", "w+"); const four = new Uint8Array(4); -- cgit v1.2.3 From aa8b832ef61ada31176d248e716074ff22bb9dee Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 8 Jul 2023 14:26:19 -0700 Subject: Implement `process.on("beforeExit", cb)` and `process.on("exit", cb)` (#3576) * Support `process.on('beforeExit')` and `process.on('exit')` * [bun:sqlite] Always call sqlite3_close on exit * Update process.test.js --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/Process.cpp | 189 +++++++++++++++++---- src/bun.js/bindings/ZigGlobalObject.h | 2 + src/bun.js/bindings/sqlite/JSSQLStatement.cpp | 86 +++++++--- src/bun.js/bindings/sqlite/JSSQLStatement.h | 15 -- src/bun.js/javascript.zig | 50 ++++++ src/bun.js/node/types.zig | 4 +- src/bun_js.zig | 14 +- test/js/node/process/process-exit-fixture.js | 16 ++ test/js/node/process/process-exitCode-fixture.js | 7 + test/js/node/process/process-exitCode-with-exit.js | 8 + .../node/process/process-onBeforeExit-fixture.js | 7 + .../node/process/process-onBeforeExit-keepAlive.js | 18 ++ test/js/node/process/process.test.js | 64 ++++++- 13 files changed, 402 insertions(+), 78 deletions(-) create mode 100644 test/js/node/process/process-exit-fixture.js create mode 100644 test/js/node/process/process-exitCode-fixture.js create mode 100644 test/js/node/process/process-exitCode-with-exit.js create mode 100644 test/js/node/process/process-onBeforeExit-fixture.js create mode 100644 test/js/node/process/process-onBeforeExit-keepAlive.js (limited to 'src/bun.js/node') diff --git a/src/bun.js/bindings/Process.cpp b/src/bun.js/bindings/Process.cpp index 6320deaf1..1d6b5d33a 100644 --- a/src/bun.js/bindings/Process.cpp +++ b/src/bun.js/bindings/Process.cpp @@ -42,6 +42,35 @@ static JSC_DECLARE_CUSTOM_GETTER(Process_getPID); static JSC_DECLARE_CUSTOM_GETTER(Process_getPPID); static JSC_DECLARE_HOST_FUNCTION(Process_functionCwd); +static bool processIsExiting = false; + +extern "C" uint8_t Bun__getExitCode(void*); +extern "C" uint8_t Bun__setExitCode(void*, uint8_t); +extern "C" void* Bun__getVM(); +extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); + +static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* process, int exitCode) +{ + + if (processIsExiting) + return; + processIsExiting = true; + auto& emitter = process->wrapped(); + auto& vm = globalObject->vm(); + + if (vm.hasTerminationRequest() || vm.hasExceptionsAfterHandlingTraps()) + return; + + auto event = Identifier::fromString(vm, "exit"_s); + if (!emitter.hasEventListeners(event)) { + return; + } + process->putDirect(vm, Identifier::fromString(vm, "_exiting"_s), jsBoolean(true), 0); + + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + emitter.emit(event, arguments); +} static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int fd) { @@ -324,6 +353,29 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, extern "C" uint64_t Bun__readOriginTimer(void*); extern "C" double Bun__readOriginTimerStart(void*); +// https://github.com/nodejs/node/blob/1936160c31afc9780e4365de033789f39b7cbc0c/src/api/hooks.cc#L49 +extern "C" void Process__dispatchOnBeforeExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast(globalObject->processObject()); + MarkedArgumentBuffer arguments; + arguments.append(jsNumber(exitCode)); + process->wrapped().emit(Identifier::fromString(globalObject->vm(), "beforeExit"_s), arguments); +} + +extern "C" void Process__dispatchOnExit(Zig::GlobalObject* globalObject, uint8_t exitCode) +{ + if (!globalObject->hasProcessObject()) { + return; + } + + auto* process = jsCast(globalObject->processObject()); + dispatchExitInternal(globalObject, process, exitCode); +} + JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -336,14 +388,38 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUptime, JSC_DEFINE_HOST_FUNCTION(Process_functionExit, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - if (callFrame->argumentCount() == 0) { - // TODO: exitCode - Bun__Process__exit(globalObject, 0); + auto throwScope = DECLARE_THROW_SCOPE(globalObject->vm()); + uint8_t exitCode = 0; + JSValue arg0 = callFrame->argument(0); + if (arg0.isNumber()) { + if (!arg0.isInt32()) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int extiCode32 = arg0.toInt32(globalObject); + RETURN_IF_EXCEPTION(throwScope, JSC::JSValue::encode(JSC::JSValue {})); + + if (extiCode32 < 0 || extiCode32 > 127) { + throwRangeError(globalObject, throwScope, "The \"code\" argument must be an integer between 0 and 127"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + exitCode = static_cast(extiCode32); + } else if (!arg0.isUndefinedOrNull()) { + throwTypeError(globalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); } else { - Bun__Process__exit(globalObject, callFrame->argument(0).toInt32(globalObject)); + exitCode = Bun__getExitCode(Bun__getVM()); + } + + auto* zigGlobal = jsDynamicCast(globalObject); + if (UNLIKELY(!zigGlobal)) { + zigGlobal = Bun__getDefaultGlobal(); } - return JSC::JSValue::encode(JSC::jsUndefined()); + Process__dispatchOnExit(zigGlobal, exitCode); + Bun__Process__exit(zigGlobal, exitCode); } extern "C" uint64_t Bun__readOriginTimer(void*); @@ -391,18 +467,15 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionHRTime, array->setIndexQuickly(vm, 1, JSC::jsNumber(nanoseconds)); return JSC::JSValue::encode(JSC::JSValue(array)); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionHRTimeBigInt); -static JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, +JSC_DEFINE_HOST_FUNCTION(Process_functionHRTimeBigInt, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); return JSC::JSValue::encode(JSValue(JSC::JSBigInt::createFrom(globalObject, Bun__readOriginTimer(globalObject->bunVM())))); } -static JSC_DECLARE_HOST_FUNCTION(Process_functionChdir); - -static JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, +JSC_DEFINE_HOST_FUNCTION(Process_functionChdir, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); @@ -611,6 +684,46 @@ JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecArgvGetter, (JSC::JSGlobalObject * glob return ret; } +JSC_DEFINE_CUSTOM_GETTER(Process__getExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) +{ + Process* process = jsDynamicCast(JSValue::decode(thisValue)); + if (!process) { + return JSValue::encode(jsUndefined()); + } + + return JSValue::encode(jsNumber(Bun__getExitCode(jsCast(process->globalObject())->bunVM()))); +} +JSC_DEFINE_CUSTOM_SETTER(Process__setExitCode, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName)) +{ + Process* process = jsDynamicCast(JSValue::decode(thisValue)); + if (!process) { + return false; + } + + auto throwScope = DECLARE_THROW_SCOPE(process->vm()); + JSValue exitCode = JSValue::decode(value); + if (!exitCode.isNumber()) { + throwTypeError(lexicalGlobalObject, throwScope, "exitCode must be a number"_s); + return false; + } + + if (!exitCode.isInt32()) { + throwRangeError(lexicalGlobalObject, throwScope, "The \"code\" argument must be an integer"_s); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + int exitCodeInt = exitCode.toInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, false); + if (exitCodeInt < 0 || exitCodeInt > 127) { + throwRangeError(lexicalGlobalObject, throwScope, "exitCode must be between 0 and 127"_s); + return false; + } + + void* ptr = jsCast(process->globalObject())->bunVM(); + Bun__setExitCode(ptr, static_cast(exitCodeInt)); + return true; +} + JSC_DEFINE_CUSTOM_GETTER(Process_lazyExecPathGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) { JSC::JSObject* thisObject = JSValue::decode(thisValue).getObject(); @@ -677,39 +790,42 @@ void Process::finishCreation(JSC::VM& vm) vm, clientData->builtinNames().versionsPublicName(), JSC::CustomGetterSetter::create(vm, Process_getVersionsLazy, Process_setVersionsLazy), 0); // this should be transpiled out, but just incase - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "browser"_s), - JSC::JSValue(false)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "browser"_s), + JSC::JSValue(false), PropertyAttribute::DontEnum | 0); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "exitCode"_s), - JSC::JSValue(JSC::jsNumber(0))); + this->putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "exitCode"_s), + JSC::CustomGetterSetter::create(vm, + Process__getExitCode, + Process__setExitCode), + 0); - this->putDirect(this->vm(), clientData->builtinNames().versionPublicName(), - JSC::jsString(this->vm(), makeString("v", REPORTED_NODE_VERSION))); + this->putDirect(vm, clientData->builtinNames().versionPublicName(), + JSC::jsString(vm, makeString("v", REPORTED_NODE_VERSION))); // this gives some way of identifying at runtime whether the SSR is happening in node or not. // this should probably be renamed to what the name of the bundler is, instead of "notNodeJS" // but it must be something that won't evaluate to truthy in Node.js - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "isBun"_s), JSC::JSValue(true)); + this->putDirect(vm, JSC::Identifier::fromString(vm, "isBun"_s), JSC::JSValue(true)); #if defined(__APPLE__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("darwin"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("darwin"))); #else - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"_s), - JSC::jsString(this->vm(), makeAtomString("linux"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "platform"_s), + JSC::jsString(vm, makeAtomString("linux"))); #endif #if defined(__x86_64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x64"))); #elif defined(__i386__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("x86"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("x86"))); #elif defined(__arm__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm"))); #elif defined(__aarch64__) - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "arch"_s), - JSC::jsString(this->vm(), makeAtomString("arm64"))); + this->putDirect(vm, JSC::Identifier::fromString(vm, "arch"_s), + JSC::jsString(vm, makeAtomString("arm64"))); #endif JSC::JSFunction* hrtime = JSC::JSFunction::create(vm, globalObject, 0, @@ -719,7 +835,7 @@ void Process::finishCreation(JSC::VM& vm) MAKE_STATIC_STRING_IMPL("bigint"), Process_functionHRTimeBigInt, ImplementationVisibility::Public); hrtime->putDirect(vm, JSC::Identifier::fromString(vm, "bigint"_s), hrtimeBigInt); - this->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "hrtime"_s), hrtime); + this->putDirect(vm, JSC::Identifier::fromString(vm, "hrtime"_s), hrtime); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "release"_s)), JSC::CustomGetterSetter::create(vm, Process_getterRelease, Process_setterRelease), 0); @@ -733,7 +849,10 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "stdin"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyStdinGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "abort"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), + 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); + + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "abort"_s), 0, Process_functionAbort, ImplementationVisibility::Public, NoIntrinsic, 0); this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "argv0"_s)), @@ -745,13 +864,13 @@ void Process::finishCreation(JSC::VM& vm) this->putDirectCustomAccessor(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "execArgv"_s)), JSC::CustomGetterSetter::create(vm, Process_lazyExecArgvGetter, Process_defaultSetter), 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "uptime"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "uptime"_s), 0, Process_functionUptime, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "umask"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "umask"_s), 1, Process_functionUmask, ImplementationVisibility::Public, NoIntrinsic, 0); - this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "binding"_s), + this->putDirectBuiltinFunction(vm, globalObject, JSC::Identifier::fromString(vm, "binding"_s), processObjectInternalsBindingCodeGenerator(vm), 0); @@ -788,7 +907,7 @@ void Process::finishCreation(JSC::VM& vm) config->putDirect(vm, JSC::Identifier::fromString(vm, "variables"_s), variables, 0); this->putDirect(vm, JSC::Identifier::fromString(vm, "config"_s), config, 0); - this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(this->vm(), "emitWarning"_s), + this->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "emitWarning"_s), 1, Process_emitWarning, ImplementationVisibility::Public, NoIntrinsic, 0); JSC::JSFunction* requireDotMainFunction = JSFunction::create( diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index da6ba92a0..f44212da1 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -270,6 +270,8 @@ public: JSWeakMap* vmModuleContextMap() { return m_vmModuleContextMap.getInitializedOnMainThread(this); } + bool hasProcessObject() const { return m_processObject.isInitialized(); } + JSC::JSObject* processObject() { return m_processObject.getInitializedOnMainThread(this); diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index a6855fd19..61ac91ba7 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -107,6 +107,50 @@ static JSC_DECLARE_HOST_FUNCTION(jsSQLStatementDeserialize); return JSValue::encode(jsUndefined()); \ } +class VersionSqlite3 { +public: + explicit VersionSqlite3(sqlite3* db) + : db(db) + , version(0) + { + } + sqlite3* db; + std::atomic version; +}; + +class SQLiteSingleton { +public: + Vector databases; + Vector> schema_versions; +}; + +static SQLiteSingleton* _instance = nullptr; + +static Vector& databases() +{ + if (!_instance) { + _instance = new SQLiteSingleton(); + _instance->databases = Vector(); + _instance->databases.reserveInitialCapacity(4); + _instance->schema_versions = Vector>(); + } + + return _instance->databases; +} + +extern "C" void Bun__closeAllSQLiteDatabasesForTermination() +{ + if (!_instance) { + return; + } + auto& dbs = _instance->databases; + + for (auto& db : dbs) { + if (db->db) + sqlite3_close_v2(db->db); + } +} + namespace WebCore { using namespace JSC; @@ -272,10 +316,6 @@ void JSSQLStatement::destroy(JSC::JSCell* cell) void JSSQLStatementConstructor::destroy(JSC::JSCell* cell) { - JSSQLStatementConstructor* thisObject = static_cast(cell); - for (auto version_db : thisObject->databases) { - delete version_db; - } } static inline bool rebindValue(JSC::JSGlobalObject* lexicalGlobalObject, sqlite3_stmt* stmt, int i, JSC::JSValue value, JSC::ThrowScope& scope, bool clone) @@ -547,8 +587,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexic return JSValue::encode(JSC::jsUndefined()); } - auto count = thisObject->databases.size(); - thisObject->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -565,12 +605,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexical } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -606,7 +646,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje } int32_t dbIndex = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (UNLIKELY(dbIndex < 0 || dbIndex >= thisObject->databases.size())) { + if (UNLIKELY(dbIndex < 0 || dbIndex >= databases().size())) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } @@ -620,7 +660,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje auto extensionString = extension.toWTFString(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - sqlite3* db = thisObject->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Can't do this on a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -661,11 +701,11 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l } int32_t handle = callFrame->argument(0).toInt32(lexicalGlobalObject); - if (thisObject->databases.size() < handle) { + if (databases().size() < handle) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -724,7 +764,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l rc = sqlite3_step(statement); if (!sqlite3_stmt_readonly(statement)) { - thisObject->databases[handle]->version++; + databases()[handle]->version++; } while (rc == SQLITE_ROW) { @@ -765,12 +805,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementIsInTransactionFunction, (JSC::JSGlobalOb int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (UNLIKELY(!db)) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Database has closed"_s)); @@ -803,12 +843,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO } int32_t handle = dbNumber.toInt32(lexicalGlobalObject); - if (handle < 0 || handle > thisObject->databases.size()) { + if (handle < 0 || handle > databases().size()) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(JSC::jsUndefined()); } - sqlite3* db = thisObject->databases[handle]->db; + sqlite3* db = databases()[handle]->db; if (!db) { throwException(lexicalGlobalObject, scope, createRangeError(lexicalGlobalObject, "Cannot use a closed database"_s)); return JSValue::encode(JSC::jsUndefined()); @@ -848,7 +888,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementPrepareStatementFunction, (JSC::JSGlobalO auto* structure = JSSQLStatement::createStructure(vm, lexicalGlobalObject, lexicalGlobalObject->objectPrototype()); // auto* structure = JSSQLStatement::createStructure(vm, globalObject(), thisObject->getDirect(vm, vm.propertyNames->prototype)); JSSQLStatement* sqlStatement = JSSQLStatement::create( - structure, reinterpret_cast(lexicalGlobalObject), statement, thisObject->databases[handle]); + structure, reinterpret_cast(lexicalGlobalObject), statement, databases()[handle]); if (bindings.isObject()) { auto* castedThis = sqlStatement; DO_REBIND(bindings) @@ -924,8 +964,8 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction, (JSC::JSGlobalObje status = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); assert(status == SQLITE_OK); - auto count = constructor->databases.size(); - constructor->databases.append(new VersionSqlite3(db)); + auto count = databases().size(); + databases().append(new VersionSqlite3(db)); RELEASE_AND_RETURN(scope, JSValue::encode(jsNumber(count))); } @@ -956,12 +996,12 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj int dbIndex = dbNumber.toInt32(lexicalGlobalObject); - if (dbIndex < 0 || dbIndex >= constructor->databases.size()) { + if (dbIndex < 0 || dbIndex >= databases().size()) { throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Invalid database handle"_s)); return JSValue::encode(jsUndefined()); } - sqlite3* db = constructor->databases[dbIndex]->db; + sqlite3* db = databases()[dbIndex]->db; // no-op if already closed if (!db) { return JSValue::encode(jsUndefined()); @@ -973,7 +1013,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementCloseStatementFunction, (JSC::JSGlobalObj return JSValue::encode(jsUndefined()); } - constructor->databases[dbIndex]->db = nullptr; + databases()[dbIndex]->db = nullptr; return JSValue::encode(jsUndefined()); } diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.h b/src/bun.js/bindings/sqlite/JSSQLStatement.h index e63b99fbb..8566fcdd9 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.h +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.h @@ -47,17 +47,6 @@ namespace WebCore { -class VersionSqlite3 { -public: - explicit VersionSqlite3(sqlite3* db) - : db(db) - , version(0) - { - } - sqlite3* db; - std::atomic version; -}; - class JSSQLStatementConstructor final : public JSC::JSFunction { public: using Base = JSC::JSFunction; @@ -82,13 +71,9 @@ public: return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - Vector databases; - Vector> schema_versions; - private: JSSQLStatementConstructor(JSC::VM& vm, NativeExecutable* native, JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, native, globalObject, structure) - , databases() { } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index b696c6cf2..7d2435823 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -334,6 +334,33 @@ pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSG jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; } +pub const ExitHandler = struct { + exit_code: u8 = 0, + + pub export fn Bun__getExitCode(vm: *VirtualMachine) u8 { + return vm.exit_handler.exit_code; + } + + pub export fn Bun__setExitCode(vm: *VirtualMachine, code: u8) void { + vm.exit_handler.exit_code = code; + } + + extern fn Process__dispatchOnBeforeExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Process__dispatchOnExit(*JSC.JSGlobalObject, code: u8) void; + extern fn Bun__closeAllSQLiteDatabasesForTermination() void; + + pub fn dispatchOnExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnExit(vm.global, this.exit_code); + Bun__closeAllSQLiteDatabasesForTermination(); + } + + pub fn dispatchOnBeforeExit(this: *ExitHandler) void { + var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); + Process__dispatchOnBeforeExit(vm.global, this.exit_code); + } +}; + /// TODO: rename this to ScriptExecutionContext /// This is the shared global state for a single JS instance execution /// Today, Bun is one VM per thread, so the name "VirtualMachine" sort of makes sense @@ -376,6 +403,7 @@ pub const VirtualMachine = struct { plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, last_reported_error_for_dedupe: JSValue = .zero, + exit_handler: ExitHandler = .{}, /// Do not access this field directly /// It exists in the VirtualMachine struct so that @@ -620,7 +648,29 @@ pub const VirtualMachine = struct { loop.run(); } + pub fn onBeforeExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnBeforeExit(); + var dispatch = false; + while (true) { + while (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) : (dispatch = true) { + this.tick(); + this.eventLoop().autoTickActive(); + } + + if (dispatch) { + this.exit_handler.dispatchOnBeforeExit(); + dispatch = false; + + if (this.eventLoop().tasks.count > 0 or this.active_tasks > 0 or this.uws_event_loop.?.active > 0) continue; + } + + break; + } + } + pub fn onExit(this: *VirtualMachine) void { + this.exit_handler.dispatchOnExit(); + var rare_data = this.rare_data orelse return; var hook = rare_data.cleanup_hook orelse return; hook.execute(); diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 96d04636e..553b292d6 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -2202,7 +2202,9 @@ pub const Process = struct { } } - pub fn exit(_: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + pub fn exit(globalObject: *JSC.JSGlobalObject, code: i32) callconv(.C) void { + globalObject.bunVM().onExit(); + std.os.exit(@truncate(u8, @intCast(u32, @max(code, 0)))); } diff --git a/src/bun_js.zig b/src/bun_js.zig index 63ffe0611..72b7f8de9 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -248,6 +248,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -279,6 +281,8 @@ pub const Run = struct { vm.eventLoop().tick(); vm.eventLoop().tickPossiblyForever(); } else { + vm.exit_handler.exit_code = 1; + vm.onExit(); Global.exit(1); } } @@ -315,6 +319,8 @@ pub const Run = struct { vm.eventLoop().autoTickActive(); } + vm.onBeforeExit(); + if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { prev_promise = this.vm.pending_internal_promise; vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); @@ -332,6 +338,8 @@ pub const Run = struct { vm.tick(); vm.eventLoop().autoTickActive(); } + + vm.onBeforeExit(); } if (vm.log.msgs.items.len > 0) { @@ -347,10 +355,14 @@ pub const Run = struct { vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; vm.global.handleRejectedPromises(); + if (this.any_unhandled and this.vm.exit_handler.exit_code == 0) { + this.vm.exit_handler.exit_code = 1; + } + const exit_code = this.vm.exit_handler.exit_code; vm.onExit(); if (!JSC.is_bindgen) JSC.napi.fixDeadCodeElimination(); - Global.exit(@intFromBool(this.any_unhandled)); + Global.exit(exit_code); } }; diff --git a/test/js/node/process/process-exit-fixture.js b/test/js/node/process/process-exit-fixture.js new file mode 100644 index 000000000..c5a492285 --- /dev/null +++ b/test/js/node/process/process-exit-fixture.js @@ -0,0 +1,16 @@ +process.on("beforeExit", () => { + throw new Error("process.on('beforeExit') called"); +}); + +if (process._exiting) { + throw new Error("process._exiting should be undefined"); +} + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("PASS"); +}); + +process.exit(0); diff --git a/test/js/node/process/process-exitCode-fixture.js b/test/js/node/process/process-exitCode-fixture.js new file mode 100644 index 000000000..2d5182d93 --- /dev/null +++ b/test/js/node/process/process-exitCode-fixture.js @@ -0,0 +1,7 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); diff --git a/test/js/node/process/process-exitCode-with-exit.js b/test/js/node/process/process-exitCode-with-exit.js new file mode 100644 index 000000000..610975bc2 --- /dev/null +++ b/test/js/node/process/process-exitCode-with-exit.js @@ -0,0 +1,8 @@ +process.exitCode = Number(process.argv.at(-1)); +process.on("exit", code => { + if (code !== process.exitCode) { + throw new Error("process.exitCode should be " + process.exitCode); + } + console.log("PASS"); +}); +process.exit(); diff --git a/test/js/node/process/process-onBeforeExit-fixture.js b/test/js/node/process/process-onBeforeExit-fixture.js new file mode 100644 index 000000000..8cbdcebf0 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-fixture.js @@ -0,0 +1,7 @@ +process.on("beforeExit", () => { + console.log("beforeExit"); +}); + +process.on("exit", () => { + console.log("exit"); +}); diff --git a/test/js/node/process/process-onBeforeExit-keepAlive.js b/test/js/node/process/process-onBeforeExit-keepAlive.js new file mode 100644 index 000000000..45b20b763 --- /dev/null +++ b/test/js/node/process/process-onBeforeExit-keepAlive.js @@ -0,0 +1,18 @@ +let counter = 0; +process.on("beforeExit", () => { + if (process._exiting) { + throw new Error("process._exiting should be undefined"); + } + + console.log("beforeExit:", counter); + if (!counter++) { + setTimeout(() => {}, 1); + } +}); + +process.on("exit", () => { + if (!process._exiting) { + throw new Error("process.on('exit') called with process._exiting false"); + } + console.log("exit:", counter); +}); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 61ac3839c..c4701f664 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1,8 +1,8 @@ -import { resolveSync, which } from "bun"; +import { resolveSync, spawnSync, which } from "bun"; import { describe, expect, it } from "bun:test"; import { existsSync, readFileSync, realpathSync } from "fs"; -import { bunExe } from "harness"; -import { basename, resolve } from "path"; +import { bunEnv, bunExe } from "harness"; +import { basename, join, resolve } from "path"; it("process", () => { // this property isn't implemented yet but it should at least return a string @@ -233,3 +233,61 @@ it("process.argv in testing", () => { // assert we aren't creating a new process.argv each call expect(process.argv).toBe(process.argv); }); + +describe("process.exitCode", () => { + it("validates int", () => { + expect(() => (process.exitCode = "potato")).toThrow("exitCode must be a number"); + expect(() => (process.exitCode = 1.2)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = NaN)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -Infinity)).toThrow('The "code" argument must be an integer'); + expect(() => (process.exitCode = -1)).toThrow("exitCode must be between 0 and 127"); + }); + + it("works with implicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-with-exit.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exitCode-fixture.js"), "42"], + env: bunEnv, + }); + expect(exitCode).toBe(42); + expect(stdout.toString().trim()).toBe("PASS"); + }); +}); + +it("process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-exit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("PASS"); +}); + +describe("process.onBeforeExit", () => { + it("emitted", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-fixture.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit\nexit"); + }); + + it("works with explicit process.exit", () => { + const { exitCode, stdout } = spawnSync({ + cmd: [bunExe(), join(import.meta.dir, "process-onBeforeExit-keepAlive.js")], + env: bunEnv, + }); + expect(exitCode).toBe(0); + expect(stdout.toString().trim()).toBe("beforeExit: 0\nbeforeExit: 1\nexit: 2"); + }); +}); -- cgit v1.2.3 From 963d4311e614ac197427104b9cf265bbe2a890af Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 9 Jul 2023 22:36:24 -0700 Subject: Fixes #3530 (#3587) * Fixes #3530 * Handle OOM * Add test --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/BunString.cpp | 23 +++++++++++++++++ src/bun.js/node/types.zig | 30 +++++++++++++++++----- src/bun.js/webcore/encoding.zig | 36 ++++++++++++--------------- src/string.zig | 26 +++++++++++++++++++ test/js/node/crypto/node-crypto.test.js | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 26 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 4c8ff384e..21541d711 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -169,6 +169,29 @@ extern "C" JSC::EncodedJSValue BunString__toJS(JSC::JSGlobalObject* globalObject return JSValue::encode(Bun::toJS(globalObject, *bunString)); } +extern "C" BunString BunString__fromUTF16Unitialized(size_t length) +{ + unsigned utf16Length = length; + UChar* ptr; + auto impl = WTF::StringImpl::createUninitialized(utf16Length, ptr); + if (UNLIKELY(!ptr)) + return { BunStringTag::Dead }; + + impl->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; +} + +extern "C" BunString BunString__fromLatin1Unitialized(size_t length) +{ + unsigned latin1Length = length; + LChar* ptr; + auto impl = WTF::StringImpl::createUninitialized(latin1Length, ptr); + if (UNLIKELY(!ptr)) + return { BunStringTag::Dead }; + impl->ref(); + return { BunStringTag::WTFStringImpl, { .wtf = &impl.leakRef() } }; +} + extern "C" BunString BunString__fromUTF8(const char* bytes, size_t length) { if (simdutf::validate_utf8(bytes, length)) { diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 553b292d6..642039ba5 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -541,9 +541,18 @@ pub const Encoding = enum(u8) { const result = JSC.ZigString.init(out).toValueGC(globalThis); return result; }, - else => { - globalThis.throwInvalidArguments("Unexpected encoding", .{}); - return JSC.JSValue.zero; + .buffer => { + return JSC.ArrayBuffer.createBuffer(globalThis, input); + }, + + inline else => |enc| { + const res = JSC.WebCore.Encoder.toString(input.ptr, size, globalThis, enc); + if (res.isError()) { + globalThis.throwValue(res); + return .zero; + } + + return res; }, } } @@ -571,9 +580,18 @@ pub const Encoding = enum(u8) { const result = JSC.ZigString.init(out).toValueGC(globalThis); return result; }, - else => { - globalThis.throwInvalidArguments("Unexpected encoding", .{}); - return JSC.JSValue.zero; + .buffer => { + return JSC.ArrayBuffer.createBuffer(globalThis, input); + }, + inline else => |enc| { + const res = JSC.WebCore.Encoder.toString(input.ptr, input.len, globalThis, enc); + + if (res.isError()) { + globalThis.throwValue(res); + return .zero; + } + + return res; }, } } diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index bb1180acb..dd47ccc29 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -829,23 +829,18 @@ pub const Encoder = struct { return ZigString.init(input).toValueGC(global); } - if (input.len < 512) { - var buf: [512]u8 = undefined; - var to = buf[0..input.len]; - strings.copyLatin1IntoASCII(to, input); - return ZigString.init(to).toValueGC(global); - } - - var to = allocator.alloc(u8, len) catch return ZigString.init("Out of memory").toErrorInstance(global); - strings.copyLatin1IntoASCII(to, input); - return ZigString.init(to).toExternalValue(global); + var str = bun.String.createUninitialized(.latin1, len) orelse return ZigString.init("Out of memory").toErrorInstance(global); + defer str.deref(); + strings.copyLatin1IntoASCII(@constCast(str.latin1()), input); + return str.toJS(global); }, .latin1 => { - var to = allocator.alloc(u8, len) catch return ZigString.init("Out of memory").toErrorInstance(global); + var str = bun.String.createUninitialized(.latin1, len) orelse return ZigString.init("Out of memory").toErrorInstance(global); + defer str.deref(); - @memcpy(to, input_ptr[0..to.len]); + @memcpy(@constCast(str.latin1()), input_ptr[0..len]); - return ZigString.init(to).toExternalValue(global); + return str.toJS(global); }, .buffer, .utf8 => { const converted = strings.toUTF16Alloc(allocator, input, false) catch return ZigString.init("Out of memory").toErrorInstance(global); @@ -861,21 +856,22 @@ pub const Encoder = struct { // Avoid incomplete characters if (len / 2 == 0) return ZigString.Empty.toValue(global); - var output = allocator.alloc(u16, len / 2) catch return ZigString.init("Out of memory").toErrorInstance(global); - var output_bytes = std.mem.sliceAsBytes(output); + var output = bun.String.createUninitialized(.utf16, len / 2) orelse return ZigString.init("Out of memory").toErrorInstance(global); + defer output.deref(); + var output_bytes = std.mem.sliceAsBytes(@constCast(output.utf16())); output_bytes[output_bytes.len - 1] = 0; @memcpy(output_bytes, input_ptr[0..output_bytes.len]); - return ZigString.toExternalU16(output.ptr, output.len, global); + return output.toJS(global); }, .hex => { - var output = allocator.alloc(u8, input.len * 2) catch return ZigString.init("Out of memory").toErrorInstance(global); + var str = bun.String.createUninitialized(.latin1, len * 2) orelse return ZigString.init("Out of memory").toErrorInstance(global); + defer str.deref(); + var output = @constCast(str.latin1()); const wrote = strings.encodeBytesToHex(output, input); std.debug.assert(wrote == output.len); - var val = ZigString.init(output); - val.mark(); - return val.toExternalValue(global); + return str.toJS(global); }, .base64url => { diff --git a/src/string.zig b/src/string.zig index 166a0a6f7..5f107197f 100644 --- a/src/string.zig +++ b/src/string.zig @@ -257,6 +257,8 @@ pub const String = extern struct { extern fn BunString__fromLatin1(bytes: [*]const u8, len: usize) String; extern fn BunString__fromBytes(bytes: [*]const u8, len: usize) String; + extern fn BunString__fromLatin1Unitialized(len: usize) String; + extern fn BunString__fromUTF16Unitialized(len: usize) String; pub fn toOwnedSlice(this: String, allocator: std.mem.Allocator) ![]u8 { switch (this.tag) { @@ -278,6 +280,30 @@ pub const String = extern struct { } } + pub fn createUninitializedLatin1(len: usize) String { + JSC.markBinding(@src()); + return BunString__fromLatin1Unitialized(len); + } + + pub fn createUninitializedUTF16(len: usize) String { + JSC.markBinding(@src()); + return BunString__fromUTF16Unitialized(len); + } + + pub fn createUninitialized(comptime kind: @Type(.EnumLiteral), len: usize) ?String { + const without_check = switch (comptime kind) { + .latin1 => createUninitializedLatin1(len), + .utf16 => createUninitializedUTF16(len), + else => @compileError("Invalid string kind"), + }; + + if (without_check.tag == .Dead) { + return null; + } + + return without_check; + } + pub fn createLatin1(bytes: []const u8) String { JSC.markBinding(@src()); return BunString__fromLatin1(bytes.ptr, bytes.len); diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index 5a68540cf..2489f96c7 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -43,6 +43,50 @@ describe("createHash", () => { expect(Buffer.isBuffer(hash.digest())).toBeTrue(); }); + const otherEncodings = { + ucs2: [ + 11626, 2466, 37699, 38942, 64564, 53010, 48101, 47943, 44761, 18499, 12442, 26994, 46434, 62582, 39395, 20542, + ], + latin1: [ + 106, 45, 162, 9, 67, 147, 30, 152, 52, 252, 18, 207, 229, 187, 71, 187, 217, 174, 67, 72, 154, 48, 114, 105, 98, + 181, 118, 244, 227, 153, 62, 80, + ], + binary: [ + 106, 45, 162, 9, 67, 147, 30, 152, 52, 252, 18, 207, 229, 187, 71, 187, 217, 174, 67, 72, 154, 48, 114, 105, 98, + 181, 118, 244, 227, 153, 62, 80, + ], + base64: [ + 97, 105, 50, 105, 67, 85, 79, 84, 72, 112, 103, 48, 47, 66, 76, 80, 53, 98, 116, 72, 117, 57, 109, 117, 81, 48, + 105, 97, 77, 72, 74, 112, 89, 114, 86, 50, 57, 79, 79, 90, 80, 108, 65, 61, + ], + hex: [ + 54, 97, 50, 100, 97, 50, 48, 57, 52, 51, 57, 51, 49, 101, 57, 56, 51, 52, 102, 99, 49, 50, 99, 102, 101, 53, 98, + 98, 52, 55, 98, 98, 100, 57, 97, 101, 52, 51, 52, 56, 57, 97, 51, 48, 55, 50, 54, 57, 54, 50, 98, 53, 55, 54, 102, + 52, 101, 51, 57, 57, 51, 101, 53, 48, + ], + ascii: [ + 106, 45, 34, 9, 67, 19, 30, 24, 52, 124, 18, 79, 101, 59, 71, 59, 89, 46, 67, 72, 26, 48, 114, 105, 98, 53, 118, + 116, 99, 25, 62, 80, + ], + utf8: [ + 106, 45, 65533, 9, 67, 65533, 30, 65533, 52, 65533, 18, 65533, 65533, 71, 65533, 1646, 67, 72, 65533, 48, 114, + 105, 98, 65533, 118, 65533, 65533, 62, 80, + ], + }; + + for (let encoding in otherEncodings) { + it("digest " + encoding, () => { + const hash = crypto.createHash("sha256"); + hash.update("some data to hash"); + expect( + hash + .digest(encoding) + .split("") + .map(a => a.charCodeAt(0)), + ).toEqual(otherEncodings[encoding]); + }); + } + it("stream (sync)", () => { const hash = crypto.createHash("sha256"); hash.write("some data to hash"); -- cgit v1.2.3 From 854ddaa909a97c0317ec29682f6c6635818d7b9e Mon Sep 17 00:00:00 2001 From: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Date: Tue, 11 Jul 2023 17:44:29 -0700 Subject: Implement throwIfNoEntry --- src/bun.js/node/node_fs.zig | 51 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 8 deletions(-) (limited to 'src/bun.js/node') diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 74a41c5bd..3f298c5c7 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -652,6 +652,7 @@ pub const Arguments = struct { pub const Stat = struct { path: PathLike, big_int: bool = false, + throw_if_no_entry: bool = true, pub fn deinit(this: Stat) void { this.path.deinit(); @@ -672,13 +673,25 @@ pub const Arguments = struct { if (exception.* != null) return null; + var throw_if_no_entry = true; + const big_int = brk: { if (arguments.next()) |next_val| { if (next_val.isObject()) { if (next_val.isCallable(ctx.ptr().vm())) break :brk false; arguments.eat(); - if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch false) |big_int| { + if (next_val.getOptional(ctx.ptr(), "throwIfNoEntry", bool) catch { + path.deinit(); + return null; + }) |throw_if_no_entry_val| { + throw_if_no_entry = throw_if_no_entry_val; + } + + if (next_val.getOptional(ctx.ptr(), "bigint", bool) catch { + path.deinit(); + return null; + }) |big_int| { break :brk big_int; } } @@ -688,7 +701,7 @@ pub const Arguments = struct { if (exception.* != null) return null; - return Stat{ .path = path, .big_int = big_int }; + return Stat{ .path = path, .big_int = big_int, .throw_if_no_entry = throw_if_no_entry }; } }; @@ -2497,6 +2510,18 @@ pub const Arguments = struct { }; }; +pub const StatOrNotFound = union(enum) { + stats: Stats, + not_found: void, + + pub fn toJS(this: *StatOrNotFound, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return switch (this.*) { + .stats => this.stats.toJS(globalObject), + .not_found => JSC.JSValue.undefined, + }; + } +}; + const Return = struct { pub const Access = void; pub const AppendFile = void; @@ -2515,7 +2540,7 @@ const Return = struct { pub const Lchmod = void; pub const Lchown = void; pub const Link = void; - pub const Lstat = Stats; + pub const Lstat = StatOrNotFound; pub const Mkdir = bun.String; pub const Mkdtemp = JSC.ZigString; pub const Open = FileDescriptor; @@ -2617,7 +2642,7 @@ const Return = struct { pub const RealpathNative = Realpath; pub const Rename = void; pub const Rmdir = void; - pub const Stat = Stats; + pub const Stat = StatOrNotFound; pub const Symlink = void; pub const Truncate = void; @@ -3129,8 +3154,13 @@ pub const NodeFS = struct { &this.sync_error_buf, ), )) { - .result => |result| Maybe(Return.Lstat){ .result = Return.Lstat.init(result, false) }, - .err => |err| Maybe(Return.Lstat){ .err = err }, + .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = Stats.init(result, args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Lstat){ .err = err }; + }, }; }, else => {}, @@ -4333,8 +4363,13 @@ pub const NodeFS = struct { &this.sync_error_buf, ), )) { - .result => |result| Maybe(Return.Stat){ .result = Return.Stat.init(result, false) }, - .err => |err| Maybe(Return.Stat){ .err = err }, + .result => |result| Maybe(Return.Stat){ .result = .{ .stats = Stats.init(result, args.big_int) } }, + .err => |err| brk: { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { + return Maybe(Return.Stat){ .result = .{ .not_found = {} } }; + } + break :brk Maybe(Return.Stat){ .err = err }; + }, }); }, else => {}, -- cgit v1.2.3