diff options
Diffstat (limited to '')
-rw-r--r-- | src/bun.js/node/node_fs.zig | 154 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 48 | ||||
-rw-r--r-- | src/bun.zig | 1 | ||||
-rw-r--r-- | src/copy_file.zig | 9 | ||||
-rw-r--r-- | test/js/bun/io/bun-write-exdev-fixture.js | 1 | ||||
-rw-r--r-- | test/js/bun/io/bun-write.test.js | 78 | ||||
-rw-r--r-- | test/js/node/fs/fs-fixture-copyFile-no-copy_file_range.js | 3 | ||||
-rw-r--r-- | test/js/node/fs/fs.test.ts | 73 |
8 files changed, 297 insertions, 70 deletions
diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index b7ae0418d..130ab8cdc 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3129,6 +3129,77 @@ pub const NodeFS = struct { return .{ .err = Syscall.Error.todo }; } + // since we use a 64 KB stack buffer, we should not let this function get inlined + pub noinline fn copyFileUsingReadWriteLoop(src: [:0]const u8, dest: [:0]const u8, src_fd: FileDescriptor, dest_fd: FileDescriptor, stat_size: usize, wrote: *u64) Maybe(Return.CopyFile) { + var stack_buf: [64 * 1024]u8 = undefined; + var buf_to_free: []u8 = &[_]u8{}; + var buf: []u8 = &stack_buf; + + maybe_allocate_large_temp_buf: { + if (stat_size > stack_buf.len * 16) { + // Don't allocate more than 8 MB at a time + const clamped_size: usize = @min(stat_size, 8 * 1024 * 1024); + + var buf_ = bun.default_allocator.alloc(u8, clamped_size) catch break :maybe_allocate_large_temp_buf; + buf = buf_; + buf_to_free = buf_; + } + } + + defer { + if (buf_to_free.len > 0) bun.default_allocator.free(buf_to_free); + } + + var remain = @as(u64, @intCast(@max(stat_size, 0))); + toplevel: while (remain > 0) { + const amt = switch (Syscall.read(src_fd, buf[0..@min(buf.len, remain)])) { + .result => |result| result, + .err => |err| return Maybe(Return.CopyFile){ .err = if (src.len > 0) err.withPath(src) else err }, + }; + // 0 == EOF + if (amt == 0) { + break :toplevel; + } + wrote.* += amt; + remain -|= amt; + + var slice = buf[0..amt]; + while (slice.len > 0) { + const written = switch (Syscall.write(dest_fd, slice)) { + .result => |result| result, + .err => |err| return Maybe(Return.CopyFile){ .err = if (dest.len > 0) err.withPath(dest) else err }, + }; + if (written == 0) break :toplevel; + slice = slice[written..]; + } + } else { + outer: while (true) { + const amt = switch (Syscall.read(src_fd, buf)) { + .result => |result| result, + .err => |err| return Maybe(Return.CopyFile){ .err = if (src.len > 0) err.withPath(src) else err }, + }; + // we don't know the size + // so we just go forever until we get an EOF + if (amt == 0) { + break; + } + wrote.* += amt; + + var slice = buf[0..amt]; + while (slice.len > 0) { + const written = switch (Syscall.write(dest_fd, slice)) { + .result => |result| result, + .err => |err| return Maybe(Return.CopyFile){ .err = if (dest.len > 0) err.withPath(dest) else err }, + }; + slice = slice[written..]; + if (written == 0) break :outer; + } + } + } + + return Maybe(Return.CopyFile).success; + } + /// https://github.com/libuv/libuv/pull/2233 /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 @@ -3191,65 +3262,11 @@ pub const NodeFS = struct { }; defer { _ = std.c.ftruncate(dest_fd, @as(std.c.off_t, @intCast(@as(u63, @truncate(wrote))))); + _ = C.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } - // stack buffer of 16 KB - // this code path isn't hit unless the buffer is < 128 KB - // 16 writes is ok - // 16 KB is high end of what is okay to use for stack space - // good thing we ask for absurdly large stack sizes - var buf: [16384]u8 = undefined; - var remain = @as(u64, @intCast(@max(stat_.size, 0))); - toplevel: while (remain > 0) { - const amt = switch (Syscall.read(src_fd, buf[0..@min(buf.len, remain)])) { - .result => |result| result, - .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(src) }, - }; - // 0 == EOF - if (amt == 0) { - break :toplevel; - } - wrote += amt; - remain -|= amt; - - var slice = buf[0..amt]; - while (slice.len > 0) { - const written = switch (Syscall.write(dest_fd, slice)) { - .result => |result| result, - .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(dest) }, - }; - if (written == 0) break :toplevel; - slice = slice[written..]; - } - } else { - outer: while (true) { - const amt = switch (Syscall.read(src_fd, &buf)) { - .result => |result| result, - .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(src) }, - }; - // we don't know the size - // so we just go forever until we get an EOF - if (amt == 0) { - break; - } - wrote += amt; - - var slice = buf[0..amt]; - while (slice.len > 0) { - const written = switch (Syscall.write(dest_fd, slice)) { - .result => |result| result, - .err => |err| return Maybe(Return.CopyFile){ .err = err.withPath(dest) }, - }; - slice = slice[written..]; - if (written == 0) break :outer; - } - } - } - // can't really do anything with this error - _ = C.fchmod(dest_fd, stat_.mode); - - return ret.success; + return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, @intCast(@max(stat_.size, 0)), &wrote); } } @@ -3298,16 +3315,21 @@ pub const NodeFS = struct { .err => |err| return Maybe(Return.CopyFile){ .err = err }, }; - var size = @as(usize, @intCast(@max(stat_.size, 0))); + var size: usize = @intCast(@max(stat_.size, 0)); defer { _ = linux.ftruncate(dest_fd, @as(i64, @intCast(@as(u63, @truncate(wrote))))); + _ = linux.fchmod(dest_fd, stat_.mode); _ = Syscall.close(dest_fd); } var off_in_copy = @as(i64, @bitCast(@as(u64, 0))); var off_out_copy = @as(i64, @bitCast(@as(u64, 0))); + if (!bun.canUseCopyFileRangeSyscall()) { + return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + } + if (size == 0) { // copy until EOF while (true) { @@ -3315,10 +3337,10 @@ pub const NodeFS = struct { // Linux Kernel 5.3 or later const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, std.mem.page_size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { - // TODO: handle EXDEV - // seems like zfs does not support copy_file_range across devices - // see https://discord.com/channels/876711213126520882/876711213126520885/1006465112707698770 - return err; + return switch (err.getErrno()) { + .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote), + else => return err, + }; } // wrote zero bytes means EOF if (written == 0) break; @@ -3329,10 +3351,10 @@ pub const NodeFS = struct { // Linux Kernel 5.3 or later const written = linux.copy_file_range(src_fd, &off_in_copy, dest_fd, &off_out_copy, size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { - // TODO: handle EXDEV - // seems like zfs does not support copy_file_range across devices - // see https://discord.com/channels/876711213126520882/876711213126520885/1006465112707698770 - return err; + return switch (err.getErrno()) { + .XDEV, .NOSYS => copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote), + else => return err, + }; } // wrote zero bytes means EOF if (written == 0) break; @@ -3340,7 +3362,7 @@ pub const NodeFS = struct { size -|= written; } } - _ = linux.fchmod(dest_fd, stat_.mode); + return ret.success; } }, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 8abc87713..983841581 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -2134,7 +2134,8 @@ pub const Blob = struct { this.read_off += this.offset; var remain = @as(usize, this.max_length); - if (remain == max_size or remain == 0) { + const unknown_size = remain == max_size or remain == 0; + if (unknown_size) { // sometimes stat lies // let's give it 4096 and see how it goes remain = 4096; @@ -2150,6 +2151,21 @@ pub const Blob = struct { var has_unset_append = false; + // If they can't use copy_file_range, they probably also can't + // use sendfile() or splice() + if (!bun.canUseCopyFileRangeSyscall()) { + switch (JSC.Node.NodeFS.copyFileUsingReadWriteLoop("", "", src_fd, dest_fd, if (unknown_size) 0 else remain, &total_written)) { + .err => |err| { + this.system_error = err.toSystemError(); + return AsyncIO.asError(err.errno); + }, + .result => { + _ = linux.ftruncate(dest_fd, @as(std.os.off_t, @intCast(total_written))); + return; + }, + } + } + while (true) { const written = switch (comptime use) { .copy_file_range => linux.copy_file_range(src_fd, null, dest_fd, null, remain, 0), @@ -2160,6 +2176,19 @@ pub const Blob = struct { switch (linux.getErrno(written)) { .SUCCESS => {}, + .NOSYS, .XDEV => { + switch (JSC.Node.NodeFS.copyFileUsingReadWriteLoop("", "", src_fd, dest_fd, if (unknown_size) 0 else remain, &total_written)) { + .err => |err| { + this.system_error = err.toSystemError(); + return AsyncIO.asError(err.errno); + }, + .result => { + _ = linux.ftruncate(dest_fd, @as(std.os.off_t, @intCast(total_written))); + return; + }, + } + }, + .INVAL => { if (comptime clear_append_if_invalid) { if (!has_unset_append) { @@ -2175,6 +2204,23 @@ pub const Blob = struct { } } + // If the Linux machine doesn't support + // copy_file_range or the file descrpitor is + // incompatible with the chosen syscall, fall back + // to a read/write loop + if (total_written == 0) { + switch (JSC.Node.NodeFS.copyFileUsingReadWriteLoop("", "", src_fd, dest_fd, if (unknown_size) 0 else remain, &total_written)) { + .err => |err| { + this.system_error = err.toSystemError(); + return AsyncIO.asError(err.errno); + }, + .result => { + _ = linux.ftruncate(dest_fd, @as(std.os.off_t, @intCast(total_written))); + return; + }, + } + } + this.system_error = (JSC.Node.Syscall.Error{ .errno = @as(JSC.Node.Syscall.Error.Int, @intCast(@intFromEnum(linux.E.INVAL))), .syscall = TryWith.tag.get(use).?, diff --git a/src/bun.zig b/src/bun.zig index a4715a862..c4dad48cf 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -873,6 +873,7 @@ pub fn FDHashMap(comptime Type: type) type { const CopyFile = @import("./copy_file.zig"); pub const copyFileRange = CopyFile.copyFileRange; +pub const canUseCopyFileRangeSyscall = CopyFile.canUseCopyFileRangeSyscall; pub const copyFile = CopyFile.copyFile; pub fn parseDouble(input: []const u8) !f64 { diff --git a/src/copy_file.zig b/src/copy_file.zig index 093c7d91d..4fbac7855 100644 --- a/src/copy_file.zig +++ b/src/copy_file.zig @@ -67,9 +67,16 @@ pub fn copyFile(fd_in: os.fd_t, fd_out: os.fd_t) CopyFileError!void { const Platform = @import("root").bun.analytics.GenerateHeader.GeneratePlatform; var can_use_copy_file_range = std.atomic.Atomic(i32).init(0); -fn canUseCopyFileRangeSyscall() bool { +pub fn canUseCopyFileRangeSyscall() bool { const result = can_use_copy_file_range.load(.Monotonic); if (result == 0) { + // This flag mostly exists to make other code more easily testable. + if (bun.getenvZ("BUN_CONFIG_DISABLE_COPY_FILE_RANGE") != null) { + bun.Output.debug("copy_file_range is disabled by BUN_CONFIG_DISABLE_COPY_FILE_RANGE", .{}); + can_use_copy_file_range.store(-1, .Monotonic); + return false; + } + const kernel = Platform.kernelVersion(); if (kernel.orderWithoutTag(.{ .major = 4, .minor = 5 }).compare(.gte)) { bun.Output.debug("copy_file_range is supported", .{}); diff --git a/test/js/bun/io/bun-write-exdev-fixture.js b/test/js/bun/io/bun-write-exdev-fixture.js new file mode 100644 index 000000000..81cd263e3 --- /dev/null +++ b/test/js/bun/io/bun-write-exdev-fixture.js @@ -0,0 +1 @@ +await Bun.write(Bun.file(process.argv.at(-1)), Bun.file(process.argv.at(-2))); diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index 120ba396d..b67df9405 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -1,6 +1,6 @@ -import fs from "fs"; +import fs, { mkdirSync } from "fs"; import { it, expect, describe } from "bun:test"; -import path from "path"; +import path, { join } from "path"; import { gcTick, withoutAggressiveGC, bunExe, bunEnv } from "harness"; import { tmpdir } from "os"; @@ -307,3 +307,77 @@ it("#2674", async () => { expect(error?.length).toBeFalsy(); expect(exitCode).toBe(0); }); + +if (process.platform === "linux") { + describe("should work when copyFileRange is not available", () => { + it("on large files", () => { + var tempdir = `${tmpdir()}/fs.test.js/${Date.now()}-1/bun-write/large`; + expect(fs.existsSync(tempdir)).toBe(false); + expect(tempdir.includes(mkdirSync(tempdir, { recursive: true }))).toBe(true); + var buffer = new Int32Array(1024 * 1024 * 64); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = i % 256; + } + + const hash = Bun.hash(buffer.buffer); + const src = join(tempdir, "Bun.write.src.blob"); + const dest = join(tempdir, "Bun.write.dest.blob"); + + try { + fs.writeFileSync(src, buffer.buffer); + + expect(fs.existsSync(dest)).toBe(false); + + const { exitCode } = Bun.spawnSync({ + stdio: ["inherit", "inherit", "inherit"], + cmd: [bunExe(), join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest], + env: { + ...bunEnv, + BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1", + }, + }); + expect(exitCode).toBe(0); + + expect(Bun.hash(fs.readFileSync(dest))).toBe(hash); + } finally { + fs.rmSync(src, { force: true }); + fs.rmSync(dest, { force: true }); + } + }); + + it("on small files", () => { + const tempdir = `${tmpdir()}/fs.test.js/${Date.now()}-1/bun-write/small`; + expect(fs.existsSync(tempdir)).toBe(false); + expect(tempdir.includes(mkdirSync(tempdir, { recursive: true }))).toBe(true); + var buffer = new Int32Array(1 * 1024); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = i % 256; + } + + const hash = Bun.hash(buffer.buffer); + const src = join(tempdir, "Bun.write.src.blob"); + const dest = join(tempdir, "Bun.write.dest.blob"); + + try { + fs.writeFileSync(src, buffer.buffer); + + expect(fs.existsSync(dest)).toBe(false); + + const { exitCode } = Bun.spawnSync({ + stdio: ["inherit", "inherit", "inherit"], + cmd: [bunExe(), join(import.meta.dir, "./bun-write-exdev-fixture.js"), src, dest], + env: { + ...bunEnv, + BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1", + }, + }); + expect(exitCode).toBe(0); + + expect(Bun.hash(fs.readFileSync(dest))).toBe(hash); + } finally { + fs.rmSync(src, { force: true }); + fs.rmSync(dest, { force: true }); + } + }); + }); +} diff --git a/test/js/node/fs/fs-fixture-copyFile-no-copy_file_range.js b/test/js/node/fs/fs-fixture-copyFile-no-copy_file_range.js new file mode 100644 index 000000000..4e990e1c7 --- /dev/null +++ b/test/js/node/fs/fs-fixture-copyFile-no-copy_file_range.js @@ -0,0 +1,3 @@ +const { copyFileSync } = require("node:fs"); + +copyFileSync(process.argv.at(-2), process.argv.at(-1)); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 58fb455fc..fac7c2b57 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -134,6 +134,79 @@ describe("copyFileSync", () => { copyFileSync(tempdir + "/copyFileSync.src.blob", tempdir + "/copyFileSync.dest.blob"); expect(Bun.hash(readFileSync(tempdir + "/copyFileSync.dest.blob"))).toBe(Bun.hash(buffer.buffer)); }); + + if (process.platform === "linux") { + describe("should work when copyFileRange is not available", () => { + it("on large files", () => { + const tempdir = `${tmpdir()}/fs.test.js/${Date.now()}-1/1234/large`; + expect(existsSync(tempdir)).toBe(false); + expect(tempdir.includes(mkdirSync(tempdir, { recursive: true })!)).toBe(true); + var buffer = new Int32Array(128 * 1024); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = i % 256; + } + + const hash = Bun.hash(buffer.buffer); + const src = tempdir + "/copyFileSync.src.blob"; + const dest = tempdir + "/copyFileSync.dest.blob"; + + writeFileSync(src, buffer.buffer); + try { + expect(existsSync(dest)).toBe(false); + + const { exitCode } = spawnSync({ + stdio: ["inherit", "inherit", "inherit"], + cmd: [bunExe(), join(import.meta.dir, "./fs-fixture-copyFile-no-copy_file_range.js"), src, dest], + env: { + ...bunEnv, + BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1", + }, + }); + expect(exitCode).toBe(0); + + expect(Bun.hash(readFileSync(dest))).toBe(hash); + } finally { + rmSync(src, { force: true }); + rmSync(dest, { force: true }); + } + }); + + it("on small files", () => { + const tempdir = `${tmpdir()}/fs.test.js/${Date.now()}-1/1234/small`; + expect(existsSync(tempdir)).toBe(false); + expect(tempdir.includes(mkdirSync(tempdir, { recursive: true })!)).toBe(true); + var buffer = new Int32Array(1 * 1024); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = i % 256; + } + + const hash = Bun.hash(buffer.buffer); + const src = tempdir + "/copyFileSync.src.blob"; + const dest = tempdir + "/copyFileSync.dest.blob"; + + try { + writeFileSync(src, buffer.buffer); + + expect(existsSync(dest)).toBe(false); + + const { exitCode } = spawnSync({ + stdio: ["inherit", "inherit", "inherit"], + cmd: [bunExe(), join(import.meta.dir, "./fs-fixture-copyFile-no-copy_file_range.js"), src, dest], + env: { + ...bunEnv, + BUN_CONFIG_DISABLE_COPY_FILE_RANGE: "1", + }, + }); + expect(exitCode).toBe(0); + + expect(Bun.hash(readFileSync(dest))).toBe(hash); + } finally { + rmSync(src, { force: true }); + rmSync(dest, { force: true }); + } + }); + }); + } }); describe("mkdirSync", () => { |