pax_global_header00006660000000000000000000000064130773543160014523gustar00rootroot0000000000000052 comment=f53b29c6c74b895ba7d5b58938e355ebf20b7bce node-proper-lockfile-2.0.1/000077500000000000000000000000001307735431600155435ustar00rootroot00000000000000node-proper-lockfile-2.0.1/.editorconfig000066400000000000000000000004021307735431600202140ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false [*.log] insert_final_newline = false [package.json] indent_size = 2 node-proper-lockfile-2.0.1/.eslintrc.json000066400000000000000000000002651307735431600203420ustar00rootroot00000000000000{ "root": true, "extends": [ "@satazor/eslint-config/es6", "@satazor/eslint-config/addons/node", "@satazor/eslint-config/addons/node-v4-es6" ] } node-proper-lockfile-2.0.1/.gitignore000066400000000000000000000001311307735431600175260ustar00rootroot00000000000000/node_modules /npm-debug.* /test/tmp* /test/nonexistentfile* /test/*.log /test/coverage node-proper-lockfile-2.0.1/.npmignore000066400000000000000000000000101307735431600175310ustar00rootroot00000000000000.* test node-proper-lockfile-2.0.1/.travis.yml000066400000000000000000000001111307735431600176450ustar00rootroot00000000000000language: node_js node_js: - "4" - "6" script: "npm run test-travis" node-proper-lockfile-2.0.1/LICENSE000066400000000000000000000020401307735431600165440ustar00rootroot00000000000000Copyright (c) 2014 IndigoUnited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. node-proper-lockfile-2.0.1/README.md000066400000000000000000000174171307735431600170340ustar00rootroot00000000000000# proper-lockfile [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] [npm-url]:https://npmjs.org/package/proper-lockfile [downloads-image]:http://img.shields.io/npm/dm/proper-lockfile.svg [npm-image]:http://img.shields.io/npm/v/proper-lockfile.svg [travis-url]:https://travis-ci.org/IndigoUnited/node-proper-lockfile [travis-image]:http://img.shields.io/travis/IndigoUnited/node-proper-lockfile/master.svg [coveralls-url]:https://coveralls.io/r/IndigoUnited/node-proper-lockfile [coveralls-image]:https://img.shields.io/coveralls/IndigoUnited/node-proper-lockfile/master.svg [david-dm-url]:https://david-dm.org/IndigoUnited/node-proper-lockfile [david-dm-image]:https://img.shields.io/david/IndigoUnited/node-proper-lockfile.svg [david-dm-dev-url]:https://david-dm.org/IndigoUnited/node-proper-lockfile#info=devDependencies [david-dm-dev-image]:https://img.shields.io/david/dev/IndigoUnited/node-proper-lockfile.svg An inter-process and inter-machine lockfile utility that works on a local or network file system. ## Installation `$ npm install proper-lockfile` ## Design There are various ways to achieve [file locking](http://en.wikipedia.org/wiki/File_locking). This library utilizes the `mkdir` strategy which works atomically on any kind of file system, even network based ones. The lockfile path is based on the file path you are trying to lock by suffixing it with `.lock`. When a lock is successfully acquired, the lockfile's `mtime` (modified time) is periodically updated to prevent staleness. This allows to effectively check if a lock is stale by checking its `mtime` against a stale threshold. If the update of the mtime fails several times, the lock might be compromised. The `mtime` is [supported](http://en.wikipedia.org/wiki/Comparison_of_file_systems) in almost every `filesystem`. ### Comparison This library is similar to [lockfile](https://github.com/isaacs/lockfile) but the later has some drawbacks: - It relies on `open` with `O_EXCL` flag which has problems in network file systems. `proper-lockfile` uses `mkdir` which doesn't have this issue. > O_EXCL is broken on NFS file systems; programs which rely on it for performing locking tasks will contain a race condition. - The lockfile staleness check is done via `ctime` (creation time) which is unsuitable for long running processes. `proper-lockfile` constantly updates lockfiles `mtime` to do proper staleness check. - It does not check if the lockfile was compromised which can led to undesirable situations. `proper-lockfile` checks the lockfile when updating the `mtime`. ### Compromised `proper-lockfile` does not detect cases in which: - A `lockfile` is manually removed and someone else acquires the lock right after - Different `stale`/`update` values are being used for the same file, possibly causing two locks to be acquired on the same file `proper-lockfile` detects cases in which: - Updates to the `lockfile` fail - Updates take longer than expected, possibly causing the lock to became stale for a certain amount of time As you see, the first two are a consequence of bad usage. Technically, it was possible to detect the first two but it would introduce complexity and eventual race conditions. ## Usage ### .lock(file, [options], [compromised], callback) Tries to acquire a lock on `file`. If the lock succeeds, a `release` function is provided that should be called when you want to release the lock. If the lock gets compromised, the `compromised` function will be called. The default `compromised` function is a simple `throw err` which will probably cause the process to die. Specify it to handle the way you desire. Available options: - `stale`: Duration in milliseconds in which the lock is considered stale, defaults to `10000` (minimum value is `5000`) - `update`: The interval in milliseconds in which the lockfile's `mtime` will be updated, defaults to `stale/2` (minimum value is `1000`, maximum value is `stale/2`) - `retries`: The number of retries or a [retry](https://www.npmjs.org/package/retry) options object, defaults to `0` - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) - `fs`: A custom fs to use, defaults to `graceful-fs` ```js const lockfile = require('proper-lockfile'); lockfile.lock('some/file', (err, release) => { if (err) { throw err; // Lock failed } // Do something while the file is locked // Call the provided release function when you're done release(); // Note that you can optionally handle release errors // Though it's not mandatory since it will eventually stale /*release((err) => { // At this point the lock was effectively released or an error // occurred while removing it if (err) { throw err; } });*/ }); ``` ### .unlock(file, [options], [callback]) Releases a previously acquired lock on `file`. Whenever possible you should use the `release` function instead (as exemplified above). Still there are cases in which its hard to keep a reference to it around code. In those cases `unlock()` might be handy. The `callback` is optional because even if the removal of the lock failed, the lockfile's `mtime` will no longer be updated causing it to eventually stale. Available options: - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) - `fs`: A custom fs to use, defaults to `graceful-fs` ```js const lockfile = require('proper-lockfile'); lockfile.lock('some/file', (err) => { if (err) { throw err; } // Later.. lockfile.unlock('some/file'); // or.. /*lockfile.unlock('some/file', (err) => { // At this point the lock was effectively released or an error // occurred while removing it if (err) { throw err; } });*/ }); ``` ### .check(file, [options], callback) Check if the file is locked and its lockfile is not stale. Callback is called with callback(error, isLocked). Available options: - `stale`: Duration in milliseconds in which the lock is considered stale, defaults to `10000` (minimum value is `5000`) - `realpath`: Resolve symlinks using realpath, defaults to `true` (note that if `true`, the `file` must exist previously) - `fs`: A custom fs to use, defaults to `graceful-fs` ```js const lockfile = require('proper-lockfile'); lockfile.check('some/file', (err, isLocked) => { if (err) { throw err; } // isLocked will be true if 'some/file' is locked, false otherwise }); ``` ### .lockSync(file, [options], [compromised]) Sync version of `.lock()`. Returns the `release` function or throws on error. ### .unlockSync(file, [options]) Sync version of `.unlock()`. Throws on error. ### .checkSync(file, [options]) Sync version of `.check()`. Returns a boolean or throws on error. ## Graceful exit `proper-lockfile` automatically remove locks if the process exists. Though, `SIGINT` and `SIGTERM` signals are handled differently by `nodejs` in the sense that they do not fire a `exit` event on the `process`. To avoid this common issue that `CLI` developers have, please do the following: ```js // Map SIGINT & SIGTERM to process exit // so that lockfile removes the lockfile automatically process .once('SIGINT', () => process.exit(1)) .once('SIGTERM', () => process.exit(1)); ``` ## Tests `$ npm test` `$ npm test-cov` to get coverage report The test suite is very extensive. There's even a stress test to guarantee exclusiveness of locks. ## License Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php). node-proper-lockfile-2.0.1/index.js000066400000000000000000000256061307735431600172210ustar00rootroot00000000000000'use strict'; const fs = require('graceful-fs'); const path = require('path'); const retry = require('retry'); const syncFs = require('./lib/syncFs'); const locks = {}; function getLockFile(file) { return `${file}.lock`; } function canonicalPath(file, options, callback) { if (!options.realpath) { return callback(null, path.resolve(file)); } // Use realpath to resolve symlinks // It also resolves relative paths options.fs.realpath(file, callback); } function acquireLock(file, options, callback) { // Use mkdir to create the lockfile (atomic operation) options.fs.mkdir(getLockFile(file), (err) => { // If successful, we are done if (!err) { return callback(); } // If error is not EEXIST then some other error occurred while locking if (err.code !== 'EEXIST') { return callback(err); } // Otherwise, check if lock is stale by analyzing the file mtime if (options.stale <= 0) { return callback(Object.assign(new Error('Lock file is already being hold'), { code: 'ELOCKED', file })); } options.fs.stat(getLockFile(file), (err, stat) => { if (err) { // Retry if the lockfile has been removed (meanwhile) // Skip stale check to avoid recursiveness if (err.code === 'ENOENT') { return acquireLock(file, Object.assign({}, options, { stale: 0 }), callback); } return callback(err); } if (!isLockStale(stat, options)) { return callback(Object.assign(new Error('Lock file is already being hold'), { code: 'ELOCKED', file })); } // If it's stale, remove it and try again! // Skip stale check to avoid recursiveness removeLock(file, options, (err) => { if (err) { return callback(err); } acquireLock(file, Object.assign({}, options, { stale: 0 }), callback); }); }); }); } function isLockStale(stat, options) { return stat.mtime.getTime() < Date.now() - options.stale; } function removeLock(file, options, callback) { // Remove lockfile, ignoring ENOENT errors options.fs.rmdir(getLockFile(file), (err) => { if (err && err.code !== 'ENOENT') { return callback(err); } callback(); }); } function updateLock(file, options) { const lock = locks[file]; /* istanbul ignore next */ if (lock.updateTimeout) { return; } lock.updateDelay = lock.updateDelay || options.update; lock.updateTimeout = setTimeout(() => { const mtime = Date.now() / 1000; lock.updateTimeout = null; options.fs.utimes(getLockFile(file), mtime, mtime, (err) => { // Ignore if the lock was released if (lock.released) { return; } // Verify if we are within the stale threshold if (lock.lastUpdate <= Date.now() - options.stale && lock.lastUpdate > Date.now() - options.stale * 2) { return compromisedLock(file, lock, Object.assign(new Error(lock.updateError || 'Unable to update lock within the stale \ threshold'), { code: 'ECOMPROMISED' })); } // If the file is older than (stale * 2), we assume the clock is moved manually, // which we consider a valid case // If it failed to update the lockfile, keep trying unless // the lockfile was deleted! if (err) { if (err.code === 'ENOENT') { return compromisedLock(file, lock, Object.assign(err, { code: 'ECOMPROMISED' })); } lock.updateError = err; lock.updateDelay = 1000; return updateLock(file, options); } // All ok, keep updating.. lock.lastUpdate = Date.now(); lock.updateError = null; lock.updateDelay = null; updateLock(file, options); }); }, lock.updateDelay); // Unref the timer so that the nodejs process can exit freely // This is safe because all acquired locks will be automatically released // on process exit // We first check that `lock.updateTimeout.unref` exists because some users // may be using this module outside of NodeJS (e.g., in an electron app), // and in those cases `setTimeout` return an integer. if (lock.updateTimeout.unref) { lock.updateTimeout.unref(); } } function compromisedLock(file, lock, err) { lock.released = true; // Signal the lock has been released /* istanbul ignore next */ lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update if (locks[file] === lock) { delete locks[file]; } lock.compromised(err); } // ----------------------------------------- function lock(file, options, compromised, callback) { if (typeof options === 'function') { callback = compromised; compromised = options; options = null; } if (!callback) { callback = compromised; compromised = null; } options = Object.assign({ stale: 10000, update: null, realpath: true, retries: 0, fs, }, options); options.retries = options.retries || 0; options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; options.stale = Math.max(options.stale || 0, 2000); options.update = options.update == null ? options.stale / 2 : options.update || 0; options.update = Math.max(Math.min(options.update, options.stale / 2), 1000); compromised = compromised || function (err) { throw err; }; // Resolve to a canonical file path canonicalPath(file, options, (err, file) => { if (err) { return callback(err); } // Attempt to acquire the lock const operation = retry.operation(options.retries); operation.attempt(() => { acquireLock(file, options, (err) => { if (operation.retry(err)) { return; } if (err) { return callback(operation.mainError()); } // We now own the lock const lock = locks[file] = { options, compromised, lastUpdate: Date.now(), }; // We must keep the lock fresh to avoid staleness updateLock(file, options); callback(null, (releasedCallback) => { if (lock.released) { return releasedCallback && releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' })); } // Not necessary to use realpath twice when unlocking unlock(file, Object.assign({}, options, { realpath: false }), releasedCallback); }); }); }); }); } function unlock(file, options, callback) { if (typeof options === 'function') { callback = options; options = null; } options = Object.assign({ fs, realpath: true, }, options); callback = callback || function () {}; // Resolve to a canonical file path canonicalPath(file, options, (err, file) => { if (err) { return callback(err); } // Skip if the lock is not acquired const lock = locks[file]; if (!lock) { return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' })); } lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update lock.released = true; // Signal the lock has been released delete locks[file]; // Delete from locks removeLock(file, options, callback); }); } function lockSync(file, options, compromised) { if (typeof options === 'function') { compromised = options; options = null; } options = options || {}; options.fs = syncFs(options.fs || fs); options.retries = options.retries || 0; options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; // Retries are not allowed because it requires the flow to be sync if (options.retries.retries) { throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' }); } let err; let release; lock(file, options, compromised, (_err, _release) => { err = _err; release = _release; }); if (err) { throw err; } return release; } function unlockSync(file, options) { options = options || {}; options.fs = syncFs(options.fs || fs); let err; unlock(file, options, (_err) => { err = _err; }); if (err) { throw err; } } function check(file, options, callback) { if (typeof options === 'function') { callback = options; options = null; } options = Object.assign({ stale: 10000, realpath: true, fs, }, options); options.stale = Math.max(options.stale || 0, 2000); // Resolve to a canonical file path canonicalPath(file, options, (err, file) => { if (err) { return callback(err); } // Check if lockfile exists options.fs.stat(getLockFile(file), (err, stat) => { if (err) { // if does not exist, file is not locked. Otherwise, callback with error return (err.code === 'ENOENT') ? callback(null, false) : callback(err); } if (options.stale <= 0) { return callback(null, true); } // Otherwise, check if lock is stale by analyzing the file mtime return callback(null, !isLockStale(stat, options)); }); }); } function checkSync(file, options) { options = options || {}; options.fs = syncFs(options.fs || fs); let err; let locked; check(file, options, (_err, _locked) => { err = _err; locked = _locked; }); if (err) { throw err; } return locked; } // Remove acquired locks on exit /* istanbul ignore next */ process.on('exit', () => { Object.keys(locks).forEach((file) => { try { locks[file].options.fs.rmdirSync(getLockFile(file)); } catch (e) { /* empty */ } }); }); module.exports = lock; module.exports.lock = lock; module.exports.unlock = unlock; module.exports.lockSync = lockSync; module.exports.unlockSync = unlockSync; module.exports.check = check; module.exports.checkSync = checkSync; node-proper-lockfile-2.0.1/lib/000077500000000000000000000000001307735431600163115ustar00rootroot00000000000000node-proper-lockfile-2.0.1/lib/syncFs.js000066400000000000000000000015061307735431600201160ustar00rootroot00000000000000'use strict'; function makeSync(fs, name) { const fn = fs[`${name}Sync`]; return function () { const callback = arguments[arguments.length - 1]; const args = Array.prototype.slice.call(arguments, 0, -1); let ret; try { ret = fn.apply(fs, args); } catch (err) { return callback(err); } callback(null, ret); }; } function syncFs(fs) { const fns = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes']; const obj = {}; // Create the sync versions of the methods that we need fns.forEach((name) => { obj[name] = makeSync(fs, name); }); // Copy the rest of the functions for (const key in fs) { if (!obj[key]) { obj[key] = fs[key]; } } return obj; } module.exports = syncFs; node-proper-lockfile-2.0.1/package.json000066400000000000000000000026461307735431600200410ustar00rootroot00000000000000{ "name": "proper-lockfile", "version": "2.0.1", "description": "A inter-process and inter-machine lockfile utility that works on a local or network file system.", "main": "index.js", "scripts": { "lint": "eslint '{*.js,lib/**/*.js,test/**/*.js}' --ignore-pattern=test/coverage", "test": "mocha", "test-cov": "istanbul cover --dir test/coverage _mocha && echo open test/coverage/lcov-report/index.html", "test-travis": "istanbul cover _mocha --report lcovonly && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" }, "bugs": { "url": "https://github.com/IndigoUnited/node-proper-lockfile/issues/" }, "repository": { "type": "git", "url": "git://github.com/IndigoUnited/node-proper-lockfile.git" }, "keywords": [ "lock", "locking", "file", "lockfile", "fs", "rename", "cross", "machine" ], "author": "IndigoUnited (http://indigounited.com)", "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "retry": "^0.10.0" }, "devDependencies": { "@satazor/eslint-config": "^3.1.1", "async": "^2.0.0", "buffered-spawn": "^3.0.0", "coveralls": "^2.11.6", "eslint": "^3.5.0", "eslint-plugin-react": "^6.2.0", "expect.js": "^0.3.1", "istanbul": "^0.4.1", "mocha": "^3.0.2", "rimraf": "^2.5.0", "stable": "^0.1.5" }, "engines": { "node": ">=4.0.0" } } node-proper-lockfile-2.0.1/test/000077500000000000000000000000001307735431600165225ustar00rootroot00000000000000node-proper-lockfile-2.0.1/test/.eslintrc.json000066400000000000000000000001401307735431600213110ustar00rootroot00000000000000{ "env": { "mocha": true }, "rules": { "no-invalid-this": 0 } } node-proper-lockfile-2.0.1/test/fixtures/000077500000000000000000000000001307735431600203735ustar00rootroot00000000000000node-proper-lockfile-2.0.1/test/fixtures/crash.js000066400000000000000000000004011307735431600220240ustar00rootroot00000000000000'use strict'; const fs = require('fs'); const lockfile = require('../../'); const file = `${__dirname}/../tmp`; fs.writeFileSync(file, ''); lockfile.lock(file, (err) => { if (err) { process.exit(25); } throw new Error('crash'); }); node-proper-lockfile-2.0.1/test/fixtures/stress.js000066400000000000000000000070411307735431600222560ustar00rootroot00000000000000'use strict'; const cluster = require('cluster'); const fs = require('fs'); const os = require('os'); const rimraf = require('rimraf'); const sort = require('stable'); const lockfile = require('../../'); const file = `${__dirname}/../tmp`; function printExcerpt(logs, index) { logs.slice(Math.max(0, index - 50), index + 50).forEach((log, index) => { process.stdout.write(`${index + 1} ${log.timestamp} ${log.message}\n`); }); } function master() { const numCPUs = os.cpus().length; let logs = []; let acquired; fs.writeFileSync(file, ''); rimraf.sync(`${file}.lock`); for (let i = 0; i < numCPUs; i += 1) { cluster.fork(); } cluster.on('online', (worker) => { worker.on('message', (data) => { logs.push(data.toString().trim()); }); }); cluster.on('exit', () => { throw new Error('Child died prematurely'); }); setTimeout(() => { cluster.removeAllListeners('exit'); cluster.disconnect(() => { // Parse & sort logs logs = logs.map((log) => { const split = log.split(' '); return { timestamp: Number(split[0]), message: split[1] }; }); logs = sort(logs, (log1, log2) => { if (log1.timestamp > log2.timestamp) { return 1; } if (log1.timestamp < log2.timestamp) { return -1; } if (log1.message === 'LOCK_RELEASED') { return -1; } if (log2.message === 'LOCK_RELEASED') { return 1; } return 0; }); // Validate logs logs.forEach((log, index) => { switch (log.message) { case 'LOCK_ACQUIRED': if (acquired) { process.stdout.write(`\nInconsistent at line ${index + 1}\n`); printExcerpt(logs, index); process.exit(1); } acquired = true; break; case 'LOCK_RELEASED': if (!acquired) { process.stdout.write(`\nInconsistent at line ${index + 1}\n`); printExcerpt(logs, index); process.exit(1); } acquired = false; break; default: // do nothing } }); process.exit(0); }); }, 60000); } function slave() { process.on('disconnect', () => process.exit(0)); const tryLock = () => { setTimeout(() => { process.send(`${Date.now()} LOCK_TRY\n`); lockfile.lock(file, (err, unlock) => { if (err) { process.send(`${Date.now()} LOCK_BUSY\n`); return tryLock(); } process.send(`${Date.now()} LOCK_ACQUIRED\n`); setTimeout(() => { process.send(`${Date.now()} LOCK_RELEASED\n`); unlock((err) => { if (err) { throw err; } tryLock(); }); }, Math.random() * 200); }); }, Math.random() * 100); }; tryLock(); } if (cluster.isMaster) { master(); } else { slave(); } node-proper-lockfile-2.0.1/test/fixtures/unref.js000066400000000000000000000003331307735431600220470ustar00rootroot00000000000000'use strict'; const fs = require('fs'); const lockfile = require('../../'); const file = `${__dirname}/../tmp`; fs.writeFileSync(file, ''); lockfile.lock(file, (err) => { if (err) { throw err; } }); node-proper-lockfile-2.0.1/test/mocha.opts000066400000000000000000000000271307735431600205170ustar00rootroot00000000000000--timeout 10000 --bail node-proper-lockfile-2.0.1/test/test.js000066400000000000000000001011071307735431600200370ustar00rootroot00000000000000'use strict'; const fs = require('graceful-fs'); const path = require('path'); const cp = require('child_process'); const expect = require('expect.js'); const rimraf = require('rimraf'); const spawn = require('buffered-spawn'); const async = require('async'); const lockfile = require('../'); const lockfileContents = fs.readFileSync(`${__dirname}/../index.js`).toString(); const tmpFileRealPath = path.join(__dirname, 'tmp'); const tmpFile = path.relative(process.cwd(), tmpFileRealPath); const tmpFileLock = `${tmpFileRealPath}.lock`; const tmpFileSymlinkRealPath = `${tmpFileRealPath}_symlink`; const tmpFileSymlink = `${tmpFile}_symlink`; const tmpFileSymlinkLock = `${tmpFileSymlinkRealPath}.lock`; const tmpNonExistentFile = path.join(__dirname, 'nonexistentfile'); function clearLocks(callback) { const toUnlock = []; toUnlock.push((callback) => { lockfile.unlock(tmpFile, { realpath: false }, (err) => { callback(!err || err.code === 'ENOTACQUIRED' ? null : err); }); }); toUnlock.push((callback) => { lockfile.unlock(tmpNonExistentFile, { realpath: false }, (err) => { callback(!err || err.code === 'ENOTACQUIRED' ? null : err); }); }); toUnlock.push((callback) => { lockfile.unlock(tmpFileSymlink, { realpath: false }, (err) => { callback(!err || err.code === 'ENOTACQUIRED' ? null : err); }); }); if (fs.existsSync(tmpFileSymlink)) { toUnlock.push((callback) => { lockfile.unlock(tmpFileSymlink, (err) => { callback(!err || err.code === 'ENOTACQUIRED' ? null : err); }); }); } async.parallel(toUnlock, (err) => { if (err) { return callback(err); } rimraf.sync(tmpFile); rimraf.sync(tmpFileLock); rimraf.sync(tmpFileSymlink); rimraf.sync(tmpFileSymlinkLock); callback(); }); } describe('.lock()', () => { beforeEach(() => { fs.writeFileSync(tmpFile, ''); rimraf.sync(tmpFileSymlink); }); afterEach(clearLocks); it('should fail if the file does not exist by default', (next) => { lockfile.lock(tmpNonExistentFile, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ENOENT'); next(); }); }); it('should not fail if the file does not exist and realpath is false', (next) => { lockfile.lock(tmpNonExistentFile, { realpath: false }, (err) => { expect(err).to.not.be.ok(); next(); }); }); it('should fail if impossible to create the lockfile', (next) => { lockfile.lock('nonexistentdir/nonexistentfile', { realpath: false }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ENOENT'); next(); }); }); it('should create the lockfile', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); expect(fs.existsSync(tmpFileLock)).to.be(true); next(); }); }); it('should fail if already locked', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); expect(err.file).to.be(tmpFileRealPath); next(); }); }); }); it('should retry several times if retries were specified', (next) => { lockfile.lock(tmpFile, (err, unlock) => { expect(err).to.not.be.ok(); setTimeout(unlock, 4000); lockfile.lock(tmpFile, { retries: { retries: 5, maxTimeout: 1000 } }, (err) => { expect(err).to.not.be.ok(); next(); }); }); }); it('should use the custom fs', (next) => { const customFs = Object.assign({}, fs); customFs.realpath = function (path, callback) { customFs.realpath = fs.realpath; callback(new Error('foo')); }; lockfile.lock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); next(); }); }); it('should resolve symlinks by default', (next) => { // Create a symlink to the tmp file fs.symlinkSync(tmpFileRealPath, tmpFileSymlinkRealPath); lockfile.lock(tmpFileSymlink, (err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); lockfile.lock(`${tmpFile}/../../test/tmp`, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); next(); }); }); }); }); it('should not resolve symlinks if realpath is false', (next) => { // Create a symlink to the tmp file fs.symlinkSync(tmpFileRealPath, tmpFileSymlinkRealPath); lockfile.lock(tmpFileSymlink, { realpath: false }, (err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, { realpath: false }, (err) => { expect(err).to.not.be.ok(); lockfile.lock(`${tmpFile}/../../test/tmp`, { realpath: false }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); next(); }); }); }); }); it('should remove and acquire over stale locks', (next) => { const mtime = (Date.now() - 60000) / 1000; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); expect(fs.statSync(tmpFileLock).mtime.getTime()).to.be.greaterThan(Date.now() - 3000); next(); }); }); it('should retry if the lockfile was removed when verifying staleness', (next) => { const mtime = (Date.now() - 60000) / 1000; const customFs = Object.assign({}, fs); customFs.stat = function (path, callback) { rimraf.sync(tmpFileLock); fs.stat(path, callback); customFs.stat = fs.stat; }; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.lock(tmpFile, { fs: customFs }, (err) => { expect(err).to.not.be.ok(); expect(fs.statSync(tmpFileLock).mtime.getTime()).to.be.greaterThan(Date.now() - 3000); next(); }); }); it('should retry if the lockfile was removed when verifying staleness (not recursively)', (next) => { const mtime = (Date.now() - 60000) / 1000; const customFs = Object.assign({}, fs); customFs.stat = function (path, callback) { const err = new Error(); err.code = 'ENOENT'; return callback(err); }; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.lock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); next(); }); }); it('should fail if stating the lockfile errors out when verifying staleness', (next) => { const mtime = (Date.now() - 60000) / 1000; const customFs = Object.assign({}, fs); customFs.stat = function (path, callback) { callback(new Error('foo')); }; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.lock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); next(); }); }); it('should fail if removing a stale lockfile errors out', (next) => { const mtime = (Date.now() - 60000) / 1000; const customFs = Object.assign({}, fs); customFs.rmdir = function (path, callback) { callback(new Error('foo')); }; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.lock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); next(); }); }); it('should update the lockfile mtime automatically', (next) => { lockfile.lock(tmpFile, { update: 1000 }, (err) => { expect(err).to.not.be.ok(); let mtime = fs.statSync(tmpFileLock).mtime; // First update occurs at 1000ms setTimeout(() => { const stat = fs.statSync(tmpFileLock); expect(stat.mtime.getTime()).to.be.greaterThan(mtime.getTime()); mtime = stat.mtime; }, 1500); // Second update occurs at 2000ms setTimeout(() => { const stat = fs.statSync(tmpFileLock); expect(stat.mtime.getTime()).to.be.greaterThan(mtime.getTime()); mtime = stat.mtime; next(); }, 2500); }); }); it('should set stale to a minimum of 2000', (next) => { fs.mkdirSync(tmpFileLock); setTimeout(() => { lockfile.lock(tmpFile, { stale: 100 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); }); }, 200); setTimeout(() => { lockfile.lock(tmpFile, { stale: 100 }, (err) => { expect(err).to.not.be.ok(); next(); }); }, 2200); }); it('should set stale to a minimum of 2000 (falsy)', (next) => { fs.mkdirSync(tmpFileLock); setTimeout(() => { lockfile.lock(tmpFile, { stale: false }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); }); }, 200); setTimeout(() => { lockfile.lock(tmpFile, { stale: false }, (err) => { expect(err).to.not.be.ok(); next(); }); }, 2200); }); it('should call the compromised function if ENOENT was detected when updating the lockfile mtime', (next) => { lockfile.lock(tmpFile, { update: 1000 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ECOMPROMISED'); expect(err.message).to.contain('ENOENT'); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); next(); }, next); }, (err) => { expect(err).to.not.be.ok(); rimraf.sync(tmpFileLock); }); }); it('should call the compromised function if failed to update the lockfile mtime too many times', (next) => { const customFs = Object.assign({}, fs); customFs.utimes = function (path, atime, mtime, callback) { callback(new Error('foo')); }; lockfile.lock(tmpFile, { fs: customFs, update: 1000, stale: 5000 }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.contain('foo'); expect(err.code).to.be('ECOMPROMISED'); next(); }, (err) => { expect(err).to.not.be.ok(); }); }); it('should call the compromised function if updating the lockfile took too much time', (next) => { const customFs = Object.assign({}, fs); customFs.utimes = function (path, atime, mtime, callback) { setTimeout(() => { callback(new Error('foo')); }, 6000); }; lockfile.lock(tmpFile, { fs: customFs, update: 1000, stale: 5000 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ECOMPROMISED'); expect(err.message).to.contain('threshold'); expect(fs.existsSync(tmpFileLock)).to.be(true); next(); }, (err) => { expect(err).to.not.be.ok(); }); }); it('should call the compromised function if lock was acquired by someone else due to staleness', (next) => { const customFs = Object.assign({}, fs); customFs.utimes = function (path, atime, mtime, callback) { setTimeout(() => { callback(new Error('foo')); }, 6000); }; lockfile.lock(tmpFile, { fs: customFs, update: 1000, stale: 5000 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ECOMPROMISED'); expect(fs.existsSync(tmpFileLock)).to.be(true); next(); }, (err) => { expect(err).to.not.be.ok(); setTimeout(() => { lockfile.lock(tmpFile, { stale: 5000 }, (err) => { expect(err).to.not.be.ok(); }); }, 5500); }); }); it('should throw an error by default when the lock is compromised', (next) => { const originalException = process.listeners('uncaughtException').pop(); process.removeListener('uncaughtException', originalException); process.once('uncaughtException', (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ECOMPROMISED'); process.nextTick(() => { process.on('uncaughtException', originalException); next(); }); }); lockfile.lock(tmpFile, { update: 1000 }, (err) => { expect(err).to.not.be.ok(); rimraf.sync(tmpFileLock); }); }); it('should set update to a minimum of 1000', (next) => { lockfile.lock(tmpFile, { update: 100 }, (err) => { const mtime = fs.statSync(tmpFileLock).mtime.getTime(); expect(err).to.not.be.ok(); setTimeout(() => { expect(mtime).to.equal(fs.statSync(tmpFileLock).mtime.getTime()); }, 200); setTimeout(() => { expect(fs.statSync(tmpFileLock).mtime.getTime()).to.be.greaterThan(mtime); next(); }, 1200); }); }); it('should set update to a minimum of 1000 (falsy)', (next) => { lockfile.lock(tmpFile, { update: false }, (err) => { const mtime = fs.statSync(tmpFileLock).mtime.getTime(); expect(err).to.not.be.ok(); setTimeout(() => { expect(mtime).to.equal(fs.statSync(tmpFileLock).mtime.getTime()); }, 200); setTimeout(() => { expect(fs.statSync(tmpFileLock).mtime.getTime()).to.be.greaterThan(mtime); next(); }, 1200); }); }); it('should set update to a maximum of stale / 2', (next) => { lockfile.lock(tmpFile, { update: 6000, stale: 5000 }, (err) => { const mtime = fs.statSync(tmpFileLock).mtime.getTime(); expect(err).to.not.be.ok(); setTimeout(() => { expect(fs.statSync(tmpFileLock).mtime.getTime()).to.equal(mtime); }, 2000); setTimeout(() => { expect(fs.statSync(tmpFileLock).mtime.getTime()).to.be.greaterThan(mtime); next(); }, 3000); }); }); }); describe('.unlock()', () => { beforeEach(() => { fs.writeFileSync(tmpFile, ''); rimraf.sync(tmpFileSymlink); }); afterEach(clearLocks); it('should fail if the lock is not acquired', (next) => { lockfile.unlock(tmpFile, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ENOTACQUIRED'); next(); }); }); it('should release the lock', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); next(); }); }); }); }); it('should release the lock (without callback)', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile); setTimeout(() => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); next(); }); }, 2000); }); }); it('should remove the lockfile', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); expect(fs.existsSync(tmpFileLock)).to.be(true); lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); expect(fs.existsSync(tmpFileLock)).to.be(false); next(); }); }); }); it('should fail if removing the lockfile errors out', (next) => { const customFs = Object.assign({}, fs); customFs.rmdir = function (path, callback) { callback(new Error('foo')); }; lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); next(); }); }); }); it('should ignore ENOENT errors when removing the lockfile', (next) => { const customFs = Object.assign({}, fs); let called; customFs.rmdir = function (path, callback) { called = true; rimraf.sync(path); fs.rmdir(path, callback); }; lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile, { fs: customFs }, (err) => { expect(err).to.not.be.ok(); expect(called).to.be(true); next(); }); }); }); it('should stop updating the lockfile mtime', (next) => { lockfile.lock(tmpFile, { update: 2000 }, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); // First update occurs at 2000ms setTimeout(next, 2500); }); }); }); it('should stop updating the lockfile mtime (slow fs)', (next) => { const customFs = Object.assign({}, fs); customFs.utimes = function (path, atime, mtime, callback) { setTimeout(fs.utimes.bind(fs, path, atime, mtime, callback), 2000); }; lockfile.lock(tmpFile, { fs: customFs, update: 2000 }, (err) => { expect(err).to.not.be.ok(); setTimeout(() => { lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); }); }, 3000); setTimeout(next, 6000); }); }); it('should stop updating the lockfile mtime (slow fs + new lock)', (next) => { const customFs = Object.assign({}, fs); customFs.utimes = function (path, atime, mtime, callback) { setTimeout(fs.utimes.bind(fs, path, atime, mtime, callback), 2000); }; lockfile.lock(tmpFile, { fs: customFs, update: 2000 }, (err) => { expect(err).to.not.be.ok(); setTimeout(() => { lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); }); }); }, 3000); setTimeout(next, 6000); }); }); it('should resolve to a canonical path', (next) => { // Create a symlink to the tmp file fs.symlinkSync(tmpFileRealPath, tmpFileSymlinkRealPath); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.unlock(tmpFile, (err) => { expect(err).to.not.be.ok(); expect(fs.existsSync(tmpFileLock)).to.be(false); next(); }); }); }); it('should use the custom fs', (next) => { const customFs = Object.assign({}, fs); customFs.realpath = function (path, callback) { customFs.realpath = fs.realpath; callback(new Error('foo')); }; lockfile.unlock(tmpFile, { fs: customFs }, (err) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); next(); }); }); }); describe('.check()', () => { beforeEach(() => { fs.writeFileSync(tmpFile, ''); rimraf.sync(tmpFileSymlink); }); afterEach(clearLocks); it('should fail if the file does not exist by default', (next) => { lockfile.check(tmpNonExistentFile, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ENOENT'); next(); }); }); it('should not fail if the file does not exist and realpath is false', (next) => { lockfile.check(tmpNonExistentFile, { realpath: false }, (err) => { expect(err).to.not.be.ok(); next(); }); }); it('should callback with true if file is locked', (next) => { lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); lockfile.check(tmpFile, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(true); next(); }); }); }); it('should callback with false if file is not locked', (next) => { lockfile.check(tmpFile, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(false); next(); }); }); it('should use the custom fs', (next) => { const customFs = Object.assign({}, fs); customFs.realpath = function (path, callback) { customFs.realpath = fs.realpath; callback(new Error('foo')); }; lockfile.check(tmpFile, { fs: customFs }, (err, locked) => { expect(err).to.be.an(Error); expect(locked).to.be(undefined); next(); }); }); it('should resolve symlinks by default', (next) => { // Create a symlink to the tmp file fs.symlinkSync(tmpFileRealPath, tmpFileSymlinkRealPath); lockfile.lock(tmpFileSymlink, (err) => { expect(err).to.not.be.ok(); lockfile.check(tmpFile, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(true); lockfile.check(`${tmpFile}/../../test/tmp`, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(true); next(); }); }); }); }); it('should not resolve symlinks if realpath is false', (next) => { // Create a symlink to the tmp file fs.symlinkSync(tmpFileRealPath, tmpFileSymlinkRealPath); lockfile.lock(tmpFileSymlink, { realpath: false }, (err) => { expect(err).to.not.be.ok(); lockfile.check(tmpFile, { realpath: false }, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(false); lockfile.check(`${tmpFile}/../../test/tmp`, { realpath: false }, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.be(false); next(); }); }); }); }); it('should fail if stating the lockfile errors out when verifying staleness', (next) => { const mtime = (Date.now() - 60000) / 1000; const customFs = Object.assign({}, fs); customFs.stat = function (path, callback) { callback(new Error('foo')); }; fs.mkdirSync(tmpFileLock); fs.utimesSync(tmpFileLock, mtime, mtime); lockfile.check(tmpFile, { fs: customFs }, (err, locked) => { expect(err).to.be.an(Error); expect(err.message).to.be('foo'); expect(locked).to.be(undefined); next(); }); }); it('should set stale to a minimum of 2000', (next) => { fs.mkdirSync(tmpFileLock); setTimeout(() => { lockfile.lock(tmpFile, { stale: 2000 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); }); }, 200); setTimeout(() => { lockfile.check(tmpFile, { stale: 100 }, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.equal(false); next(); }); }, 2200); }); it('should set stale to a minimum of 2000 (falsy)', (next) => { fs.mkdirSync(tmpFileLock); setTimeout(() => { lockfile.lock(tmpFile, { stale: 2000 }, (err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ELOCKED'); }); }, 200); setTimeout(() => { lockfile.check(tmpFile, { stale: false }, (err, locked) => { expect(err).to.not.be.ok(); expect(locked).to.equal(false); next(); }); }, 2200); }); }); describe('release()', () => { beforeEach(() => { fs.writeFileSync(tmpFile, ''); }); afterEach(clearLocks); it('should release the lock after calling the provided release function', (next) => { lockfile.lock(tmpFile, (err, release) => { expect(err).to.not.be.ok(); release((err) => { expect(err).to.not.be.ok(); lockfile.lock(tmpFile, (err) => { expect(err).to.not.be.ok(); next(); }); }); }); }); it('should fail when releasing twice', (next) => { lockfile.lock(tmpFile, (err, release) => { expect(err).to.not.be.ok(); release((err) => { expect(err).to.not.be.ok(); release((err) => { expect(err).to.be.an(Error); expect(err.code).to.be('ERELEASED'); next(); }); }); }); }); }); describe('sync api', () => { beforeEach(() => { fs.writeFileSync(tmpFile, ''); rimraf.sync(tmpFileSymlink); }); afterEach(clearLocks); it('should expose a working lockSync', () => { let release; // Test success release = lockfile.lockSync(tmpFile); expect(release).to.be.a('function'); expect(fs.existsSync(tmpFileLock)).to.be(true); release(); expect(fs.existsSync(tmpFileLock)).to.be(false); // Test compromise being passed and no options release = lockfile.lockSync(tmpFile, () => {}); expect(fs.existsSync(tmpFileLock)).to.be(true); release(); expect(fs.existsSync(tmpFileLock)).to.be(false); // Test options being passed and no compromised release = lockfile.lockSync(tmpFile, {}); expect(fs.existsSync(tmpFileLock)).to.be(true); release(); expect(fs.existsSync(tmpFileLock)).to.be(false); // Test both options and compromised being passed release = lockfile.lockSync(tmpFile, {}, () => {}); expect(fs.existsSync(tmpFileLock)).to.be(true); release(); expect(fs.existsSync(tmpFileLock)).to.be(false); // Test fail lockfile.lockSync(tmpFile); expect(() => { lockfile.lockSync(tmpFile); }).to.throwException(/already being hold/); }); it('should not allow retries to be passed', () => { expect(() => { lockfile.lockSync(tmpFile, { retries: 10 }); }).to.throwException(/Cannot use retries/i); expect(() => { lockfile.lockSync(tmpFile, { retries: { retries: 10 } }); }).to.throwException(/Cannot use retries/i); expect(() => { const release = lockfile.lockSync(tmpFile, { retries: 0 }); release(); }).to.not.throwException(); expect(() => { const release = lockfile.lockSync(tmpFile, { retries: { retries: 0 } }); release(); }).to.not.throwException(); }); it('should expose a working unlockSync', () => { // Test success lockfile.lockSync(tmpFile); expect(fs.existsSync(tmpFileLock)).to.be(true); lockfile.unlockSync(tmpFile); expect(fs.existsSync(tmpFileLock)).to.be(false); // Test fail expect(() => { lockfile.unlockSync(tmpFile); }).to.throwException(/not acquired\/owned by you/); }); it('should expose a working checkSync', () => { let release; let locked; // Test success unlocked locked = lockfile.checkSync(tmpFile); expect(locked).to.be.a('boolean'); expect(locked).to.be(false); // Test success locked release = lockfile.lockSync(tmpFile); locked = lockfile.checkSync(tmpFile); expect(locked).to.be.a('boolean'); expect(locked).to.be(true); // Test success unlocked after release release(); locked = lockfile.checkSync(tmpFile); expect(locked).to.be.a('boolean'); expect(locked).to.be(false); // Test options being passed locked = lockfile.checkSync(tmpFile, {}); expect(locked).to.be.a('boolean'); expect(locked).to.be(false); release = lockfile.lockSync(tmpFile); locked = lockfile.checkSync(tmpFile, {}); expect(locked).to.be.a('boolean'); expect(locked).to.be(true); release(); locked = lockfile.checkSync(tmpFile, {}); expect(locked).to.be.a('boolean'); expect(locked).to.be(false); // Test fail with non-existent file expect(() => { lockfile.checkSync('nonexistentdir/nonexistentfile'); }).to.throwException(/ENOENT/); }); it('should update the lockfile mtime automatically', (next) => { let mtime; lockfile.lockSync(tmpFile, { update: 1000 }); mtime = fs.statSync(tmpFileLock).mtime; // First update occurs at 1000ms setTimeout(() => { const stat = fs.statSync(tmpFileLock); expect(stat.mtime.getTime()).to.be.greaterThan(mtime.getTime()); mtime = stat.mtime; }, 1500); // Second update occurs at 2000ms setTimeout(() => { const stat = fs.statSync(tmpFileLock); expect(stat.mtime.getTime()).to.be.greaterThan(mtime.getTime()); mtime = stat.mtime; next(); }, 2500); }); it('should use a custom fs', () => { const customFs = Object.assign({}, fs); let called; customFs.realpathSync = function () { called = true; return fs.realpathSync.apply(fs, arguments); }; lockfile.lockSync(tmpFile, { fs: customFs }); expect(called).to.be(true); }); }); describe('misc', () => { afterEach(clearLocks); it('should not contain suspicious nodejs native fs calls', () => { expect(/\s{2,}fs\.[a-z]+/i.test(lockfileContents)).to.be(false); }); it('should remove open locks if the process crashes', (next) => { cp.exec(`node ${__dirname}/fixtures/crash.js`, (err, stdout, stderr) => { if (!err) { return next(new Error('Should have failed')); } if (err.code === 25) { return next(new Error('Lock failed')); } expect(stderr).to.contain('crash'); expect(fs.existsSync(tmpFileLock)).to.be(false); next(); }); }); it('should not hold the process if it has no more work to do', (next) => { spawn('node', [`${__dirname}/fixtures/unref.js`], next); }); it('should work on stress conditions', function (next) { this.timeout(80000); spawn('node', [`${__dirname}/fixtures/stress.js`], (err, stdout) => { if (err) { stdout = stdout || ''; if (process.env.TRAVIS) { process.stdout.write(stdout); } else { fs.writeFileSync(`${__dirname}/stress.log`, stdout); } return next(err); } next(); }); }); });