├── .gitignore ├── test ├── fixtures │ ├── child.js │ └── bad-child.js ├── unlock-no-cb.js ├── retry-time.js ├── stale-contention.js └── basic.js ├── .travis.yml ├── gen-changelog.sh ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md └── lockfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /test/fixtures/child.js: -------------------------------------------------------------------------------- 1 | var lockFile = require('../../lockfile.js') 2 | 3 | lockFile.lock('never-forget', function () {}) 4 | -------------------------------------------------------------------------------- /test/fixtures/bad-child.js: -------------------------------------------------------------------------------- 1 | var lockFile = require('../../lockfile.js') 2 | 3 | lockFile.lockSync('never-forget') 4 | 5 | throw new Error('waaaaaaaaa') 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "10" 5 | - "8" 6 | - "6" 7 | - "11" 8 | notifications: 9 | email: false 10 | cache: 11 | directories: 12 | - $HOME/.npm 13 | -------------------------------------------------------------------------------- /gen-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ( 3 | echo '# Changes' 4 | echo '' 5 | git log --first-parent --pretty=format:'%s' \ 6 | | grep -v '^update changelog' \ 7 | | perl -p -e 's/^((v?[0-9]+\.?)+)$/\n## \1\n/g' \ 8 | | perl -p -e 's/^([^#\s].*)$/* \1/g' 9 | )> CHANGELOG.md 10 | -------------------------------------------------------------------------------- /test/unlock-no-cb.js: -------------------------------------------------------------------------------- 1 | var t = require('tap') 2 | if (/0\.(10|8)/.test(process.version)) { 3 | t.pass('just a dummy test, no beforeExit in this node version') 4 | } else { 5 | process.on('beforeExit', function (code) { 6 | t.equal(code, 0, 'did not throw') 7 | }) 8 | } 9 | var lf = require('../lockfile.js') 10 | lf.unlock('no-file-no-cb') 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lockfile", 3 | "version": "1.0.4", 4 | "main": "lockfile.js", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "dependencies": { 9 | "signal-exit": "^3.0.2" 10 | }, 11 | "devDependencies": { 12 | "tap": "^12.4.0", 13 | "touch": "^3.1.0" 14 | }, 15 | "scripts": { 16 | "test": "tap test/*.js --cov -J", 17 | "changelog": "bash gen-changelog.sh", 18 | "postversion": "npm run changelog && git add CHANGELOG.md && git commit -m 'update changelog - '${npm_package_version}" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/npm/lockfile.git" 23 | }, 24 | "keywords": [ 25 | "lockfile", 26 | "lock", 27 | "file", 28 | "fs", 29 | "O_EXCL" 30 | ], 31 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 32 | "license": "ISC", 33 | "description": "A very polite lock file utility, which endeavors to not litter, and to wait patiently for others." 34 | } 35 | -------------------------------------------------------------------------------- /test/retry-time.js: -------------------------------------------------------------------------------- 1 | // In these tests, we do the following: 2 | // try for 200ms (rt=2) 3 | // wait for 300ms 4 | // try for 200ms (rt=1) 5 | // wait for 300ms 6 | // try for 200ms (rt=0) 7 | // fail after 1200 8 | // Actual time will be more like 1220-ish for setTimeout irregularity 9 | // But it should NOT be as slow as 2000. 10 | 11 | var lockFile = require('../') 12 | var touch = require('touch') 13 | var test = require('tap').test 14 | var fs = require('fs') 15 | 16 | var RETRYWAIT = 100 17 | var WAIT = 100 18 | var RETRIES = 2 19 | var EXPECTTIME = (RETRYWAIT * RETRIES) + (WAIT * (RETRIES + 1)) 20 | var TOOLONG = EXPECTTIME * 1.5 21 | 22 | test('setup', function (t) { 23 | touch.sync('file.lock') 24 | t.end() 25 | }) 26 | 27 | var pollPeriods = [10, 100, 10000] 28 | pollPeriods.forEach(function (pp) { 29 | test('retry+wait, poll=' + pp, function (t) { 30 | var ended = false 31 | var timer = setTimeout(function() { 32 | t.fail('taking too long!') 33 | ended = true 34 | t.end() 35 | }, 2000) 36 | 37 | if (timer.unref) 38 | timer.unref() 39 | 40 | var start = Date.now() 41 | lockFile.lock('file.lock', { 42 | wait: WAIT, 43 | retries: RETRIES, 44 | retryWait: RETRYWAIT, 45 | pollPeriod: pp 46 | }, function (er) { 47 | if (ended) return 48 | var time = Date.now() - start 49 | t.ok(time >= EXPECTTIME, 'should take at least ' + EXPECTTIME) 50 | t.ok(time < TOOLONG, 'should take less than ' + TOOLONG) 51 | clearTimeout(timer) 52 | t.end() 53 | }) 54 | }) 55 | }) 56 | 57 | test('cleanup', function (t) { 58 | fs.unlinkSync('file.lock') 59 | t.end() 60 | var timer = setTimeout(function() { 61 | process.exit(1) 62 | }, 500) 63 | if (timer.unref) 64 | timer.unref() 65 | else 66 | clearTimeout(timer) 67 | }) 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | 4 | ## 1.0.4 5 | 6 | * test parallel 7 | * upgrade tap 8 | * upgrade node versions in travis.yml 9 | * Use signal-exit package to detect exit instead of process.on('exit') 10 | * added some debugging lines 11 | 12 | ## v1.0.3 13 | 14 | * handle the case where callback is not passed by user 15 | 16 | ## v1.0.2 17 | 18 | * git ignore coverage and node_modules 19 | * update tap to v7 20 | * build a changelog 21 | * package: fix repository link 22 | * pass tests on 0.8 23 | * before_script needs to be before_install 24 | * tap 1.2.0 and travis 25 | 26 | ## v1.0.1 27 | 28 | * isc license 29 | * updated README.md 30 | 31 | ## v1.0.0 32 | 33 | * Simulate staleness instead of waiting excessively 34 | * whitespace 35 | * manage 'retries' so it does not clash with 'wait' polling 36 | * manage 'wait' timer properly 37 | * Get rid of the excessive Object.create opts shadowing stuff 38 | * failing test for the time taken for retries + wait options 39 | * doc: add pollPeriod, correct opts.wait 40 | * Fixed #6: polling period should be configurable 41 | 42 | ## v0.4.3 43 | 44 | * Implement race-resistant stale lock detection 45 | * set req id to 1 to start out 46 | 47 | ## v0.4.2 48 | 49 | * stale option fix for windows file tunneling 50 | 51 | ## v0.4.1 52 | 53 | * Fix version parsing 54 | 55 | ## v0.4.0 56 | 57 | * Don't keep lockfiles open 58 | 59 | ## v0.3.4 60 | 61 | * retry more aggressively 62 | 63 | ## v0.3.3 64 | 65 | * Add debugging function 66 | 67 | ## v0.3.2 68 | 69 | * remove console.error 70 | 71 | ## v0.3.1 72 | 73 | * Support lack of subsecond fs precision 74 | * Fix error closure overwriting in notStale 75 | 76 | ## v0.3.0 77 | 78 | * Use polling instead of watchers 79 | * Add more overhead buffer to contention test 80 | 81 | ## v0.2.2 82 | 83 | * Fix wait calculation 84 | * fixup 85 | * Style: prefer early return to giant if/else 86 | * unlock: Close before unlinking 87 | * Don't get tripped up by locks named 'hasOwnProperty' 88 | * test: Pathological extreme lock contention 89 | * refactor license 90 | 91 | ## 0.2.1 92 | 93 | * Handle race conditions more thoroughly 94 | 95 | ## 0.2.0 96 | 97 | * Rename to 'lockfile' 98 | 99 | ## 0.0.2 100 | 101 | * Add retries 102 | * bsd 103 | 104 | ## 0.0.1 105 | 106 | * tests 107 | * package.json 108 | * the code 109 | * first 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lockfile 2 | 3 | A very polite lock file utility, which endeavors to not litter, and to 4 | wait patiently for others. 5 | 6 | ## Usage 7 | 8 | ```javascript 9 | var lockFile = require('lockfile') 10 | 11 | // opts is optional, and defaults to {} 12 | lockFile.lock('some-file.lock', opts, function (er) { 13 | // if the er happens, then it failed to acquire a lock. 14 | // if there was not an error, then the file was created, 15 | // and won't be deleted until we unlock it. 16 | 17 | // do my stuff, free of interruptions 18 | // then, some time later, do: 19 | lockFile.unlock('some-file.lock', function (er) { 20 | // er means that an error happened, and is probably bad. 21 | }) 22 | }) 23 | ``` 24 | 25 | ## Methods 26 | 27 | Sync methods return the value/throw the error, others don't. Standard 28 | node fs stuff. 29 | 30 | All known locks are removed when the process exits. Of course, it's 31 | possible for certain types of failures to cause this to fail, but a best 32 | effort is made to not be a litterbug. 33 | 34 | ### lockFile.lock(path, [opts], cb) 35 | 36 | Acquire a file lock on the specified path 37 | 38 | ### lockFile.lockSync(path, [opts]) 39 | 40 | Acquire a file lock on the specified path 41 | 42 | ### lockFile.unlock(path, cb) 43 | 44 | Close and unlink the lockfile. 45 | 46 | ### lockFile.unlockSync(path) 47 | 48 | Close and unlink the lockfile. 49 | 50 | ### lockFile.check(path, [opts], cb) 51 | 52 | Check if the lockfile is locked and not stale. 53 | 54 | Callback is called with `cb(error, isLocked)`. 55 | 56 | ### lockFile.checkSync(path, [opts]) 57 | 58 | Check if the lockfile is locked and not stale. 59 | 60 | Returns boolean. 61 | 62 | ## Options 63 | 64 | ### opts.wait 65 | 66 | A number of milliseconds to wait for locks to expire before giving up. 67 | Only used by lockFile.lock. Poll for `opts.wait` ms. If the lock is 68 | not cleared by the time the wait expires, then it returns with the 69 | original error. 70 | 71 | ### opts.pollPeriod 72 | 73 | When using `opts.wait`, this is the period in ms in which it polls to 74 | check if the lock has expired. Defaults to `100`. 75 | 76 | ### opts.stale 77 | 78 | A number of milliseconds before locks are considered to have expired. 79 | 80 | ### opts.retries 81 | 82 | Used by lock and lockSync. Retry `n` number of times before giving up. 83 | 84 | ### opts.retryWait 85 | 86 | Used by lock. Wait `n` milliseconds before retrying. 87 | -------------------------------------------------------------------------------- /test/stale-contention.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var lockFile = require('../') 3 | var test = require('tap').test 4 | var path = require('path') 5 | var lock = path.resolve(__dirname, 'stale.lock') 6 | var touch = require('touch') 7 | var spawn = require('child_process').spawn 8 | var node = process.execPath 9 | 10 | // We're using a lockfile with an artificially old date, 11 | // so make it use that instead of ctime. 12 | // Probably you should never do this in production! 13 | lockFile.filetime = 'mtime' 14 | 15 | if (process.argv[2] === 'child') { 16 | return child() 17 | } 18 | 19 | function child () { 20 | // Make fs.stat take 100ms to return its data 21 | // This is important because, in a test scenario where 22 | // we're statting the same exact file rapid-fire like this, 23 | // it'll end up being cached by the FS, and never trigger 24 | // the race condition we're trying to expose. 25 | fs.stat = function (stat) { return function () { 26 | var args = [].slice.call(arguments) 27 | var cb = args.pop() 28 | stat.apply(fs, args.concat(function(er, st) { 29 | setTimeout(function () { 30 | cb(er, st) 31 | }, 100) 32 | })) 33 | }}(fs.stat) 34 | 35 | lockFile.lock(lock, { stale: 100000 }, function (er) { 36 | if (er && er.code !== 'EEXIST') 37 | throw er 38 | else if (er) 39 | process.exit(17) 40 | else 41 | setTimeout(function(){}, 500) 42 | }) 43 | } 44 | 45 | test('create stale file', function (t) { 46 | try { fs.unlinkSync(lock) } catch (er) {} 47 | touch.sync(lock, { time: '1979-07-01T19:10:00.000Z' }) 48 | t.end() 49 | }) 50 | 51 | test('contenders', function (t) { 52 | var n = 10 53 | var fails = 0 54 | var wins = 0 55 | var args = [ __filename, 'child' ] 56 | var opt = { stdio: [0, "pipe", 2] } 57 | for (var i = 0; i < n; i++) { 58 | spawn(node, args, opt).on('close', then) 59 | } 60 | 61 | function then (code) { 62 | if (code === 17) { 63 | fails ++ 64 | } else if (code) { 65 | t.fail("unexpected failure", code) 66 | fails ++ 67 | } else { 68 | wins ++ 69 | } 70 | if (fails + wins === n) { 71 | done() 72 | } 73 | } 74 | 75 | function done () { 76 | t.equal(wins, 1, "should have 1 lock winner") 77 | t.equal(fails, n - 1, "all others should lose") 78 | t.end() 79 | } 80 | }) 81 | 82 | test('remove stale file', function (t) { 83 | try { fs.unlinkSync(lock) } catch (er) {} 84 | t.end() 85 | }) 86 | -------------------------------------------------------------------------------- /lockfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | var wx = 'wx' 4 | if (process.version.match(/^v0\.[0-6]/)) { 5 | var c = require('constants') 6 | wx = c.O_TRUNC | c.O_CREAT | c.O_WRONLY | c.O_EXCL 7 | } 8 | 9 | var os = require('os') 10 | exports.filetime = 'ctime' 11 | if (os.platform() == "win32") { 12 | exports.filetime = 'mtime' 13 | } 14 | 15 | var debug 16 | var util = require('util') 17 | if (util.debuglog) 18 | debug = util.debuglog('LOCKFILE') 19 | else if (/\blockfile\b/i.test(process.env.NODE_DEBUG)) 20 | debug = function() { 21 | var msg = util.format.apply(util, arguments) 22 | console.error('LOCKFILE %d %s', process.pid, msg) 23 | } 24 | else 25 | debug = function() {} 26 | 27 | var locks = {} 28 | 29 | function hasOwnProperty (obj, prop) { 30 | return Object.prototype.hasOwnProperty.call(obj, prop) 31 | } 32 | 33 | var onExit = require('signal-exit') 34 | onExit(function () { 35 | debug('exit listener') 36 | // cleanup 37 | Object.keys(locks).forEach(exports.unlockSync) 38 | }) 39 | 40 | // XXX https://github.com/joyent/node/issues/3555 41 | // Remove when node 0.8 is deprecated. 42 | if (/^v0\.[0-8]\./.test(process.version)) { 43 | debug('uncaughtException, version = %s', process.version) 44 | process.on('uncaughtException', function H (er) { 45 | debug('uncaughtException') 46 | var l = process.listeners('uncaughtException').filter(function (h) { 47 | return h !== H 48 | }) 49 | if (!l.length) { 50 | // cleanup 51 | try { Object.keys(locks).forEach(exports.unlockSync) } catch (e) {} 52 | process.removeListener('uncaughtException', H) 53 | throw er 54 | } 55 | }) 56 | } 57 | 58 | exports.unlock = function (path, cb) { 59 | debug('unlock', path) 60 | // best-effort. unlocking an already-unlocked lock is a noop 61 | delete locks[path] 62 | fs.unlink(path, function (unlinkEr) { cb && cb() }) 63 | } 64 | 65 | exports.unlockSync = function (path) { 66 | debug('unlockSync', path) 67 | // best-effort. unlocking an already-unlocked lock is a noop 68 | try { fs.unlinkSync(path) } catch (er) {} 69 | delete locks[path] 70 | } 71 | 72 | 73 | // if the file can be opened in readonly mode, then it's there. 74 | // if the error is something other than ENOENT, then it's not. 75 | exports.check = function (path, opts, cb) { 76 | if (typeof opts === 'function') cb = opts, opts = {} 77 | debug('check', path, opts) 78 | fs.open(path, 'r', function (er, fd) { 79 | if (er) { 80 | if (er.code !== 'ENOENT') return cb(er) 81 | return cb(null, false) 82 | } 83 | 84 | if (!opts.stale) { 85 | return fs.close(fd, function (er) { 86 | return cb(er, true) 87 | }) 88 | } 89 | 90 | fs.fstat(fd, function (er, st) { 91 | if (er) return fs.close(fd, function (er2) { 92 | return cb(er) 93 | }) 94 | 95 | fs.close(fd, function (er) { 96 | var age = Date.now() - st[exports.filetime].getTime() 97 | return cb(er, age <= opts.stale) 98 | }) 99 | }) 100 | }) 101 | } 102 | 103 | exports.checkSync = function (path, opts) { 104 | opts = opts || {} 105 | debug('checkSync', path, opts) 106 | if (opts.wait) { 107 | throw new Error('opts.wait not supported sync for obvious reasons') 108 | } 109 | 110 | try { 111 | var fd = fs.openSync(path, 'r') 112 | } catch (er) { 113 | if (er.code !== 'ENOENT') throw er 114 | return false 115 | } 116 | 117 | if (!opts.stale) { 118 | try { fs.closeSync(fd) } catch (er) {} 119 | return true 120 | } 121 | 122 | // file exists. however, might be stale 123 | if (opts.stale) { 124 | try { 125 | var st = fs.fstatSync(fd) 126 | } finally { 127 | fs.closeSync(fd) 128 | } 129 | var age = Date.now() - st[exports.filetime].getTime() 130 | return (age <= opts.stale) 131 | } 132 | } 133 | 134 | 135 | 136 | var req = 1 137 | exports.lock = function (path, opts, cb) { 138 | if (typeof opts === 'function') cb = opts, opts = {} 139 | opts.req = opts.req || req++ 140 | debug('lock', path, opts) 141 | opts.start = opts.start || Date.now() 142 | 143 | if (typeof opts.retries === 'number' && opts.retries > 0) { 144 | debug('has retries', opts.retries) 145 | var retries = opts.retries 146 | opts.retries = 0 147 | cb = (function (orig) { return function cb (er, fd) { 148 | debug('retry-mutated callback') 149 | retries -= 1 150 | if (!er || retries < 0) return orig(er, fd) 151 | 152 | debug('lock retry', path, opts) 153 | 154 | if (opts.retryWait) setTimeout(retry, opts.retryWait) 155 | else retry() 156 | 157 | function retry () { 158 | opts.start = Date.now() 159 | debug('retrying', opts.start) 160 | exports.lock(path, opts, cb) 161 | } 162 | }})(cb) 163 | } 164 | 165 | // try to engage the lock. 166 | // if this succeeds, then we're in business. 167 | fs.open(path, wx, function (er, fd) { 168 | if (!er) { 169 | debug('locked', path, fd) 170 | locks[path] = fd 171 | return fs.close(fd, function () { 172 | return cb() 173 | }) 174 | } 175 | 176 | debug('failed to acquire lock', er) 177 | 178 | // something other than "currently locked" 179 | // maybe eperm or something. 180 | if (er.code !== 'EEXIST') { 181 | debug('not EEXIST error', er) 182 | return cb(er) 183 | } 184 | 185 | // someone's got this one. see if it's valid. 186 | if (!opts.stale) return notStale(er, path, opts, cb) 187 | 188 | return maybeStale(er, path, opts, false, cb) 189 | }) 190 | debug('lock return') 191 | } 192 | 193 | 194 | // Staleness checking algorithm 195 | // 1. acquire $lock, fail 196 | // 2. stat $lock, find that it is stale 197 | // 3. acquire $lock.STALE 198 | // 4. stat $lock, assert that it is still stale 199 | // 5. unlink $lock 200 | // 6. link $lock.STALE $lock 201 | // 7. unlink $lock.STALE 202 | // On any failure, clean up whatever we've done, and raise the error. 203 | function maybeStale (originalEr, path, opts, hasStaleLock, cb) { 204 | fs.stat(path, function (statEr, st) { 205 | if (statEr) { 206 | if (statEr.code === 'ENOENT') { 207 | // expired already! 208 | opts.stale = false 209 | debug('lock stale enoent retry', path, opts) 210 | exports.lock(path, opts, cb) 211 | return 212 | } 213 | return cb(statEr) 214 | } 215 | 216 | var age = Date.now() - st[exports.filetime].getTime() 217 | if (age <= opts.stale) return notStale(originalEr, path, opts, cb) 218 | 219 | debug('lock stale', path, opts) 220 | if (hasStaleLock) { 221 | exports.unlock(path, function (er) { 222 | if (er) return cb(er) 223 | debug('lock stale retry', path, opts) 224 | fs.link(path + '.STALE', path, function (er) { 225 | fs.unlink(path + '.STALE', function () { 226 | // best effort. if the unlink fails, oh well. 227 | cb(er) 228 | }) 229 | }) 230 | }) 231 | } else { 232 | debug('acquire .STALE file lock', opts) 233 | exports.lock(path + '.STALE', opts, function (er) { 234 | if (er) return cb(er) 235 | maybeStale(originalEr, path, opts, true, cb) 236 | }) 237 | } 238 | }) 239 | } 240 | 241 | function notStale (er, path, opts, cb) { 242 | debug('notStale', path, opts) 243 | 244 | // if we can't wait, then just call it a failure 245 | if (typeof opts.wait !== 'number' || opts.wait <= 0) { 246 | debug('notStale, wait is not a number') 247 | return cb(er) 248 | } 249 | 250 | // poll for some ms for the lock to clear 251 | var now = Date.now() 252 | var start = opts.start || now 253 | var end = start + opts.wait 254 | 255 | if (end <= now) 256 | return cb(er) 257 | 258 | debug('now=%d, wait until %d (delta=%d)', start, end, end-start) 259 | var wait = Math.min(end - start, opts.pollPeriod || 100) 260 | var timer = setTimeout(poll, wait) 261 | 262 | function poll () { 263 | debug('notStale, polling', path, opts) 264 | exports.lock(path, opts, cb) 265 | } 266 | } 267 | 268 | exports.lockSync = function (path, opts) { 269 | opts = opts || {} 270 | opts.req = opts.req || req++ 271 | debug('lockSync', path, opts) 272 | if (opts.wait || opts.retryWait) { 273 | throw new Error('opts.wait not supported sync for obvious reasons') 274 | } 275 | 276 | try { 277 | var fd = fs.openSync(path, wx) 278 | locks[path] = fd 279 | try { fs.closeSync(fd) } catch (er) {} 280 | debug('locked sync!', path, fd) 281 | return 282 | } catch (er) { 283 | if (er.code !== 'EEXIST') return retryThrow(path, opts, er) 284 | 285 | if (opts.stale) { 286 | var st = fs.statSync(path) 287 | var ct = st[exports.filetime].getTime() 288 | if (!(ct % 1000) && (opts.stale % 1000)) { 289 | // probably don't have subsecond resolution. 290 | // round up the staleness indicator. 291 | // Yes, this will be wrong 1/1000 times on platforms 292 | // with subsecond stat precision, but that's acceptable 293 | // in exchange for not mistakenly removing locks on 294 | // most other systems. 295 | opts.stale = 1000 * Math.ceil(opts.stale / 1000) 296 | } 297 | var age = Date.now() - ct 298 | if (age > opts.stale) { 299 | debug('lockSync stale', path, opts, age) 300 | exports.unlockSync(path) 301 | return exports.lockSync(path, opts) 302 | } 303 | } 304 | 305 | // failed to lock! 306 | debug('failed to lock', path, opts, er) 307 | return retryThrow(path, opts, er) 308 | } 309 | } 310 | 311 | function retryThrow (path, opts, er) { 312 | if (typeof opts.retries === 'number' && opts.retries > 0) { 313 | var newRT = opts.retries - 1 314 | debug('retryThrow', path, opts, newRT) 315 | opts.retries = newRT 316 | return exports.lockSync(path, opts) 317 | } 318 | throw er 319 | } 320 | 321 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var lockFile = require('../lockfile.js') 3 | var path = require('path') 4 | var fs = require('fs') 5 | var touch = require('touch') 6 | 7 | // On Unix systems, it uses ctime by default for staleness checks, since it's 8 | // the most reliable. However, because this test artificially sets some locks 9 | // to an earlier time to simulate staleness, we use mtime here. 10 | lockFile.filetime = 'mtime' 11 | 12 | test('setup', function (t) { 13 | try { lockFile.unlockSync('basic-lock') } catch (er) {} 14 | try { lockFile.unlockSync('sync-lock') } catch (er) {} 15 | try { lockFile.unlockSync('never-forget') } catch (er) {} 16 | try { lockFile.unlockSync('stale-lock') } catch (er) {} 17 | try { lockFile.unlockSync('watch-lock') } catch (er) {} 18 | try { lockFile.unlockSync('retry-lock') } catch (er) {} 19 | try { lockFile.unlockSync('contentious-lock') } catch (er) {} 20 | try { lockFile.unlockSync('stale-wait-lock') } catch (er) {} 21 | try { lockFile.unlockSync('stale-windows-lock') } catch (er) {} 22 | t.end() 23 | }) 24 | 25 | test('lock contention', function (t) { 26 | var gotlocks = 0; 27 | var N = 200 28 | var delay = 10 29 | // allow for some time for each lock acquisition and release. 30 | // note that raising N higher will mean that the overhead 31 | // increases, because we're creating more and more watchers. 32 | // irl, you should never have several hundred contenders for a 33 | // single lock, so this situation is somewhat pathological. 34 | var overhead = 200 35 | var wait = N * overhead + delay 36 | 37 | // first make it locked, so that everyone has to wait 38 | lockFile.lock('contentious-lock', function(er, lock) { 39 | t.ifError(er, 'acquiring starter') 40 | if (er) throw er; 41 | t.pass('acquired starter lock') 42 | setTimeout(function() { 43 | lockFile.unlock('contentious-lock', function (er) { 44 | t.ifError(er, 'unlocking starter') 45 | if (er) throw er 46 | t.pass('unlocked starter') 47 | }) 48 | }, delay) 49 | }) 50 | 51 | for (var i=0; i < N; i++) 52 | lockFile.lock('contentious-lock', { wait: wait }, function(er, lock) { 53 | if (er) throw er; 54 | lockFile.unlock('contentious-lock', function(er) { 55 | if (er) throw er 56 | gotlocks++ 57 | t.pass('locked and unlocked #' + gotlocks) 58 | if (gotlocks === N) { 59 | t.pass('got all locks') 60 | t.end() 61 | } 62 | }) 63 | }) 64 | }) 65 | 66 | test('basic test', function (t) { 67 | lockFile.check('basic-lock', function (er, locked) { 68 | if (er) throw er 69 | t.notOk(locked) 70 | lockFile.lock('basic-lock', function (er) { 71 | if (er) throw er 72 | lockFile.lock('basic-lock', function (er) { 73 | t.ok(er) 74 | lockFile.check('basic-lock', function (er, locked) { 75 | if (er) throw er 76 | t.ok(locked) 77 | lockFile.unlock('basic-lock', function (er) { 78 | if (er) throw er 79 | lockFile.check('basic-lock', function (er, locked) { 80 | if (er) throw er 81 | t.notOk(locked) 82 | t.end() 83 | }) 84 | }) 85 | }) 86 | }) 87 | }) 88 | }) 89 | }) 90 | 91 | test('sync test', function (t) { 92 | var locked 93 | locked = lockFile.checkSync('sync-lock') 94 | t.notOk(locked) 95 | lockFile.lockSync('sync-lock') 96 | locked = lockFile.checkSync('sync-lock') 97 | t.ok(locked) 98 | lockFile.unlockSync('sync-lock') 99 | locked = lockFile.checkSync('sync-lock') 100 | t.notOk(locked) 101 | t.end() 102 | }) 103 | 104 | test('exit cleanup test', function (t) { 105 | var child = require.resolve('./fixtures/child.js') 106 | var node = process.execPath 107 | var spawn = require('child_process').spawn 108 | spawn(node, [child]).on('exit', function () { 109 | setTimeout(function () { 110 | var locked = lockFile.checkSync('never-forget') 111 | t.notOk(locked) 112 | t.end() 113 | }, 100) 114 | }) 115 | }) 116 | 117 | test('error exit cleanup test', function (t) { 118 | var child = require.resolve('./fixtures/bad-child.js') 119 | var node = process.execPath 120 | var spawn = require('child_process').spawn 121 | spawn(node, [child]).on('exit', function () { 122 | setTimeout(function () { 123 | var locked = lockFile.checkSync('never-forget') 124 | t.notOk(locked) 125 | t.end() 126 | }, 100) 127 | }) 128 | }) 129 | 130 | 131 | test('staleness test', function (t) { 132 | lockFile.lock('stale-lock', function (er) { 133 | if (er) throw er 134 | 135 | // simulate 2s old 136 | touch.sync('stale-lock', { time: new Date(Date.now() - 2000) }) 137 | 138 | var opts = { stale: 1 } 139 | lockFile.check('stale-lock', opts, function (er, locked) { 140 | if (er) throw er 141 | t.notOk(locked) 142 | lockFile.lock('stale-lock', opts, function (er) { 143 | if (er) throw er 144 | lockFile.unlock('stale-lock', function (er) { 145 | if (er) throw er 146 | t.end() 147 | }) 148 | }) 149 | }) 150 | }) 151 | }) 152 | 153 | test('staleness sync test', function (t) { 154 | var opts = { stale: 1 } 155 | lockFile.lockSync('stale-lock') 156 | // simulate 2s old 157 | touch.sync('stale-lock', { time: new Date(Date.now() - 2000) }) 158 | var locked 159 | locked = lockFile.checkSync('stale-lock', opts) 160 | t.notOk(locked) 161 | lockFile.lockSync('stale-lock', opts) 162 | lockFile.unlockSync('stale-lock') 163 | t.end() 164 | }) 165 | 166 | test('retries', function (t) { 167 | // next 5 opens will fail. 168 | var opens = 5 169 | fs._open = fs.open 170 | fs.open = function (path, mode, cb) { 171 | if (--opens === 0) { 172 | fs.open = fs._open 173 | return fs.open(path, mode, cb) 174 | } 175 | var er = new Error('bogus') 176 | // to be, or not to be, that is the question. 177 | er.code = opens % 2 ? 'EEXIST' : 'ENOENT' 178 | process.nextTick(cb.bind(null, er)) 179 | } 180 | 181 | lockFile.lock('retry-lock', { retries: opens }, function (er) { 182 | if (er) throw er 183 | t.equal(opens, 0) 184 | lockFile.unlockSync('retry-lock') 185 | t.end() 186 | }) 187 | }) 188 | 189 | test('retryWait', function (t) { 190 | // next 5 opens will fail. 191 | var opens = 5 192 | fs._open = fs.open 193 | fs.open = function (path, mode, cb) { 194 | if (--opens === 0) { 195 | fs.open = fs._open 196 | return fs.open(path, mode, cb) 197 | } 198 | var er = new Error('bogus') 199 | // to be, or not to be, that is the question. 200 | er.code = opens % 2 ? 'EEXIST' : 'ENOENT' 201 | process.nextTick(cb.bind(null, er)) 202 | } 203 | 204 | var opts = { retries: opens, retryWait: 100 } 205 | lockFile.lock('retry-lock', opts, function (er) { 206 | if (er) throw er 207 | t.equal(opens, 0) 208 | lockFile.unlockSync('retry-lock') 209 | t.end() 210 | }) 211 | }) 212 | 213 | test('retry sync', function (t) { 214 | // next 5 opens will fail. 215 | var opens = 5 216 | fs._openSync = fs.openSync 217 | fs.openSync = function (path, mode) { 218 | if (--opens === 0) { 219 | fs.openSync = fs._openSync 220 | return fs.openSync(path, mode) 221 | } 222 | var er = new Error('bogus') 223 | // to be, or not to be, that is the question. 224 | er.code = opens % 2 ? 'EEXIST' : 'ENOENT' 225 | throw er 226 | } 227 | 228 | var opts = { retries: opens } 229 | lockFile.lockSync('retry-lock', opts) 230 | t.equal(opens, 0) 231 | lockFile.unlockSync('retry-lock') 232 | t.end() 233 | }) 234 | 235 | test('wait and stale together', function (t) { 236 | // first locker. 237 | var interval 238 | lockFile.lock('stale-wait-lock', function(er) { 239 | // keep refreshing the lock, so we keep it forever 240 | interval = setInterval(function() { 241 | touch.sync('stale-wait-lock') 242 | }, 10) 243 | 244 | // try to get another lock. this must fail! 245 | var opt = { stale: 1000, wait: 2000, pollInterval: 1000 } 246 | lockFile.lock('stale-wait-lock', opt, function (er) { 247 | if (!er) 248 | t.fail('got second lock? that unpossible!') 249 | else 250 | t.pass('second lock failed, as i have foreseen it') 251 | clearInterval(interval) 252 | t.end() 253 | }) 254 | }) 255 | }) 256 | 257 | 258 | test('stale windows file tunneling test', function (t) { 259 | // for windows only 260 | // nt file system tunneling feature will make file creation time not updated 261 | var opts = { stale: 1000 } 262 | lockFile.lockSync('stale-windows-lock') 263 | touch.sync('stale-windows-lock', { time: new Date(Date.now() - 3000) }) 264 | 265 | var locked 266 | lockFile.unlockSync('stale-windows-lock') 267 | lockFile.lockSync('stale-windows-lock', opts) 268 | locked = lockFile.checkSync('stale-windows-lock', opts) 269 | t.ok(locked, "should be locked and not stale") 270 | lockFile.lock('stale-windows-lock', opts, function (er) { 271 | if (!er) 272 | t.fail('got second lock? impossible, windows file tunneling problem!') 273 | else 274 | t.pass('second lock failed, windows file tunneling problem fixed') 275 | t.end() 276 | }) 277 | }) 278 | 279 | 280 | test('cleanup', function (t) { 281 | try { lockFile.unlockSync('basic-lock') } catch (er) {} 282 | try { lockFile.unlockSync('sync-lock') } catch (er) {} 283 | try { lockFile.unlockSync('never-forget') } catch (er) {} 284 | try { lockFile.unlockSync('stale-lock') } catch (er) {} 285 | try { lockFile.unlockSync('watch-lock') } catch (er) {} 286 | try { lockFile.unlockSync('retry-lock') } catch (er) {} 287 | try { lockFile.unlockSync('contentious-lock') } catch (er) {} 288 | try { lockFile.unlockSync('stale-wait-lock') } catch (er) {} 289 | try { lockFile.unlockSync('stale-windows-lock') } catch (er) {} 290 | t.end() 291 | }) 292 | 293 | --------------------------------------------------------------------------------