Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ function clone (obj) {
var copy = Object.create(null)

Object.getOwnPropertyNames(obj).forEach(function (key) {
Object.defineProperty(copy, key, Object.getOwnPropertyDescriptor(obj, key))
var desc = Object.getOwnPropertyDescriptor(obj, key)
// Getter-only properties (common on bundler/ESM `fs` shims) cannot
// be assigned on the clone either unless we materialize them.
if (desc && desc.get && !desc.set) {
Object.defineProperty(copy, key, {
value: obj[key],
writable: true,
enumerable: desc.enumerable,
configurable: true
})
} else {
Object.defineProperty(copy, key, desc)
}
})

return copy
Expand Down
130 changes: 99 additions & 31 deletions graceful-fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,98 @@ else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || ''))
console.error(m)
}

// Some bundlers (Vite/esbuild) and ESM interop layers expose `fs`
// methods as getter-only properties. Assigning then throws
// "Cannot set property close of #<Object> which has only a getter".
function setProp (obj, key, value) {
var desc
try {
desc = Object.getOwnPropertyDescriptor(obj, key)
} catch (er) {}

if (desc) {
if (desc.writable || typeof desc.set === 'function') {
try {
obj[key] = value
return true
} catch (er) {
return false
}
}
if (desc.configurable) {
try {
Object.defineProperty(obj, key, {
value: value,
writable: true,
enumerable: desc.enumerable,
configurable: true
})
return true
} catch (er) {
return false
}
}
return false
}

try {
obj[key] = value
return true
} catch (er) {
try {
Object.defineProperty(obj, key, {
value: value,
writable: true,
enumerable: true,
configurable: true
})
return true
} catch (er2) {
return false
}
}
}

function patchClose (target) {
var fs$close = target.close
if (typeof fs$close !== 'function' || fs$close[previousSymbol])
return

function close (fd, cb) {
return fs$close.call(fs, fd, function (err) {
// This function uses the graceful-fs shared queue
if (!err) {
resetQueue()
}

if (typeof cb === 'function')
cb.apply(this, arguments)
})
}

Object.defineProperty(close, previousSymbol, {
value: fs$close
})
setProp(target, 'close', close)
}

function patchCloseSync (target) {
var fs$closeSync = target.closeSync
if (typeof fs$closeSync !== 'function' || fs$closeSync[previousSymbol])
return

function closeSync (fd) {
// This function uses the graceful-fs shared queue
fs$closeSync.apply(fs, arguments)
resetQueue()
}

Object.defineProperty(closeSync, previousSymbol, {
value: fs$closeSync
})
setProp(target, 'closeSync', closeSync)
}

// Once time initialization
if (!fs[gracefulQueue]) {
// This queue can be shared by multiple loaded instances
Expand All @@ -49,37 +141,8 @@ if (!fs[gracefulQueue]) {
// to retry() whenever a close happens *anywhere* in the program.
// This is essential when multiple graceful-fs instances are
// in play at the same time.
fs.close = (function (fs$close) {
function close (fd, cb) {
return fs$close.call(fs, fd, function (err) {
// This function uses the graceful-fs shared queue
if (!err) {
resetQueue()
}

if (typeof cb === 'function')
cb.apply(this, arguments)
})
}

Object.defineProperty(close, previousSymbol, {
value: fs$close
})
return close
})(fs.close)

fs.closeSync = (function (fs$closeSync) {
function closeSync (fd) {
// This function uses the graceful-fs shared queue
fs$closeSync.apply(fs, arguments)
resetQueue()
}

Object.defineProperty(closeSync, previousSymbol, {
value: fs$closeSync
})
return closeSync
})(fs.closeSync)
patchClose(fs)
patchCloseSync(fs)

if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) {
process.on('exit', function() {
Expand Down Expand Up @@ -365,6 +428,11 @@ function patch (fs) {
}
}

// If the real `fs.close` was getter-only and could not be assigned,
// still wrap close on this lookalike so EMFILE retries keep working.
patchClose(fs)
patchCloseSync(fs)

return fs
}

Expand Down
84 changes: 84 additions & 0 deletions test/close-getter-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
var fs = require('fs')
var path = require('path')
var test = require('tap').test
var origClose = fs.close
var origCloseSync = fs.closeSync
var globalPatch = !!process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH
var self = path.resolve(__filename)

function defineGetterOnly (name, fn, configurable) {
Object.defineProperty(fs, name, {
get: function () { return fn },
enumerable: true,
configurable: configurable
})
}

if (process.argv.indexOf('--nonconfig') !== -1) {
defineGetterOnly('close', origClose, false)
defineGetterOnly('closeSync', origCloseSync, false)

test('non-configurable getter-only close does not throw', function (t) {
var gfs
t.doesNotThrow(function () {
gfs = require('../')
})
t.equal(fs.close, origClose, 'real fs.close left unchanged')
t.equal(fs.closeSync, origCloseSync, 'real fs.closeSync left unchanged')
if (!globalPatch) {
t.match(gfs.close.toString(), /graceful-fs shared queue/,
'export close still patched')
t.match(gfs.closeSync.toString(), /graceful-fs shared queue/,
'export closeSync still patched')
}
t.end()
})
} else {
test('clone materializes getter-only properties as writable', function (t) {
var clone = require('../clone.js')
var src = {}
var fn = function () { return 1 }
Object.defineProperty(src, 'close', {
get: function () { return fn },
enumerable: true
})
var copy = clone(src)
t.equal(copy.close, fn)
var replacement = function () { return 2 }
copy.close = replacement
t.equal(copy.close, replacement, 'clone close is writable')
t.end()
})

defineGetterOnly('close', origClose, true)
defineGetterOnly('closeSync', origCloseSync, true)

test('configurable getter-only close is patched', function (t) {
var gfs = require('../')
t.match(fs.close.toString(), /graceful-fs shared queue/, 'patch fs.close')
t.match(fs.closeSync.toString(), /graceful-fs shared queue/,
'patch fs.closeSync')
t.match(gfs.close.toString(), /graceful-fs shared queue/, 'patch gfs.close')
t.match(gfs.closeSync.toString(), /graceful-fs shared queue/,
'patch gfs.closeSync')

var fd = gfs.openSync(__filename, 'r')
t.doesNotThrow(function () { gfs.closeSync(fd) }, 'patched closeSync works')
gfs.open(__filename, 'r', function (er, fd2) {
t.error(er)
gfs.close(fd2, function (er2) {
t.error(er2, 'patched close works')
t.end()
})
})
})

test('non-configurable getter-only close', function (t) {
var env = {}
Object.keys(process.env).forEach(function (k) {
env[k] = process.env[k]
})
t.spawn(process.execPath, [self, '--nonconfig'], { env: env })
t.end()
})
}