pax_global_header00006660000000000000000000000064122050416130014504gustar00rootroot0000000000000052 comment=f36e1d67c971de008f5411808b955a5bb61f3e2c lockfile-0.4.1/000077500000000000000000000000001220504161300132765ustar00rootroot00000000000000lockfile-0.4.1/LICENSE000066400000000000000000000024361220504161300143100ustar00rootroot00000000000000Copyright (c) Isaac Z. Schlueter ("Author") All rights reserved. The BSD License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. lockfile-0.4.1/README.md000066400000000000000000000036161220504161300145630ustar00rootroot00000000000000# lockfile A very polite lock file utility, which endeavors to not litter, and to wait patiently for others. ## Usage ```javascript var lockFile = require('lockfile') // opts is optional, and defaults to {} lockFile.lock('some-file.lock', opts, function (er) { // if the er happens, then it failed to acquire a lock. // if there was not an error, then the file was created, // and won't be deleted until we unlock it. // do my stuff, free of interruptions // then, some time later, do: lockFile.unlock('some-file.lock', function (er) { // er means that an error happened, and is probably bad. }) }) ``` ## Methods Sync methods return the value/throw the error, others don't. Standard node fs stuff. All known locks are removed when the process exits. Of course, it's possible for certain types of failures to cause this to fail, but a best effort is made to not be a litterbug. ### lockFile.lock(path, [opts], cb) Acquire a file lock on the specified path ### lockFile.lockSync(path, [opts]) Acquire a file lock on the specified path ### lockFile.unlock(path, cb) Close and unlink the lockfile. ### lockFile.unlockSync(path) Close and unlink the lockfile. ### lockFile.check(path, [opts], cb) Check if the lockfile is locked and not stale. Returns boolean. ### lockFile.checkSync(path, [opts], cb) Check if the lockfile is locked and not stale. Callback is called with `cb(error, isLocked)`. ## Options ### opts.wait A number of milliseconds to wait for locks to expire before giving up. Only used by lockFile.lock. Relies on fs.watch. If the lock is not cleared by the time the wait expires, then it returns with the original error. ### opts.stale A number of milliseconds before locks are considered to have expired. ### opts.retries Used by lock and lockSync. Retry `n` number of times before giving up. ### opts.retryWait Used by lock. Wait `n` milliseconds before retrying. lockfile-0.4.1/lockfile.js000066400000000000000000000160601220504161300154270ustar00rootroot00000000000000var 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 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() }) } 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.ctime.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.ctime.getTime() return (age <= opts.stale) } } var req = 0 exports.lock = function (path, opts, cb) { if (typeof opts === 'function') cb = opts, opts = {} opts.req = opts.req || req++ debug('lock', path, 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 }}) debug('lock retry', path, 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) { 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) fs.stat(path, function (statEr, st) { if (statEr) { if (statEr.code === 'ENOENT') { // expired already! var opts_ = Object.create(opts, { stale: { value: false }}) debug('lock stale enoent retry', path, opts_) exports.lock(path, opts_, cb) return } return cb(statEr) } var age = Date.now() - st.ctime.getTime() if (age > opts.stale) { debug('lock stale', path, opts_) exports.unlock(path, function (er) { if (er) return cb(er) var opts_ = Object.create(opts, { stale: { value: false }}) debug('lock stale retry', path, opts_) exports.lock(path, opts_, cb) }) } else notStale(er, path, opts, cb) }) else notStale(er, path, opts, 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) // console.error('wait', path, opts.wait) // wait for some ms for the lock to clear var start = Date.now() var end = start + opts.wait function retry () { debug('notStale retry', path, opts) var now = Date.now() var newWait = end - now var newOpts = Object.create(opts, { wait: { value: newWait }}) exports.lock(path, newOpts, cb) } var timer = setTimeout(retry, 100) } 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.ctime.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) var opts_ = Object.create(opts, { retries: { value: newRT }}) return exports.lockSync(path, opts_) } throw er } lockfile-0.4.1/package.json000066400000000000000000000011641220504161300155660ustar00rootroot00000000000000{ "name": "lockfile", "version": "0.4.1", "main": "lockfile.js", "directories": { "test": "test" }, "dependencies": {}, "devDependencies": { "tap": "~0.2.5", "touch": "0" }, "scripts": { "test": "tap test/*.js" }, "repository": { "type": "git", "url": "git://github.com/isaacs/lockfile" }, "keywords": [ "lockfile", "lock", "file", "fs", "O_EXCL" ], "author": "Isaac Z. Schlueter (http://blog.izs.me/)", "license": "BSD", "description": "A very polite lock file utility, which endeavors to not litter, and to wait patiently for others." } lockfile-0.4.1/test/000077500000000000000000000000001220504161300142555ustar00rootroot00000000000000lockfile-0.4.1/test/basic.js000066400000000000000000000163351220504161300157040ustar00rootroot00000000000000var test = require('tap').test var lockFile = require('../lockfile.js') var path = require('path') var fs = require('fs') var touch = require('touch') test('setup', function (t) { try { lockFile.unlockSync('basic-lock') } catch (er) {} try { lockFile.unlockSync('sync-lock') } catch (er) {} try { lockFile.unlockSync('never-forget') } catch (er) {} try { lockFile.unlockSync('stale-lock') } catch (er) {} try { lockFile.unlockSync('watch-lock') } catch (er) {} try { lockFile.unlockSync('retry-lock') } catch (er) {} try { lockFile.unlockSync('contentious-lock') } catch (er) {} try { lockFile.unlockSync('stale-wait-lock') } catch (er) {} t.end() }) test('lock contention', function (t) { var gotlocks = 0; var N = 200 var delay = 10 // allow for some time for each lock acquisition and release. // note that raising N higher will mean that the overhead // increases, because we're creating more and more watchers. // irl, you should never have several hundred contenders for a // single lock, so this situation is somewhat pathological. var overhead = 200 var wait = N * overhead + delay // first make it locked, so that everyone has to wait lockFile.lock('contentious-lock', function(er, lock) { t.ifError(er, 'acquiring starter') if (er) throw er; t.pass('acquired starter lock') setTimeout(function() { lockFile.unlock('contentious-lock', function (er) { t.ifError(er, 'unlocking starter') if (er) throw er t.pass('unlocked starter') }) }, delay) }) for (var i=0; i < N; i++) lockFile.lock('contentious-lock', { wait: wait }, function(er, lock) { if (er) throw er; lockFile.unlock('contentious-lock', function(er) { if (er) throw er gotlocks++ t.pass('locked and unlocked #' + gotlocks) if (gotlocks === N) { t.pass('got all locks') t.end() } }) }) }) test('basic test', function (t) { lockFile.check('basic-lock', function (er, locked) { if (er) throw er t.notOk(locked) lockFile.lock('basic-lock', function (er) { if (er) throw er lockFile.lock('basic-lock', function (er) { t.ok(er) lockFile.check('basic-lock', function (er, locked) { if (er) throw er t.ok(locked) lockFile.unlock('basic-lock', function (er) { if (er) throw er lockFile.check('basic-lock', function (er, locked) { if (er) throw er t.notOk(locked) t.end() }) }) }) }) }) }) }) test('sync test', function (t) { var locked locked = lockFile.checkSync('sync-lock') t.notOk(locked) lockFile.lockSync('sync-lock') locked = lockFile.checkSync('sync-lock') t.ok(locked) lockFile.unlockSync('sync-lock') locked = lockFile.checkSync('sync-lock') t.notOk(locked) t.end() }) test('exit cleanup test', function (t) { var child = require.resolve('./fixtures/child.js') var node = process.execPath var spawn = require('child_process').spawn spawn(node, [child]).on('exit', function () { setTimeout(function () { var locked = lockFile.checkSync('never-forget') t.notOk(locked) t.end() }, 100) }) }) test('error exit cleanup test', function (t) { var child = require.resolve('./fixtures/bad-child.js') var node = process.execPath var spawn = require('child_process').spawn spawn(node, [child]).on('exit', function () { setTimeout(function () { var locked = lockFile.checkSync('never-forget') t.notOk(locked) t.end() }, 100) }) }) test('staleness test', function (t) { lockFile.lock('stale-lock', function (er) { if (er) throw er var opts = { stale: 1 } setTimeout(next, 1000) function next () { lockFile.check('stale-lock', opts, function (er, locked) { if (er) throw er t.notOk(locked) lockFile.lock('stale-lock', opts, function (er) { if (er) throw er lockFile.unlock('stale-lock', function (er) { if (er) throw er t.end() }) }) }) } }) }) test('staleness sync test', function (t) { var opts = { stale: 1 } lockFile.lockSync('stale-lock') setTimeout(next, 1000) function next () { var locked locked = lockFile.checkSync('stale-lock', opts) t.notOk(locked) lockFile.lockSync('stale-lock', opts) lockFile.unlockSync('stale-lock') t.end() } }) test('retries', function (t) { // next 5 opens will fail. var opens = 5 fs._open = fs.open fs.open = function (path, mode, cb) { if (--opens === 0) { fs.open = fs._open return fs.open(path, mode, cb) } var er = new Error('bogus') // to be, or not to be, that is the question. er.code = opens % 2 ? 'EEXIST' : 'ENOENT' process.nextTick(cb.bind(null, er)) } lockFile.lock('retry-lock', { retries: opens }, function (er) { if (er) throw er t.equal(opens, 0) lockFile.unlockSync('retry-lock') t.end() }) }) test('retryWait', function (t) { // next 5 opens will fail. var opens = 5 fs._open = fs.open fs.open = function (path, mode, cb) { if (--opens === 0) { fs.open = fs._open return fs.open(path, mode, cb) } var er = new Error('bogus') // to be, or not to be, that is the question. er.code = opens % 2 ? 'EEXIST' : 'ENOENT' process.nextTick(cb.bind(null, er)) } var opts = { retries: opens, retryWait: 100 } lockFile.lock('retry-lock', opts, function (er) { if (er) throw er t.equal(opens, 0) lockFile.unlockSync('retry-lock') t.end() }) }) test('retry sync', function (t) { // next 5 opens will fail. var opens = 5 fs._openSync = fs.openSync fs.openSync = function (path, mode) { if (--opens === 0) { fs.openSync = fs._openSync return fs.openSync(path, mode) } var er = new Error('bogus') // to be, or not to be, that is the question. er.code = opens % 2 ? 'EEXIST' : 'ENOENT' throw er } var opts = { retries: opens } lockFile.lockSync('retry-lock', opts) t.equal(opens, 0) lockFile.unlockSync('retry-lock') t.end() }) test('wait and stale together', function (t) { // first locker. var interval lockFile.lock('stale-wait-lock', function(er) { // keep refreshing the lock, so we keep it forever interval = setInterval(function() { touch.sync('stale-wait-lock') }, 10) // try to get another lock. this must fail! var opt = { stale: 1000, wait: 2000 } lockFile.lock('stale-wait-lock', opt, function (er) { if (!er) t.fail('got second lock? that unpossible!') else t.pass('second lock failed, as i have foreseen it') clearInterval(interval) t.end() }) }) }) test('cleanup', function (t) { try { lockFile.unlockSync('basic-lock') } catch (er) {} try { lockFile.unlockSync('sync-lock') } catch (er) {} try { lockFile.unlockSync('never-forget') } catch (er) {} try { lockFile.unlockSync('stale-lock') } catch (er) {} try { lockFile.unlockSync('watch-lock') } catch (er) {} try { lockFile.unlockSync('retry-lock') } catch (er) {} try { lockFile.unlockSync('contentious-lock') } catch (er) {} try { lockFile.unlockSync('stale-wait-lock') } catch (er) {} t.end() }) lockfile-0.4.1/test/fixtures/000077500000000000000000000000001220504161300161265ustar00rootroot00000000000000lockfile-0.4.1/test/fixtures/bad-child.js000066400000000000000000000001561220504161300202750ustar00rootroot00000000000000var lockFile = require('../../lockfile.js') lockFile.lockSync('never-forget') throw new Error('waaaaaaaaa') lockfile-0.4.1/test/fixtures/child.js000066400000000000000000000001331220504161300175440ustar00rootroot00000000000000var lockFile = require('../../lockfile.js') lockFile.lock('never-forget', function () {})