Skip to content

Commit a27fe21

Browse files
authored
vfs: fix VirtualReadStream race with async iteration
Fixes: #64377 Signed-off-by: y1d7ng <y1d7ng@yeah.net> PR-URL: #64394 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 61b8e12 commit a27fe21

3 files changed

Lines changed: 41 additions & 17 deletions

File tree

lib/internal/vfs/streams.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,7 @@ class VirtualReadStream extends Readable {
8080
this.#autoClose = options.autoClose !== false;
8181

8282
if (fd !== null && fd !== undefined) {
83-
// Use the already-open file descriptor
8483
this.#fd = fd;
85-
process.nextTick(() => {
86-
this.emit('open', this.#fd);
87-
this.emit('ready');
88-
});
89-
} else {
90-
// Open the file on next tick so listeners can be attached.
91-
// Note: #openFile will not throw - if it fails, the stream is destroyed.
92-
process.nextTick(() => this.#openFile());
9384
}
9485
}
9586

@@ -101,18 +92,16 @@ class VirtualReadStream extends Readable {
10192
return this.#path;
10293
}
10394

104-
/**
105-
* Opens the virtual file.
106-
* Events are emitted synchronously within this method, which runs
107-
* asynchronously via process.nextTick - matching real fs behavior.
108-
*/
109-
#openFile() {
95+
_construct(callback) {
11096
try {
111-
this.#fd = this.#vfs.openSync(this.#path);
97+
if (this.#fd === null) {
98+
this.#fd = this.#vfs.openSync(this.#path);
99+
}
100+
callback();
112101
this.emit('open', this.#fd);
113102
this.emit('ready');
114103
} catch (err) {
115-
this.destroy(err);
104+
callback(err);
116105
}
117106
}
118107

test/parallel/test-vfs-stream-explicit-fd.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ function readStream(stream) {
3232
}));
3333
}
3434

35+
// ReadStream with explicit fd via for-await; autoClose:false leaves fd open
36+
(async () => {
37+
const fd = myVfs.openSync('/file.txt', 'r');
38+
const stream = myVfs.createReadStream('/file.txt', { fd, autoClose: false });
39+
const chunks = [];
40+
for await (const chunk of stream) {
41+
chunks.push(chunk);
42+
}
43+
assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world');
44+
myVfs.closeSync(fd);
45+
})().then(common.mustCall());
46+
3547
// WriteStream with explicit fd; autoClose:false leaves the fd open
3648
(async () => {
3749
const fd = myVfs.openSync('/fd-write.txt', 'w');

test/parallel/test-vfs-streams.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ const { pipeline } = require('stream/promises');
229229
}));
230230
}
231231

232+
// createReadStream via for-await iteration
233+
(async () => {
234+
const myVfs = vfs.create();
235+
myVfs.writeFileSync('/iter.txt', 'abc');
236+
const rs = myVfs.createReadStream('/iter.txt', { encoding: 'utf8' });
237+
const chunks = [];
238+
for await (const chunk of rs) {
239+
chunks.push(chunk);
240+
}
241+
assert.strictEqual(chunks.join(''), 'abc');
242+
})().then(common.mustCall());
243+
232244
// start: beyond file size → empty stream
233245
{
234246
const myVfs = vfs.create();
@@ -257,6 +269,17 @@ const { pipeline } = require('stream/promises');
257269
assert.strictEqual(myVfs.readFileSync('/out.txt', 'utf8'), 'hello world');
258270
})().then(common.mustCall());
259271

272+
// Pipeline read
273+
(async () => {
274+
const myVfs = vfs.create();
275+
myVfs.writeFileSync('/in.txt', 'hello world');
276+
await pipeline(
277+
myVfs.createReadStream('/in.txt'),
278+
myVfs.createWriteStream('/copied.txt'),
279+
);
280+
assert.strictEqual(myVfs.readFileSync('/copied.txt', 'utf8'), 'hello world');
281+
})().then(common.mustCall());
282+
260283
// Pipeline write with start position
261284
(async () => {
262285
const myVfs = vfs.create();

0 commit comments

Comments
 (0)