await FileHandle functions (#9451)

* Await FileHandle functions

* Update fs.promises.ts

* use await using = await

* Make this more robust + fix tests

---------

Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
Jarred Sumner
2024-03-15 21:05:43 -07:00
committed by GitHub
parent 8c5ac06113
commit 307cac5ecd
2 changed files with 381 additions and 266 deletions

View File

@@ -218,267 +218,292 @@ const real_export = {
};
export default real_export;
// Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148
class FileHandle extends EventEmitter {
constructor(fd) {
super();
this[kFd] = fd ? fd : -1;
this[kRefs] = 1;
this[kClosePromise] = null;
}
getAsyncId() {
throw new Error("BUN TODO FileHandle.getAsyncId");
}
get fd() {
return this[kFd];
}
appendFile(data, options) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.writeFile, fd);
try {
this[kRef]();
return real_export.writeFile(fd, data, options);
} finally {
this[kUnref]();
}
}
chmod(mode) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.fchmod, fd);
try {
this[kRef]();
return real_export.fchmod(fd, mode);
} finally {
this[kUnref]();
}
}
chown(uid, gid) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.fchown, fd);
try {
this[kRef]();
return real_export.fchown(fd, uid, gid);
} finally {
this[kUnref]();
}
}
datasync() {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.fdatasync, fd);
try {
this[kRef]();
return real_export.fdatasync(fd);
} finally {
this[kUnref]();
}
}
sync() {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.fsync, fd);
try {
this[kRef]();
return real_export.fsync(fd);
} finally {
this[kUnref]();
}
}
read(buffer, offset, length, position) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.read, fd);
try {
this[kRef]();
const bytesRead = real_export.read(fd, buffer, offset, length, position);
return { bytesRead, buffer };
} finally {
this[kUnref]();
}
}
readv(buffers, position) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.readv, fd);
try {
this[kRef]();
const bytesRead = real_export.readv(fd, buffers, position);
return { bytesRead, buffers };
} finally {
this[kUnref]();
}
}
readFile(options) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.readFile, fd);
try {
this[kRef]();
return real_export.readFile(fd, options);
} finally {
this[kUnref]();
}
}
readLines(options = undefined) {
throw new Error("BUN TODO FileHandle.readLines");
}
stat(options) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.fstat, fd);
try {
this[kRef]();
return real_export.fstat(fd, options);
} finally {
this[kUnref]();
}
}
truncate(len = 0) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.ftruncate, fd);
try {
this[kRef]();
return real_export.ftruncate(fd, len);
} finally {
this[kUnref]();
}
}
utimes(atime, mtime) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.futimes, fd);
try {
this[kRef]();
return real_export.futimes(fd, atime, mtime);
} finally {
this[kUnref]();
}
}
write(buffer, offset, length, position) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.write, fd);
try {
this[kRef]();
return real_export.write(fd, buffer, offset, length, position);
} finally {
this[kUnref]();
}
}
writev(buffers, position) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.writev, fd);
try {
this[kRef]();
return real_export.writev(fd, buffers, position);
} finally {
this[kUnref]();
}
}
writeFile(data, options) {
const fd = this[kFd];
throwEBADFIfNecessary(real_export.writeFile, fd);
try {
this[kRef]();
return real_export.writeFile(fd, data, options);
} finally {
this[kUnref]();
}
}
close() {
if (this[kFd] === -1) {
return PromiseResolve();
{
const {
writeFile,
readFile,
fchmod,
fchown,
fdatasync,
fsync,
read,
readv,
fstat,
ftruncate,
futimes,
write,
writev,
close,
} = real_export;
// Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148
// These functions await the result so that errors propagate correctly with
// async stack traces and so that the ref counting is correct.
var FileHandle = class FileHandle extends EventEmitter {
constructor(fd) {
super();
this[kFd] = fd ? fd : -1;
this[kRefs] = 1;
this[kClosePromise] = null;
}
if (this[kClosePromise]) {
getAsyncId() {
throw new Error("BUN TODO FileHandle.getAsyncId");
}
get fd() {
return this[kFd];
}
async appendFile(data, options) {
const fd = this[kFd];
throwEBADFIfNecessary(writeFile, fd);
try {
this[kRef]();
return await writeFile(fd, data, options);
} finally {
this[kUnref]();
}
}
async chmod(mode) {
const fd = this[kFd];
throwEBADFIfNecessary(fchmod, fd);
try {
this[kRef]();
return await fchmod(fd, mode);
} finally {
this[kUnref]();
}
}
async chown(uid, gid) {
const fd = this[kFd];
throwEBADFIfNecessary(fchown, fd);
try {
this[kRef]();
return await fchown(fd, uid, gid);
} finally {
this[kUnref]();
}
}
async datasync() {
const fd = this[kFd];
throwEBADFIfNecessary(fdatasync, fd);
try {
this[kRef]();
return await fdatasync(fd);
} finally {
this[kUnref]();
}
}
async sync() {
const fd = this[kFd];
throwEBADFIfNecessary(fsync, fd);
try {
this[kRef]();
return await fsync(fd);
} finally {
this[kUnref]();
}
}
async read(buffer, offset, length, position) {
const fd = this[kFd];
throwEBADFIfNecessary(read, fd);
try {
this[kRef]();
return { buffer, bytesRead: await read(fd, buffer, offset, length, position) };
} finally {
this[kUnref]();
}
}
async readv(buffers, position) {
const fd = this[kFd];
throwEBADFIfNecessary(readv, fd);
try {
this[kRef]();
return await readv(fd, buffers, position);
} finally {
this[kUnref]();
}
}
async readFile(options) {
const fd = this[kFd];
throwEBADFIfNecessary(readFile, fd);
try {
this[kRef]();
return await readFile(fd, options);
} finally {
this[kUnref]();
}
}
readLines(options = undefined) {
throw new Error("BUN TODO FileHandle.readLines");
}
async stat(options) {
const fd = this[kFd];
throwEBADFIfNecessary(fstat, fd);
try {
this[kRef]();
return await fstat(fd, options);
} finally {
this[kUnref]();
}
}
async truncate(len = 0) {
const fd = this[kFd];
throwEBADFIfNecessary(ftruncate, fd);
try {
this[kRef]();
return await ftruncate(fd, len);
} finally {
this[kUnref]();
}
}
async utimes(atime, mtime) {
const fd = this[kFd];
throwEBADFIfNecessary(futimes, fd);
try {
this[kRef]();
return await futimes(fd, atime, mtime);
} finally {
this[kUnref]();
}
}
async write(buffer, offset, length, position) {
const fd = this[kFd];
throwEBADFIfNecessary(write, fd);
try {
this[kRef]();
return { buffer, bytesWritten: await write(fd, buffer, offset, length, position) };
} finally {
this[kUnref]();
}
}
async writev(buffers, position) {
const fd = this[kFd];
throwEBADFIfNecessary(writev, fd);
try {
this[kRef]();
return await writev(fd, buffers, position);
} finally {
this[kUnref]();
}
}
async writeFile(data, options) {
const fd = this[kFd];
throwEBADFIfNecessary(writeFile, fd);
try {
this[kRef]();
return await writeFile(fd, data, options);
} finally {
this[kUnref]();
}
}
async close() {
if (this[kFd] === -1) {
return;
}
if (this[kClosePromise]) {
return this[kClosePromise];
}
this[kRefs]--;
if (this[kRefs] === 0) {
this[kClosePromise] = SafePromisePrototypeFinally.$call(close(this[kFd]), () => {
this[kClosePromise] = undefined;
});
} else {
this[kClosePromise] = SafePromisePrototypeFinally.$call(
new Promise((resolve, reject) => {
this[kCloseResolve] = resolve;
this[kCloseReject] = reject;
}),
() => {
this[kClosePromise] = undefined;
this[kCloseReject] = undefined;
this[kCloseResolve] = undefined;
},
);
}
this.emit("close");
return this[kClosePromise];
}
this[kRefs]--;
if (this[kRefs] === 0) {
this[kClosePromise] = SafePromisePrototypeFinally.$call(real_export.close(this[kFd]), () => {
this[kClosePromise] = undefined;
});
} else {
this[kClosePromise] = SafePromisePrototypeFinally.$call(
new Promise((resolve, reject) => {
this[kCloseResolve] = resolve;
this[kCloseReject] = reject;
}),
() => {
this[kClosePromise] = undefined;
this[kCloseReject] = undefined;
this[kCloseResolve] = undefined;
},
);
async [SymbolAsyncDispose]() {
return this.close();
}
this.emit("close");
return this[kClosePromise];
}
readableWebStream(options = kEmptyObject) {
const fd = this[kFd];
throwEBADFIfNecessary(fs.createReadStream, fd);
async [SymbolAsyncDispose]() {
return this.close();
}
readableWebStream(options = kEmptyObject) {
throw new Error("BUN TODO FileHandle.readableWebStream");
}
createReadStream(options = undefined) {
throw new Error("BUN TODO FileHandle.createReadStream");
}
createWriteStream(options = undefined) {
throw new Error("BUN TODO FileHandle.createWriteStream");
}
[kTransfer]() {
throw new Error("BUN TODO FileHandle.kTransfer");
}
[kTransferList]() {
throw new Error("BUN TODO FileHandle.kTransferList");
}
[kDeserialize]({ handle }) {
throw new Error("BUN TODO FileHandle.kDeserialize");
}
[kRef]() {
this[kRefs]++;
}
[kUnref]() {
this[kRefs]--;
if (this[kRefs] === 0) {
PromisePrototypeThen(this.close(), this[kCloseResolve], this[kCloseReject]);
return Bun.file(fd).stream();
}
}
createReadStream(options) {
const fd = this[kFd];
throwEBADFIfNecessary(fs.createReadStream, fd);
return require("node:fs").createReadStream("", { fd, highWaterMark: 64 * 1024, ...(options || {}) });
}
createWriteStream(options) {
const fd = this[kFd];
throwEBADFIfNecessary(fs.createWriteStream, fd);
return require("node:fs").createWriteStream("", { fd, ...(options || {}) });
}
[kTransfer]() {
throw new Error("BUN TODO FileHandle.kTransfer");
}
[kTransferList]() {
throw new Error("BUN TODO FileHandle.kTransferList");
}
[kDeserialize]({ handle }) {
throw new Error("BUN TODO FileHandle.kDeserialize");
}
[kRef]() {
this[kRefs]++;
}
[kUnref]() {
const refCount = this[kRefs]--;
if (refCount === 1) {
PromisePrototypeThen.$call(this.close(), this[kCloseResolve], this[kCloseReject]);
}
}
};
}
function throwEBADFIfNecessary(fn, fd) {

View File

@@ -1,5 +1,5 @@
// @known-failing-on-windows: 1 failing
import { describe, expect, it } from "bun:test";
import { describe, expect, it, spyOn } from "bun:test";
import { dirname, resolve, relative } from "node:path";
import { promisify } from "node:util";
import { bunEnv, bunExe, gc, getMaxFD, isIntelMacOS, isWindows } from "harness";
@@ -40,7 +40,7 @@ import fs, {
fdatasyncSync,
} from "node:fs";
import _promises from "node:fs/promises";
import _promises, { type FileHandle } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -60,6 +60,108 @@ function mkdirForce(path: string) {
if (!existsSync(path)) mkdirSync(path, { recursive: true });
}
describe("FileHandle", () => {
it("FileHandle#read returns object", async () => {
await using fd = await fs.promises.open(__filename);
const buf = Buffer.alloc(10);
expect(await fd.read(buf, 0, 10, 0)).toEqual({ bytesRead: 10, buffer: buf });
});
it("FileHandle#readv returns object", async () => {
await using fd = await fs.promises.open(__filename);
const buffers = [Buffer.alloc(10), Buffer.alloc(10)];
expect(await fd.readv(buffers, 0)).toEqual({ bytesRead: 20, buffers });
});
it("FileHandle#write throws EBADF when closed", async () => {
let handle: FileHandle;
let spy;
{
await using fd = await fs.promises.open(__filename);
handle = fd;
spy = spyOn(handle, "close");
const buffers = [Buffer.alloc(10), Buffer.alloc(10)];
expect(await fd.readv(buffers, 0)).toEqual({ bytesRead: 20, buffers });
}
expect(handle.close).toHaveBeenCalled();
expect(async () => await handle.read(Buffer.alloc(10))).toThrow("Bad file descriptor");
});
it("FileHandle#write returns object", async () => {
await using fd = await fs.promises.open(`${tmpdir()}/${Date.now()}.writeFile.txt`, "w");
const buf = Buffer.from("test");
expect(await fd.write(buf, 0, 4, 0)).toEqual({ bytesWritten: 4, buffer: buf });
});
it("FileHandle#writev returns object", async () => {
await using fd = await fs.promises.open(`${tmpdir()}/${Date.now()}.writeFile.txt`, "w");
const buffers = [Buffer.from("test"), Buffer.from("test")];
expect(await fd.writev(buffers, 0)).toEqual({ bytesWritten: 8, buffers });
});
it("FileHandle#readFile returns buffer", async () => {
await using fd = await fs.promises.open(__filename);
const buf = await fd.readFile();
expect(buf instanceof Buffer).toBe(true);
});
it("FileHandle#readableWebStream", async () => {
await using fd = await fs.promises.open(__filename);
const stream = fd.readableWebStream();
const reader = stream.getReader();
const chunk = await reader.read();
expect(chunk.value instanceof Uint8Array).toBe(true);
reader.releaseLock();
await stream.cancel();
});
it("FileHandle#createReadStream", async () => {
await using fd = await fs.promises.open(__filename);
const readable = fd.createReadStream();
const data = await new Promise(resolve => {
let data = "";
readable.on("data", chunk => {
data += chunk;
});
readable.on("end", () => {
resolve(data);
});
});
expect(data).toBe(readFileSync(__filename, "utf8"));
});
it("FileHandle#writeFile", async () => {
const path = `${tmpdir()}/${Date.now()}.writeFile.txt`;
await using fd = await fs.promises.open(path, "w");
await fd.writeFile("File written successfully");
expect(readFileSync(path, "utf8")).toBe("File written successfully");
});
it("FileHandle#createWriteStream", async () => {
const path = `${tmpdir()}/${Date.now()}.createWriteStream.txt`;
{
await using fd = await fs.promises.open(path, "w");
const stream = fd.createWriteStream();
stream.write("Test file written successfully");
stream.end();
await new Promise((resolve, reject) => {
stream.on("error", e => {
reject(e);
});
stream.on("finish", () => {
expect(readFileSync(path, "utf8")).toBe("Test file written successfully");
resolve(true);
});
});
}
expect(readFileSync(path, "utf8")).toBe("Test file written successfully");
});
});
it("fdatasyncSync", () => {
const fd = openSync(import.meta.path, "w", 0o664);
fdatasyncSync(fd);
@@ -2159,18 +2261,6 @@ describe("fs/promises", () => {
it("opendir should have a path property, issue#4995", async () => {
expect((await fs.promises.opendir(".")).path).toBe(".");
});
it("FileHandle#read returns object", async () => {
const fd = await fs.promises.open(__filename);
const buf = Buffer.alloc(10);
expect(await fd.read(buf, 0, 10, 0)).toEqual({ bytesRead: 10, buffer: buf });
});
it("FileHandle#readv returns object", async () => {
const fd = await fs.promises.open(__filename);
const buffers = [Buffer.alloc(10), Buffer.alloc(10)];
expect(await fd.readv(buffers, 0, 20, 0)).toEqual({ bytesRead: 20, buffers });
});
});
it("stat on a large file", () => {