var fs = require('fs') var wx = 'wx' if (process.version.match(/^v0.[456]/)) { var c = require('constants') wx = c.O_TRUNC | c.O_CREAT | c.O_WRONLY | c.O_EXCL } var locks = {} process.on('exit', function () { // cleanup Object.keys(locks).forEach(exports.unlockSync) }) // XXX https://github.com/joyent/node/issues/3555 // Remove when node 0.8 is deprecated. process.on('uncaughtException', function H (er) { var l = process.listeners('uncaughtException').filter(function (h) { return h !== H }) if (!l.length) { // cleanup Object.keys(locks).forEach(exports.unlockSync) process.removeListener('uncaughtException', H) throw er } }) exports.unlock = function (path, cb) { // best-effort. unlocking an already-unlocked lock is a noop fs.unlink(path, function (unlinkEr) { if (!locks.hasOwnProperty(path)) return cb() fs.close(locks[path], function (closeEr) { delete locks[path] cb() }) }) } exports.unlockSync = function (path) { try { fs.unlinkSync(path) } catch (er) {} if (!locks.hasOwnProperty(path)) return // best-effort. unlocking an already-unlocked lock is a noop try { fs.close(locks[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 = {} 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.ctime.getTime() return cb(er, age <= opts.stale) }) }) }) } exports.checkSync = function (path, opts) { opts = 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) { fs.closeSync(fd) 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.ctime.getTime() return (age <= opts.stale) } } exports.lock = function (path, opts, cb) { if (typeof opts === 'function') cb = opts, opts = {} if (typeof opts.retries === 'number' && opts.retries > 0) { cb = (function (orig) { return function (er, fd) { if (!er) return orig(er, fd) var newRT = opts.retries - 1 opts_ = Object.create(opts, { retries: { value: newRT }}) if (opts.retryWait) setTimeout(function() { exports.lock(path, opts_, orig) }, opts.retryWait) else exports.lock(path, opts_, orig) }})(cb) } // try to engage the lock. // if this succeeds, then we're in business. fs.open(path, wx, function (er, fd) { if (!er) { locks[path] = fd return cb(null, fd) } // 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) fs.stat(path, function (er, st) { if (er) { if (er.code === 'ENOENT') { // expired already! var opts_ = Object.create(opts, { stale: { value: false }}) exports.lock(path, opts_, cb) return } return cb(er) } var age = Date.now() - st.ctime.getTime() if (age > opts.stale) { exports.unlock(path, function (er) { if (er) return cb(er) var opts_ = Object.create(opts, { stale: { value: false }}) exports.lock(path, opts_, cb) }) } else notStale(er, path, opts, cb) }) else notStale(er, path, opts, cb) }) } function notStale (er, path, opts, cb) { if (typeof opts.wait === 'number' && opts.wait > 0) { // wait for some ms for the lock to clear var start = Date.now() var retried = false function retry () { if (retried) return retried = true // maybe already closed. try { watcher.close() } catch (e) {} clearTimeout(timer) var newWait = Date.now() - start var opts_ = Object.create(opts, { wait: { value: newWait }}) exports.lock(path, opts_, cb) } try { var watcher = fs.watch(path, function (change) { if (change === 'rename') { // ok, try and get it now. // if this fails, then continue waiting, maybe. retry() } }) watcher.on('error', function (er) { // usually means it expired before the watcher spotted it retry() }) } catch (er) { retry() } var timer = setTimeout(function () { try { watcher.close() } catch (e) {} cb(er) }, opts.wait) } else { // failed to lock! return cb(er) } } exports.lockSync = function (path, opts) { opts = 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 return fd } catch (er) { if (er.code !== 'EEXIST') return retryThrow(path, opts, er) if (opts.stale) { var st = fs.statSync(path) var age = Date.now() - st.ctime.getTime() if (age > opts.stale) { exports.unlockSync(path) return exports.lockSync(path, opts) } } // failed to lock! return retryThrow(path, opts, er) } } function retryThrow (path, opts, er) { if (typeof opts.retries === 'number' && opts.retries > 0) { var newRT = opts.retries - 1 var opts_ = Object.create(opts, { retries: { value: newRT }}) return exports.lockSync(path, opts_) } throw er }