Skip to content

Commit 88ba79b

Browse files
ronagatlowChemi
authored andcommitted
fs: make sure to write entire buffer
fs.write(v) is not guaranteed to write everything in a single call. Make sure we don't assume so. PR-URL: #49211 Co-authored-by: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent 59e66a1 commit 88ba79b

2 files changed

Lines changed: 104 additions & 15 deletions

File tree

‎lib/internal/fs/streams.js‎

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ const {
1313

1414
const{
1515
ERR_INVALID_ARG_TYPE,
16-
ERR_OUT_OF_RANGE,
1716
ERR_METHOD_NOT_IMPLEMENTED,
17+
ERR_OUT_OF_RANGE,
18+
ERR_STREAM_DESTROYED,
19+
ERR_SYSTEM_ERROR,
1820
}=require('internal/errors').codes;
1921
const{
2022
deprecate,
@@ -392,22 +394,75 @@ WriteStream.prototype.open = openWriteFs;
392394

393395
WriteStream.prototype._construct=_construct;
394396

397+
functionwriteAll(data,size,pos,cb,retries=0){
398+
this[kFs].write(this.fd,data,0,size,pos,(er,bytesWritten,buffer)=>{
399+
// No data currently available and operation should be retried later.
400+
if(er?.code==='EAGAIN'){
401+
er=null;
402+
bytesWritten=0;
403+
}
404+
405+
if(this.destroyed||er){
406+
returncb(er||newERR_STREAM_DESTROYED('write'));
407+
}
408+
409+
this.bytesWritten+=bytesWritten;
410+
411+
retries=bytesWritten ? 0 : retries+1;
412+
size-=bytesWritten;
413+
pos+=bytesWritten;
414+
415+
// Try writing non-zero number of bytes up to 5 times.
416+
if(retries>5){
417+
cb(newERR_SYSTEM_ERROR('write failed'));
418+
}elseif(size){
419+
writeAll.call(this,buffer.slice(bytesWritten),size,pos,cb,retries);
420+
}else{
421+
cb();
422+
}
423+
});
424+
}
425+
426+
functionwritevAll(chunks,size,pos,cb,retries=0){
427+
this[kFs].writev(this.fd,chunks,this.pos,(er,bytesWritten,buffers)=>{
428+
// No data currently available and operation should be retried later.
429+
if(er?.code==='EAGAIN'){
430+
er=null;
431+
bytesWritten=0;
432+
}
433+
434+
if(this.destroyed||er){
435+
returncb(er||newERR_STREAM_DESTROYED('writev'));
436+
}
437+
438+
this.bytesWritten+=bytesWritten;
439+
440+
retries=bytesWritten ? 0 : retries+1;
441+
size-=bytesWritten;
442+
pos+=bytesWritten;
443+
444+
// Try writing non-zero number of bytes up to 5 times.
445+
if(retries>5){
446+
cb(newERR_SYSTEM_ERROR('writev failed'));
447+
}elseif(size){
448+
writevAll.call(this,[Buffer.concat(buffers).slice(bytesWritten)],size,pos,cb,retries);
449+
}else{
450+
cb();
451+
}
452+
});
453+
}
454+
395455
WriteStream.prototype._write=function(data,encoding,cb){
396456
this[kIsPerformingIO]=true;
397-
this[kFs].write(this.fd,data,0,data.length,this.pos,(er,bytes)=>{
457+
writeAll.call(this,data,data.length,this.pos,(er)=>{
398458
this[kIsPerformingIO]=false;
399459
if(this.destroyed){
400460
// Tell ._destroy() that it's safe to close the fd now.
401461
cb(er);
402462
returnthis.emit(kIoDone,er);
403463
}
404464

405-
if(er){
406-
returncb(er);
407-
}
408-
409-
this.bytesWritten+=bytes;
410-
cb();
465+
cb(er);
411466
});
412467

413468
if(this.pos!==undefined)
@@ -427,20 +482,15 @@ WriteStream.prototype._writev = function(data, cb) {
427482
}
428483

429484
this[kIsPerformingIO]=true;
430-
this[kFs].writev(this.fd,chunks,this.pos,(er,bytes)=>{
485+
writevAll.call(this,chunks,size,this.pos,(er)=>{
431486
this[kIsPerformingIO]=false;
432487
if(this.destroyed){
433488
// Tell ._destroy() that it's safe to close the fd now.
434489
cb(er);
435490
returnthis.emit(kIoDone,er);
436491
}
437492

438-
if(er){
439-
returncb(er);
440-
}
441-
442-
this.bytesWritten+=bytes;
443-
cb();
493+
cb(er);
444494
});
445495

446496
if(this.pos!==undefined)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import*ascommonfrom'../common/index.mjs';
2+
importtmpdirfrom'../common/tmpdir.js';
3+
importassertfrom'node:assert';
4+
importfsfrom'node:fs';
5+
import{describe,it,mock}from'node:test';
6+
import{finished}from'node:stream/promises';
7+
8+
tmpdir.refresh();
9+
constfile=tmpdir.resolve('writeStreamEAGAIN.txt');
10+
consterrorWithEAGAIN=(fd,buffer,offset,length,position,callback)=>{
11+
callback(Object.assign(newError(),{code: 'EAGAIN'}),0,buffer);
12+
};
13+
14+
describe('WriteStream EAGAIN',{concurrency: true},()=>{
15+
it('_write',async()=>{
16+
constmockWrite=mock.fn(fs.write);
17+
mockWrite.mock.mockImplementationOnce(errorWithEAGAIN);
18+
conststream=fs.createWriteStream(file,{
19+
fs: {
20+
open: common.mustCall(fs.open),
21+
write: mockWrite,
22+
close: common.mustCall(fs.close),
23+
}
24+
});
25+
stream.end('foo');
26+
stream.on('close',common.mustCall());
27+
stream.on('error',common.mustNotCall());
28+
awaitfinished(stream);
29+
assert.strictEqual(mockWrite.mock.callCount(),2);
30+
assert.strictEqual(fs.readFileSync(file,'utf8'),'foo');
31+
});
32+
33+
it('_write',async()=>{
34+
conststream=fs.createWriteStream(file);
35+
mock.getter(stream,'destroyed',()=>true);
36+
stream.end('foo');
37+
awaitfinished(stream).catch(common.mustCall());
38+
});
39+
});

0 commit comments

Comments
 (0)