312 lines
8.0 KiB
JavaScript
312 lines
8.0 KiB
JavaScript
|
var fs = require('fs')
|
||
|
|
||
|
var wx = 'wx'
|
||
|
if (process.version.match(/^v0\.[0-6]/)) {
|
||
|
var c = require('constants')
|
||
|
wx = c.O_TRUNC | c.O_CREAT | c.O_WRONLY | c.O_EXCL
|
||
|
}
|
||
|
|
||
|
var os = require('os')
|
||
|
exports.filetime = 'ctime'
|
||
|
if (os.platform() == "win32") {
|
||
|
exports.filetime = 'mtime'
|
||
|
}
|
||
|
|
||
|
var debug
|
||
|
var util = require('util')
|
||
|
if (util.debuglog)
|
||
|
debug = util.debuglog('LOCKFILE')
|
||
|
else if (/\blockfile\b/i.test(process.env.NODE_DEBUG))
|
||
|
debug = function() {
|
||
|
var msg = util.format.apply(util, arguments)
|
||
|
console.error('LOCKFILE %d %s', process.pid, msg)
|
||
|
}
|
||
|
else
|
||
|
debug = function() {}
|
||
|
|
||
|
var locks = {}
|
||
|
|
||
|
function hasOwnProperty (obj, prop) {
|
||
|
return Object.prototype.hasOwnProperty.call(obj, prop)
|
||
|
}
|
||
|
|
||
|
process.on('exit', function () {
|
||
|
debug('exit listener')
|
||
|
// cleanup
|
||
|
Object.keys(locks).forEach(exports.unlockSync)
|
||
|
})
|
||
|
|
||
|
// XXX https://github.com/joyent/node/issues/3555
|
||
|
// Remove when node 0.8 is deprecated.
|
||
|
if (/^v0\.[0-8]\./.test(process.version)) {
|
||
|
debug('uncaughtException, version = %s', process.version)
|
||
|
process.on('uncaughtException', function H (er) {
|
||
|
debug('uncaughtException')
|
||
|
var l = process.listeners('uncaughtException').filter(function (h) {
|
||
|
return h !== H
|
||
|
})
|
||
|
if (!l.length) {
|
||
|
// cleanup
|
||
|
try { Object.keys(locks).forEach(exports.unlockSync) } catch (e) {}
|
||
|
process.removeListener('uncaughtException', H)
|
||
|
throw er
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
exports.unlock = function (path, cb) {
|
||
|
debug('unlock', path)
|
||
|
// best-effort. unlocking an already-unlocked lock is a noop
|
||
|
delete locks[path]
|
||
|
fs.unlink(path, function (unlinkEr) { cb && cb() })
|
||
|
}
|
||
|
|
||
|
exports.unlockSync = function (path) {
|
||
|
debug('unlockSync', path)
|
||
|
// best-effort. unlocking an already-unlocked lock is a noop
|
||
|
try { fs.unlinkSync(path) } catch (er) {}
|
||
|
delete locks[path]
|
||
|
}
|
||
|
|
||
|
|
||
|
// if the file can be opened in readonly mode, then it's there.
|
||
|
// if the error is something other than ENOENT, then it's not.
|
||
|
exports.check = function (path, opts, cb) {
|
||
|
if (typeof opts === 'function') cb = opts, opts = {}
|
||
|
debug('check', path, opts)
|
||
|
fs.open(path, 'r', function (er, fd) {
|
||
|
if (er) {
|
||
|
if (er.code !== 'ENOENT') return cb(er)
|
||
|
return cb(null, false)
|
||
|
}
|
||
|
|
||
|
if (!opts.stale) {
|
||
|
return fs.close(fd, function (er) {
|
||
|
return cb(er, true)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
fs.fstat(fd, function (er, st) {
|
||
|
if (er) return fs.close(fd, function (er2) {
|
||
|
return cb(er)
|
||
|
})
|
||
|
|
||
|
fs.close(fd, function (er) {
|
||
|
var age = Date.now() - st[exports.filetime].getTime()
|
||
|
return cb(er, age <= opts.stale)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
exports.checkSync = function (path, opts) {
|
||
|
opts = opts || {}
|
||
|
debug('checkSync', path, opts)
|
||
|
if (opts.wait) {
|
||
|
throw new Error('opts.wait not supported sync for obvious reasons')
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
var fd = fs.openSync(path, 'r')
|
||
|
} catch (er) {
|
||
|
if (er.code !== 'ENOENT') throw er
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if (!opts.stale) {
|
||
|
try { fs.closeSync(fd) } catch (er) {}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// file exists. however, might be stale
|
||
|
if (opts.stale) {
|
||
|
try {
|
||
|
var st = fs.fstatSync(fd)
|
||
|
} finally {
|
||
|
fs.closeSync(fd)
|
||
|
}
|
||
|
var age = Date.now() - st[exports.filetime].getTime()
|
||
|
return (age <= opts.stale)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
var req = 1
|
||
|
exports.lock = function (path, opts, cb) {
|
||
|
if (typeof opts === 'function') cb = opts, opts = {}
|
||
|
opts.req = opts.req || req++
|
||
|
debug('lock', path, opts)
|
||
|
opts.start = opts.start || Date.now()
|
||
|
|
||
|
if (typeof opts.retries === 'number' && opts.retries > 0) {
|
||
|
debug('has retries', opts.retries)
|
||
|
var retries = opts.retries
|
||
|
opts.retries = 0
|
||
|
cb = (function (orig) { return function cb (er, fd) {
|
||
|
debug('retry-mutated callback')
|
||
|
retries -= 1
|
||
|
if (!er || retries < 0) return orig(er, fd)
|
||
|
|
||
|
debug('lock retry', path, opts)
|
||
|
|
||
|
if (opts.retryWait) setTimeout(retry, opts.retryWait)
|
||
|
else retry()
|
||
|
|
||
|
function retry () {
|
||
|
opts.start = Date.now()
|
||
|
debug('retrying', opts.start)
|
||
|
exports.lock(path, opts, cb)
|
||
|
}
|
||
|
}})(cb)
|
||
|
}
|
||
|
|
||
|
// try to engage the lock.
|
||
|
// if this succeeds, then we're in business.
|
||
|
fs.open(path, wx, function (er, fd) {
|
||
|
if (!er) {
|
||
|
debug('locked', path, fd)
|
||
|
locks[path] = fd
|
||
|
return fs.close(fd, function () {
|
||
|
return cb()
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// something other than "currently locked"
|
||
|
// maybe eperm or something.
|
||
|
if (er.code !== 'EEXIST') return cb(er)
|
||
|
|
||
|
// someone's got this one. see if it's valid.
|
||
|
if (!opts.stale) return notStale(er, path, opts, cb)
|
||
|
|
||
|
return maybeStale(er, path, opts, false, cb)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
|
||
|
// Staleness checking algorithm
|
||
|
// 1. acquire $lock, fail
|
||
|
// 2. stat $lock, find that it is stale
|
||
|
// 3. acquire $lock.STALE
|
||
|
// 4. stat $lock, assert that it is still stale
|
||
|
// 5. unlink $lock
|
||
|
// 6. link $lock.STALE $lock
|
||
|
// 7. unlink $lock.STALE
|
||
|
// On any failure, clean up whatever we've done, and raise the error.
|
||
|
function maybeStale (originalEr, path, opts, hasStaleLock, cb) {
|
||
|
fs.stat(path, function (statEr, st) {
|
||
|
if (statEr) {
|
||
|
if (statEr.code === 'ENOENT') {
|
||
|
// expired already!
|
||
|
opts.stale = false
|
||
|
debug('lock stale enoent retry', path, opts)
|
||
|
exports.lock(path, opts, cb)
|
||
|
return
|
||
|
}
|
||
|
return cb(statEr)
|
||
|
}
|
||
|
|
||
|
var age = Date.now() - st[exports.filetime].getTime()
|
||
|
if (age <= opts.stale) return notStale(originalEr, path, opts, cb)
|
||
|
|
||
|
debug('lock stale', path, opts)
|
||
|
if (hasStaleLock) {
|
||
|
exports.unlock(path, function (er) {
|
||
|
if (er) return cb(er)
|
||
|
debug('lock stale retry', path, opts)
|
||
|
fs.link(path + '.STALE', path, function (er) {
|
||
|
fs.unlink(path + '.STALE', function () {
|
||
|
// best effort. if the unlink fails, oh well.
|
||
|
cb(er)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
} else {
|
||
|
debug('acquire .STALE file lock', opts)
|
||
|
exports.lock(path + '.STALE', opts, function (er) {
|
||
|
if (er) return cb(er)
|
||
|
maybeStale(originalEr, path, opts, true, cb)
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function notStale (er, path, opts, cb) {
|
||
|
debug('notStale', path, opts)
|
||
|
|
||
|
// if we can't wait, then just call it a failure
|
||
|
if (typeof opts.wait !== 'number' || opts.wait <= 0)
|
||
|
return cb(er)
|
||
|
|
||
|
// poll for some ms for the lock to clear
|
||
|
var now = Date.now()
|
||
|
var start = opts.start || now
|
||
|
var end = start + opts.wait
|
||
|
|
||
|
if (end <= now)
|
||
|
return cb(er)
|
||
|
|
||
|
debug('now=%d, wait until %d (delta=%d)', start, end, end-start)
|
||
|
var wait = Math.min(end - start, opts.pollPeriod || 100)
|
||
|
var timer = setTimeout(poll, wait)
|
||
|
|
||
|
function poll () {
|
||
|
debug('notStale, polling', path, opts)
|
||
|
exports.lock(path, opts, cb)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.lockSync = function (path, opts) {
|
||
|
opts = opts || {}
|
||
|
opts.req = opts.req || req++
|
||
|
debug('lockSync', path, opts)
|
||
|
if (opts.wait || opts.retryWait) {
|
||
|
throw new Error('opts.wait not supported sync for obvious reasons')
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
var fd = fs.openSync(path, wx)
|
||
|
locks[path] = fd
|
||
|
try { fs.closeSync(fd) } catch (er) {}
|
||
|
debug('locked sync!', path, fd)
|
||
|
return
|
||
|
} catch (er) {
|
||
|
if (er.code !== 'EEXIST') return retryThrow(path, opts, er)
|
||
|
|
||
|
if (opts.stale) {
|
||
|
var st = fs.statSync(path)
|
||
|
var ct = st[exports.filetime].getTime()
|
||
|
if (!(ct % 1000) && (opts.stale % 1000)) {
|
||
|
// probably don't have subsecond resolution.
|
||
|
// round up the staleness indicator.
|
||
|
// Yes, this will be wrong 1/1000 times on platforms
|
||
|
// with subsecond stat precision, but that's acceptable
|
||
|
// in exchange for not mistakenly removing locks on
|
||
|
// most other systems.
|
||
|
opts.stale = 1000 * Math.ceil(opts.stale / 1000)
|
||
|
}
|
||
|
var age = Date.now() - ct
|
||
|
if (age > opts.stale) {
|
||
|
debug('lockSync stale', path, opts, age)
|
||
|
exports.unlockSync(path)
|
||
|
return exports.lockSync(path, opts)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// failed to lock!
|
||
|
debug('failed to lock', path, opts, er)
|
||
|
return retryThrow(path, opts, er)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function retryThrow (path, opts, er) {
|
||
|
if (typeof opts.retries === 'number' && opts.retries > 0) {
|
||
|
var newRT = opts.retries - 1
|
||
|
debug('retryThrow', path, opts, newRT)
|
||
|
opts.retries = newRT
|
||
|
return exports.lockSync(path, opts)
|
||
|
}
|
||
|
throw er
|
||
|
}
|
||
|
|